shell脚本规范

0. 一般信息

1. 背景(Background)

1.1 用哪个Shell

  • [RULE 1-1] Bash 是公司唯一指定的Shell语言,版本在3.0(含)以上(如果仍然是2.x版本,你的OS很可能已不符合公司规范,请联系OP尽快升级)
  1. # 查看bash版本的方法
  2. $ bash --version
  3. bash --version
  4. GNU bash(bdsh), version 3.00.22(2)-release (x86_64-redhat-linux-gnu)
  5. Copyright (C) 2004 Free Software Foundation, Inc.
  • [RULE 1-2] 请遵守此规范,或保证和原有代码风格、语法、规范一致
  1. 解释:对历史代码,不做强制打平到当前代码规范的要求

1.2 何时不选择使用 Shell

  • [ADVISE 1-1] Shell仅用于开发小工具(small utilities)和包装脚本(wrapper scripts)
  • [ADVISE 1-2] 如果仅仅调用其他程序,或是极少的数据处理,Shell是合适的选择
  • [ADVISE 1-3] 如需要使用hash(Bash3.x以下原生不支持)、嵌套array,建议用其他语言实现
  • [ADVISE 1-4] 对性能要求较高的场景,建议用其他语言实现

2. Shell文件和解释器调用(Shell Files and Interpreter Invocation)

2.1 扩展文件名

  • [RULE 2-1] 可执行Shell脚本无需后缀或使用后缀.sh
  • [RULE 2-2] 库文件必须用后缀.sh

2.2 SUID/SGID

  • [RULE 2-3] SUID、SGID禁止使用,需要的时候使用 sudo
  1. 解释:防止脚本静默访问没有权限的资源,如果必须访问请通过sudo显式指定

2.3 Usage

  • [RULE 2-4] 可执行脚本在加-h参数调用时应打印Usage
  1. # 示例:在开发机上可以调用scmtools.sh -h观察此脚本的Usage
  2. # 提示:脚本输入参数的处理可以参考:getopt、case等
  3. $ scmtools.sh -h
  4. scmtools.sh -h
  5. Usage:
  6. source scmtools [option]...
  7. Options:
  8. -h|--help
  9. -v|--version
  10. -u|--update install/update scmtools client
  11. -s|--show-status show status of scmtools
  12. -E|--enable-shared-ccache enable ccache with shared cache mode for muti-users
  13. -F|--enable-safe-ccache enable ccache with no impact on build date
  14. -c|--disable-ccache disable ccache
  15. -D|--enable-dynamic-distcc enable distcc with dynamic scheduler
  16. -C|--disable-dynamic-distcc disable dynamic distcc
  17. -O|--enable-ccover enable ccover
  18. -o|--disable-ccover disable ccover
  19. -w|--clean-scmtools-whole clean whole scmtools environment
  20. -M|--enable-module-cache enable build module cache
  21. -S|--enable-source-module-cache enable source module cache
  22. -m|--disable-module-cache disable module cache
  23. --enable-L2-cache enable L2 cache
  24. --disable-L2-cache disable L2 cache
  25. --enable-btest enable btest
  26. --disable-btest disable btest

3. 书写样板(Layout)

3.1 样板(Layout)

  • [ADVISE 3-1] 按照开关,环境变量,source文件,常量,变量,函数,主函数/主逻辑的顺序书写脚本
  1. #!/bin/bash
  2. # 1. 开关
  3. set -x # set -o xtrace
  4. set -e # set -o errexit
  5. set -u # set -o nounset
  6. set -o pipefail
  7. # 2. 环境变量
  8. PATH=/home/abc/bin:$PATH
  9. export PATH
  10. # 3. source文件
  11. source lib/some_lib.sh
  12. # 4. 常量
  13. readonly PI=3.14
  14. # 5. 变量
  15. my_var=1
  16. # 6. 函数
  17. # Usage
  18. function usage() {
  19. }
  20. # 函数注释,格式参考注释章节
  21. function my_func1() {
  22. }
  23. # 函数注释,格式参考注释章节
  24. function my_func2() {
  25. }
  26. # 函数注释,格式参考注释章节
  27. function main() {
  28. }
  29. # 7. 主函数/主逻辑
  30. main "$@"

4. 命名规范(Naming Conventions)

4.1 常量/环境变量名(Constants and Environment Variable Names)

  • [RULE 4-1] 全部大写,下划线分割,且在文件头部声明
  • [RULE 4-2] 所有需要export的变量都要大写
  1. #配置文件常量
  2. FILE_PATH="/home/spider/conf/"
  3. #全局常量
  4. declare -r MAX_PATH_SIZE=256
  5. #环境变量
  6. export PYPATH="/home/spider/python"

4.2 source文件名(Source Filenames)

  • [RULE 4-3] 被source的文件,其文件名全部小写,下划线分割单词,且要写明路径(相对路径或者绝对路径)。
  1. source lib/color_print.sh

4.3 常量(Read-only Variables)

  • [RULE 4-4] 使用readonlydeclare -r,明确声明常量
  1. # 使用readonly声明方式
  2. readonly NAME='spider'
  3. # 使用declare声明方式
  4. declare -r NAME='spider'

4.4 变量名(Variable Names)

  • [RULE 4-5] 命名全部小写,下划线分割单词
  • [ADVISE 4-1] 多个关联的变量名在含义上保持风格一致,比如for in循环的时候
  1. # $url , $url_list 含义一致
  2. for url in ${url_list}; do
  3. something_with "${url}"
  4. done

4.5 函数名(Function Names)

  • [RULE 4-6] 命名全部小写,下划线分割单词
  • [RULE 4-7] 小括号必须在函数名以后,不能有空格
  • [RULE 4-8] 函数的左大括号与函数名在同一行
  • [RULE 4-9] 使用function关键字
  • [ADVISE 4-2] 使用 :: 区分包、库
  1. function my_func() {
  2. ...
  3. }
  4. function lib::my_func() {
  5. ...
  6. }

4.6 local变量(Use Local Variables)

  • [RULE 4-10] 使用local声明函数内部使用的变量
  1. # Good
  2. function my_func2() {
  3. # 声明和赋值在一行
  4. local name="$1"
  5. # 声明和赋值拆为2行
  6. local my_var
  7. my_var="$1"
  8. }
  9. # Bad
  10. function my_fun3() {
  11. # 不要这样用,$?为local的返回值,而不是my_func
  12. local my_var="$(my_func)"
  13. [[ $? -eq 0 ]] || return
  14. }

4.7 main函数

  • [ADVISE 4-3] 对于长脚本,使用main函数放到所有函数声明的后面
  1. # 参见第三章例子
  2. function main() {
  3. }
  4. main "$@"

5. 注释(Comments)

5.1 注释样式

  • [RULE 5-1] 采用单行注释#
  1. # comments
  • [ADVISE 5-1] 使用utf8编码(尽量用英文注释)
  • [ADVISE 5-2] 仅调试时才使用多行注释,多行注释建议使用 :<<\###的方式,方便开关
  1. :<<\### 下面是本次要注释的内容
  2. do_something
  3. do_other_thing
  4. ###
  5. 如果需要执行这段代码,则
  6. #:<<\### 下面是本次要注释的内容
  7. do_something
  8. do_other_thing
  9. ###

5.2 文件头注释(File Header)

  • [RULE 5-2] 脚本第一行为 #!/bin/bash
  1. # Good
  2. #!/bin/bash
  3. # Bad
  4. #!/bin/sh
  5. #!/bin/bash -x
  • [RULE 5-3] 脚本顶部必须有对脚本功能的说明
  • [ADVISE 5-3] 注释内容可包括:版权说明、作者、时间、代码功能、用法说明、全局变量、命令行参数、返回值
  1. #!/bin/bash
  2. #
  3. # Copyright (c) 2015 Baidu.com, Inc. All Rights Reserved
  4. #
  5. # Author: XXX
  6. # Date: 2015/7/28
  7. # Brief:
  8. # Perform hot backups of Oracle databases.
  9. # Globals:
  10. # BACKUP_DIR
  11. # Arguments:
  12. # None
  13. # Returns:
  14. # succ:0
  15. # fail:1

5.3 函数注释(Function Comments)

  • [RULE 5-4] 函数需要有注释。
  • [ADVISE 5-4] 注释内容可包括:函数功能,全局变量,参数,返回值。对于简单的函数,如:main,usage可以不加注释或者只加1行注释
  1. #######################################
  2. # Brief:
  3. # Cleanup files from the backup dir.
  4. # Globals:
  5. # BACKUP_DIR
  6. # Arguments:
  7. # None
  8. # Returns:
  9. # None
  10. #######################################
  11. function cleanup() {
  12. rm -rf ${BACKUP_DIR}
  13. }
  14. # usage
  15. function usage() {
  16. }
  17. function main() {
  18. }

5.4 TODO 注释(TODO Comments)

  • [ADVISE 5-5] 使用 “#TODO(添加者)” 标注将来还要做的工作
  1. # TODO(Someone): xxx (issue ####)

6. 格式(Formatting)

6.1 缩进

  • [RULE 6-1] 用4个空格,不要用tab
  1. # 解释:不同编辑器对TAB的设定可能不同,使用TAB容易造成在一些编辑器下代码混乱,所以建议一率转换成空格。
  2. # 在vim下,建议打开如下设置:
  3. # :set tabstop=4 设定tab宽度为4个字符
  4. # :set shiftwidth=4 设定自动缩进为4个字符
  5. # :set expandtab 用space自动替代tab
  6. # 对于原有脚本,可以使用:set list查看是否存在tab,并使用sed或vim的替换命令进行统一转换。

6.2 分号

  • [RULE 6-2]行尾不使用;,除非必要
  1. # 解释:通常情况下,行尾的分号没有意义。
  2. # 特例:find工具
  3. find . -type f -exec grep "something" {} \;

6.3 单行长度

  • [ADVISE 6-1] 一行不要超过80个字符,可以使用引号、 \ 、here document 等方式换行
  1. # 解释:由于屏幕宽度有限,单行长度不宜超过80字符。
  2. # 80个字符的长度如下:
  3. #0123456789012345678901234567890123456789012345678901234567890123456789012345678
  4. # Good
  5. # 使用here doc换行。
  6. cat <<END
  7. I am an exceptionally long
  8. string.
  9. END
  10. # 使用引号换行。
  11. long_string="I am an exceptionally
  12. long string."
  13. # 使用\换行。
  14. echo "An exceptionally long string." \
  15. | grep "long string"
  • [RULE 6-3] 文件名和路径名不能折行
  1. # 解释:折行会破坏文件名和路径名完整性,极易引发错误。
  2. # Good
  3. # 使用拼接来缩短单行长度。
  4. dir_path="/home/work/workspace/myspace"
  5. file_name="this_is_a_file"
  6. file_path="${dir_path}/${file_name}"
  7. # Bad
  8. # 对路径名使用引号折行。这会导致路径中包含换行,路径错误。
  9. file_path="/home/work/workspace/myspace
  10. /this_is_a_file"
  11. # 对文件名使用\折行。除非破坏缩进顶格换行,否则都会在路径中引入缩进的空格,路径错误。
  12. file_name="this_is_\
  13. a_file"

6.4 管道(Pipe)

  • [RULE 6-4] 如果使用一个或多个管道导致超长,只允许使用 \ 换行
  1. # 解释:超长的管道不易拆分,而引号和here doc等折行方式通常仅适用于字符串,因此使用\换行。
  2. # Short commands
  3. command1 | command2
  4. # Long commands
  5. command1 \
  6. | command2 \
  7. | command3 \
  8. | command4

6.5 条件和循环(Loops and Condition)

  • [RULE 6-5] ; do 和 ; then,必须和 forifelif 、while 在一行,elsefidone独自一行,并与forifwhile垂直首部对齐
  1. # 解释:do和then与C语言中的{类似,fi和done与}类似,也采用类似的折行与对齐方式,便于阅读。
  2. for dir in ${dirs_to_cleanup}; do
  3. if [[ -d "${dir}/${ORACLE_SID}" ]]; then
  4. echo "Cleaning up old files in ${dir}/${ORACLE_SID}"
  5. rm "${dir}/${ORACLE_SID}/"*
  6. if [[ "$?" -ne 0 ]]; then
  7. echo "Clean up old files error" >&2
  8. fi
  9. else
  10. mkdir -p "${dir}/${ORACLE_SID}"
  11. if [[ "$?" -ne 0 ]]; then
  12. echo "Create directory ${dir}/${ORACLE_SID} error" >&2
  13. fi
  14. fi
  15. done

6.6 Case语句

  • [RULE 6-6]不能混用单行和多行写法
  • [RULE 6-7]单行写法需要在)后 和 ;;前增加1个空格
  • [RULE 6-8]多行写法,需要将每个语句拆成1行
  1. # 解释:case语句往往较长,若不保持格式统一,会对阅读造成很大障碍。
  2. # Good
  3. # 单行写法,适用于所有分支都较为简单的场景。
  4. verbose='false'
  5. aflag=''
  6. bflag=''
  7. files=''
  8. while getopts 'abf:v' flag; do
  9. case "${flag}" in
  10. a) aflag='true' ;;
  11. b) bflag='true' ;;
  12. f) files="${OPTARG}" ;;
  13. v) verbose='true' ;;
  14. *) error "Unexpected option ${flag}" ;;
  15. esac
  16. done
  17. # 多行写法,适用于部分或全部分支较为复杂的场景。
  18. case "${expression}" in
  19. a)
  20. variable="xxx"
  21. some_command "${variable}" "${other_expr}"
  22. ;;
  23. absolute)
  24. actions="relative"
  25. another_command "${actions}" "${other_expr}"
  26. ;;
  27. *)
  28. error "Unexpected expression '${expression}'"
  29. ;;
  30. esac
  31. # Bad
  32. # 升级或新增某一分支时,因较为复杂,使用了多行写法,但其它简单分支使用了单行写法,造成混用。
  33. verbose='false'
  34. aflag=''
  35. bflag=''
  36. files=''
  37. while getopts 'abf:hv' flag; do
  38. case "${flag}" in
  39. a) aflag='true' ;;
  40. b) bflag='true' ;;
  41. f) files="${OPTARG}" ;;
  42. h)
  43. usage
  44. exit 0
  45. ;;
  46. v) verbose='true' ;;
  47. *) error "Unexpected option ${flag}" ;;
  48. esac
  49. done

6.7 引用(quoting)

  • [RULE 6-9] 引用包含变量的字符串、命令替换、空格、shell元字符
  1. # 解释:空格和tab等会造成字符串或参数被拆分成多个,引发错误,$、&等元字符更会引发各种控制逻辑。
  2. # 变量以及命令替换的结果中,往往也会包括空格和各种shell元字符。
  3. # 因此,这些都需要加上引号。
  4. # Good
  5. # 涉及空格,需要加引号。
  6. # 如果不加引号,s1中的world会被当成独立命令并报错,s2中更是会删除名为file的文件,s1和s2都不会被赋值。
  7. s1="Hello world"
  8. s2="I rm file"
  9. # 涉及包含变量的字符串,需要加引号。
  10. s3="${s1}. ${s2}"
  11. # 涉及命令替换,需要加引号。
  12. dir_path="$(dirname $BASH_SOURCE)"
  13. # Bad
  14. # 涉及shell元字符,未加引号。&会被视为控制字符,其后的url参数全部无效,并且将wget进程挂到后台。
  15. wget http://wiki.baidu.com/pages/editpage.action?otherParam=xxx&pageId=70992849
  • [ADVISE 6-2] 不引用命令选项、路径名
  1. # 解释:通常在命令行中执行命令时,不会给命令选项以及路径名加引号,脚本中也建议保持一致。
  2. # 但如果路径中存在空格等不加引号就可能出错的情况,仍然需要加上引号。
  3. # Good
  4. # 命令选项和路径名不加引号。
  5. ls -l /home/work
  6. # Bad
  7. # 路径中存在空格,不加引号会被当成两个路径并引发预期外的错误。
  8. ls -l /home/work/my space/workspace
  9. # 使用变量拼接了命令选项,并按RULE 6-9加了引号,这会造成命令选项无法被识别。确认作为命令选项时,不要引用。
  10. param="-l -r -t"
  11. ls "${param}" /home/work
  • [RULE 6-10] 双引号用于引用需要替换的部分;单引号用于引用不替换的部分
  1. # 解释:双引号中的变量、命令等都会被替换,大部分元字符可生效;单引号中不进行替换,大部分shell元字符不生效。
  2. # Good
  3. # s1和s2的值都是当前目录绝对路径。
  4. s1="$PWD"
  5. s2="$(pwd)"
  6. # 建议引用单词字符,不是强制的。
  7. readonly USE_INTEGER='true'
  8. # 单引号中shell元字符无需转义,双引号中需要转义,否则会被解释为其它含义。
  9. echo 'Hello stranger, and well met. Earn lots of $$$'
  10. echo "Process $$: Done making \$\$\$."
  11. # Bad
  12. # s3和s4的值分别是$PWD和$(pwd)这两个字符串,除非有特殊需求,否则是错误的。
  13. s3='$PWD'
  14. s4='$(pwd)'
  • [ADVISE 6-3] 不引用整数字面量
  1. # 解释:根据习惯,整数字面量不使用引用,以示与纯数字字符串区分。
  2. # Good
  3. # 整数字面量不引用,纯数字字符串需要引用。
  4. value=32
  5. id="0123456789012345"
  6. # Bad
  7. # 使用了命令替换,即使是整形,但仍然应该引用。
  8. number=$(generate_number)
  • [RULE 6-11] 使用"$@""$*",$@和$*必须被双引号引用
  1. # 解释:由于参数往往有多个,而每个参数中又很可能有空格或tab,若不引用则往往获取的结果和预期不一样。
  2. # Good
  3. inputs="$*"
  4. main "$@"
  5. # Bad
  6. # 由于$@没加引号,实际输出是每行1个单词,共计4行,而非预期的每行一个参数,共计2行。
  7. # 函数注释,格式参考注释章节。
  8. function dump() {
  9. for i in $@; do
  10. echo "${i}"
  11. done
  12. }
  13. dump "hello world" "bye bye"
  • [ADVISE 6-4] 尽量使用 "$@", 除非你充分的有理由使用 "$*"
  1. # 解释:"$@"保持原样传递参数,而"$*"则会把所有参数合并成一个字符串,多数情况下我们需要的是"$@"的逻辑。
  2. # 向main函数传递参数时,通常都应当使用"$@"。
  3. main "$@"

6.8 变量扩展(Variable expansion)

  • [RULE 6-12] 对于自定义变量,使用"${var}",不用 "$var"
  1. # 解释:自定义变量往往需要参与各种字符串拼接,如果不加括号,既不利于阅读,也很容易导致逻辑错误。
  2. # Good
  3. prefix="pre"
  4. echo "${prefix}_dir/file"
  5. # Bad
  6. # 会把prefix_dir视为变量名,由于无定义,会输出/file,若设置了set -u,则会报错。
  7. prefix="pre"
  8. echo "$prefix_dir/file"

7. 特性和缺陷(Features and Bugs)

7.1 环境变量赋值(Environment Variable Assignment)

  • [RULE 7-1] 环境变量赋值中,严格禁止使用相对路径。
  1. # 解释:环境变量会应用到被调用的其他命令上。若其他命令改变了当前路径,环境变量中的相对路径会发生变化。
  2. # Good
  3. PATH=$(pwd)/bin:$PATH
  4. LD_LIBRARY_PATH=$(pwd)/../lib:$LD_LIBRARY_PATH
  5. export PATH
  6. export LD_LIBRARY_PATH
  7. # Bad
  8. PATH=./bin:$PATH
  9. LD_LIBRARY_PATH=../lib:$LD_LIBRARY_PATH
  10. export PATH
  11. export LD_LIBRARY_PATH

7.2 命令替换(Command Substitution)

  • [RULE 7-2] 使用 $() 代替 反引号 `` 
  1. # 解释:$() 语法可读性更好,且支持嵌套。

7.3 管道对接while(| while read)

  • [RULE 7-3] 不使用管道对接 while read,使用 进程替换
  1. # 解释:每个管道符‘|’都会开启一个子shell,子shell中的变量,以及对变量的操作在父shell中不可见
  2. # Good
  3. total=0
  4. last_file=''
  5. while read count filename ignored; do
  6. (( total += $count ))
  7. last_file="${filename}"
  8. done < <(uniq -c file.txt) # 使用进程替换,这样不会生成隐形的子shell
  9. echo "Total = ${total}"
  10. echo "Last one = ${last_file}"
  11. # Bad
  12. last_line='NULL'
  13. your_command | while read line; do
  14. last_line="${line}"
  15. done
  16. echo "${last_line}" # 依然输出 'NULL',而不是 `your_command` 赋予的值

7.4 test , [ 和 [[

  • [RULE 7-4] 使用 [[ ]] 代替 [test。但在测试代码中可以使用test
  • [RULE 7-5] 明确的使用 -n-z,表明意图;不能使用[[ xx"${my_var}" = xx"some_string" ]]这种形式
  1. # Good
  2. # 使用 -z、-n 判断变量是否为空(或存在)
  3. if [[ -z "${my_var}" ]]; then
  4. do_something
  5. fi
  6. # 判断变量是否相等,使用这种形式
  7. [[ "${my_var}" = "some_string" ]]
  8. # 不使用这种形式
  9. [[ xx"${my_var}" = xx"some_string" ]]
  10. # Bad
  11. if [[ "${var}" ]]; then
  12. do_something
  13. fi
  14. if [[ "${my_var}" = "" ]]; then
  15. do_something
  16. fi
  17. # 在测试代码中,可以使用test命令,更加直观
  18. it_displays_usage_with_hyphen_and_h() {
  19. usage=$(bash runit -h | head -n1)
  20. test "${usage}" = "${usage_result}"
  21. }

7.5 通配符(Wildcard Expansion of Filenames)

  • [RULE 7-6] 使用文件通配符时必须指明路径,避免由于含有 -r-f 等这类文件名,造成风险
  1. # Good
  2. # 明确路径,避免误删
  3. [work@dev ~]$ touch -- '-r' '-f' 'foo2' 'bar2'
  4. [work@dev ~]$ mkdir rich
  5. [work@dev ~]$ rm ./*
  6. rm: cannot remove './rich': Is a directory # 文件夹没有被删
  7. [work@dev ~]$ ls -l
  8. total 4
  9. drwxrwxr-x 2 work work 4096 Mar 27 20:16 rich
  10. # Bad
  11. # 危险行为,'*' 中可能包含了 '-rf'
  12. [work@dev ~]$ touch foo bar
  13. [work@dev ~]$ touch -- '-r' '-f'
  14. [work@dev ~]$ mkdir loser
  15. [work@dev ~]$ ls -l
  16. total 0
  17. -rw-rw-r-- 1 work work 0 Mar 27 20:08 bar
  18. -rw-rw-r-- 1 work work 0 Mar 27 20:08 -f
  19. -rw-rw-r-- 1 work work 0 Mar 27 20:08 foo
  20. -rw-rw-r-- 1 work work 0 Mar 27 20:08 -r
  21. drwxrwxr-x 2 work work 4096 Mar 27 20:12 loser/
  22. # 此处 '*' 中包含了 '-r' '-f',所以 'loser' 文件夹被误删了
  23. [work@dev ~]$ rm *
  24. [work@dev ~]$ ls -l
  25. total 0
  26. -rw-rw-r-- 1 work work 0 Mar 27 20:08 -f
  27. -rw-rw-r-- 1 work work 0 Mar 27 20:08 -r
  28. [work@dev ~]$ rm -- -r -f
  29. [work@dev ~]$ ls -l
  30. total 0
  • [RULE 7-7] 如果作为参数传递,需要引用转义。当通配符本身与其他命令的参数重复时,注意通配符优先的问题。
  1. # Good
  2. [work@dev ~]$ echo 2 \* 3 | bc
  3. 6
  4. [work@dev ~]$ echo '2 * 3' | bc
  5. 6
  6. # Bad
  7. [work@dev ~]$ touch + # touch一个文件名为“+”的文件
  8. [work@dev ~]$ echo 2 * 3
  9. 2 + 3
  10. [work@dev ~]$ echo 2 * 3 | bc # “*”先被解析为了“+”
  11. 5

7.6 算数计算(Arithmetic)

  • [RULE 7-8] 使用$(()), 不使用let
  1. # Good
  2. # i自增1
  3. (( i += 1 ))
  4. # 复杂的算数计算
  5. min=5
  6. sec=30
  7. echo $(( (min * 60) + sec ))
  8. # Bad
  9. let "i += 1"
  10. min=5
  11. sec=30
  12. ret=0
  13. let "ret = ( min * 60 ) + sec"
  14. echo "${ret}"

7.7 函数返回值(Function Return)

  • [RULE 7-9] 函数返回值0代表成功,非零代表其他含义
  • [RULE 7-10] return仅做返回值用,禁止用函数返回值返回计算结果
  1. 解释:return只能返回0-255的数字,如果有其他需要返回的内容,使用对命令输出捕获的方式

8. 调用命令(Calling Commands)

8.1 检查返回值(Checking Return Values)

  • [ADVISE 8-1] 检查返回值。建议开启set -e选项。
  1. # cd命令的返回值,被'&&'判断,只有cd成功了,才rm
  2. cd /a/dir && rm file
  3. # 或者使用 `if/else`
  4. if ! mv "${file_list}" "${dest_dir}/"; then
  5. echo "Unable to move ${file_list} to ${dest_dir}" >&2
  6. exit 1
  7. fi
  • [ADVISE 8-2] 使用 ${PIPESTATUS[@]} 数组检查管道中命令的返回值。建议开启set -o pipefail选项。
  1. tar -cf - ./* | ( cd "${dir}" && tar -xf - )
  2. if [[ "${PIPESTATUS[0]}" -ne 0 || "${PIPESTATUS[1]}" -ne 0 ]]; then
  3. echo "Unable to tar files to ${dir}" >&2
  4. fi
  5. # 注意, ${PIPESTATUS[@]} 数组在下一个命令执行完毕后,将清空
  6. tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
  7. return_codes=(${PIPESTATUS[*]})
  8. if [[ "${return_codes[0]}" -ne 0 ]]; then
  9. do_something
  10. fi
  11. if [[ "${return_codes[1]}" -ne 0 ]]; then
  12. do_something_else
  13. fi

8.2 内建与外部命令(Builtin Commands vs. External Commands)

  • [RULE 8-1] 优先使用内建命令而不是外部命令
  1. # 解释: 内建命令更健壮. 外部命令存在于磁盘上, 而磁盘是一个故障率很高的设备, 如果挂掉, 则命令不可用.
  2. # 算术表达式
  3. # Good:
  4. addition=$((${X} + ${Y}))
  5. # Bad:
  6. addition="$(expr ${X} + ${Y})"
  7. # 字符串操作
  8. # Good:
  9. substitution="${string/#foo/bar}"
  10. # Bad:
  11. substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
  12. # 读取文件
  13. # Good:
  14. var=$(</proc/loadavg)
  15. # Bad:
  16. var=$(cat /proc/loadavg)

9. 环境(Environment)

9.1 STDOUT 与 STDERR

  • [RULE 9-1] 错误信息输出至STDERR,以更容易区分正常消息
  • [ADVISE 9-1] 推荐用函数来输出错误
  1. # print error message to stdout
  2. function err() {
  3. echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2
  4. }
  5. if ! do_something; then
  6. err "Unable to do_something"
  7. exit "${E_DID_NOTHING}"
  8. fi

10. 其他最佳实践(Best Practice)

10.1 set

  • [ADVISE 10-1] 建议开启 set -eset -uset -o pipefail 开关,帮助定位问题。
  1. #!/bin/bash
  2. set -u # 使用的变量必须提前定义过
  3. set -e # 所有非0的返回状态都需要捕获
  4. set -o pipefail # 管道间错误需要捕获
  5. var="NotNull" # 如果此行注释掉,那么脚本将不会继续执行
  6. echo "${var}"
  7. # 错误需要捕获,否则不会继续执行
  8. false || {
  9. echo "Something false."
  10. }
  11. # 管道间遇到的第一个错误,被捕获
  12. true | false | true || {
  13. echo "Something false in the pipe."
  14. }
  15. echo "End"

10.2 临时文件

  • [ADVISE 10-2] 尽量不使用临时文件,避免异常退出时垃圾文件的残留(日积月累后,无人知道哪些文件可删除),建议使用进程替换技术替换文件,将内容放入内存中。临时文件通常指,处理过程中(如:文本处理的中间结果)生成的文件,pid文件是程序标准行为的一部分,不在此列。
  1. # 直接diff两个命令的结果
  2. [work@dev ~]$ diff <(echo x) <(echo y)
  3. 1c1
  4. < x
  5. ---
  6. > y
  1. #!/bin/bash
  2. # 直接使用pstree,在每一行前加了行号
  3. while read i; do
  4. echo -e "$((++j))" "\t $i"
  5. done < <( pstree )
  6. # 使用 here-document,直接对多行文本进行逐行处理
  7. while read line; do
  8. echo "this is $line"
  9. done < <(cat <<EOF
  10. xxx
  11. yyy
  12. zzz
  13. EOF)
  14. # 使用 here-string
  15. while read line; do
  16. echo "this is $line"
  17. done <<< "xxx
  18. yyy
  19. zzz"

10.3 幂等性

  • [ADVISE 10-3] 尽量确保脚本执行的幂等性,保证逻辑不会重复被执行两次以上。
  1. #!/bin/bash
  2. # 注1:此脚本框架例子,利用了临时文件保证幂等,依赖临时文件的持久化
  3. # 注2:当脚本异常终止时(kill -9或机器死机),脚本中的rm命令可能不会被执行,从而影响下一次运行
  4. set -eu
  5. set -o pipefail
  6. pid_file=/home/work/dev/app/app.pid
  7. if (set -C; echo $$ > ${pid_file}) 2>/dev/null; then
  8. # set -C 使已存在的文件不能再被写
  9. # echo 生成锁文件,将pid放入其中
  10. # 当此pid文件存在时,if返回失败,跳到else
  11. # trap保证了脚本异常中断时,释放锁文件(删)
  12. trap 'rm ${pid_file}; exit $?' INT TERM EXIT
  13. {
  14. echo "my critical code..." # 此处是正式的脚本代码
  15. echo "my critical code..."
  16. echo "my critical code..."
  17. }
  18. rm ${pid_file} # 正式代码运行完了,释放锁文件
  19. trap - INT TERM EXIT # 恢复trap的设置(如在脚本最后时,非必要恢复)
  20. exit 0
  21. else
  22. # pid文件生效,会跳到此处, 打印错误信息
  23. echo "${pid_file} exist, pid $(<${pid_file}) is running."
  24. exit 1
  25. fi
posted @ 2020-04-07 11:18  lidowson  阅读(7)  评论(0)    收藏  举报