Shell编码风格
1.注释
1.1文件头
- 每个文件的开头是其文件内容的描述。
每个文件必须包含一个顶层注释,对其内容进行简要概述。版权声明和作者信息是可选的。
例如:
#!/bin/bash # # Perform hot backups of Oracle databases.
1.2.功能注释
- 任何不是既明显又短的函数都必须被注释。任何库函数无论其长短和复杂性都必须被注释。
其他人通过阅读注释(和帮助信息,如果有的话)就能够学会如何使用你的程序或库函数,而不需要阅读代码。
所有的函数注释应该包含:
- 函数的描述
- 全局变量的使用和修改
- 使用的参数说明
- 返回值,而不是上一条命令运行后默认的退出状态
例如:
#!/bin/bash # # Perform hot backups of Oracle databases. export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin' ####################################### # Cleanup files from the backup dir # Globals: # BACKUP_DIR # ORACLE_SID # Arguments: # None # Returns: # None ####################################### cleanup() { ... }
1.3.实现部分的注释
- 注释你代码中含有技巧、不明显、有趣的或者重要的部分。
这部分遵循谷歌代码注释的通用做法。不要注释所有代码。如果有一个复杂的算法或者你正在做一些与众不同的,放一个简单的注释。
1.4.TODO注释
- 使用TODO注释临时的、短期解决方案的、或者足够好但不够完美的代码。
这与C++指南中的约定相一致。
TODOs应该包含全部大写的字符串TODO,接着是括号中你的用户名。冒号是可选的。最好在TODO条目之后加上 bug或者ticket 的序号。
例如:
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
2.格式
2.1缩进
- 缩进两个空格,没有制表符。
在代码块之间请使用空行以提升可读性。缩进为两个空格。无论你做什么,请不要使用制表符。对于已有文件,保持已有的缩进格式。
2.2.行的长度和长字符串
- 行的最大长度为80个字符。
如果你必须写长度超过80个字符的字符串,如果可能的话,尽量使用here document或者嵌入的换行符。长度超过80个字符的文字串且不能被合理地分割,这是正常的。但强烈建议找到一个方法使其变短。
# DO use 'here document's cat <<END; I am an exceptionally long string. END # Embedded newlines are ok too long_string="I am an exceptionally long string."
2.3.管道
- 如果一行容不下整个管道操作,那么请将整个管道操作分割成每行一个管段。
如果一行容得下整个管道操作,那么请将整个管道操作写在同一行。
否则,应该将整个管道操作分割成每行一个管段,管道操作的下一部分应该将管道符放在新行并且缩进2个空格。这适用于使用管道符’|’的合并命令链以及使用’||’和’&&’的逻辑运算链。
# All fits on one line command1 | command2 # Long commands command1 \ | command2 \ | command3 \ | command4
2.4.循环
- 请将
; do
,; then
和while
,for
,if
放在同一行。
shell中的循环略有不同,但是我们遵循跟声明函数时的大括号相同的原则。也就是说, ; do
, ; then
应该和 if/for/while 放在同一行。 else
应该单独一行,结束语句应该单独一行并且跟开始语句垂直对齐。
例如:
for dir in ${dirs_to_cleanup}; do if [[ -d "${dir}/${ORACLE_SID}" ]]; then log_date "Cleaning up old files in ${dir}/${ORACLE_SID}" rm "${dir}/${ORACLE_SID}/"* if [[ "$?" -ne 0 ]]; then error_message fi else mkdir -p "${dir}/${ORACLE_SID}" if [[ "$?" -ne 0 ]]; then error_message fi fi done
2.5.case语句
- 通过2个空格缩进可选项。
- 在同一行可选项的模式右圆括号之后和结束符
;;
之前各需要一个空格。 - 长可选项或者多命令可选项应该被拆分成多行,模式、操作和结束符
;;
在不同的行。
匹配表达式比 case
和 esac
缩进一级。多行操作要再缩进一级。一般情况下,不需要引用匹配表达式。模式表达式前面不应该出现左括号。避免使用 ;&
和 ;;&
符号。
case "${expression}" in a) variable="..." some_command "${variable}" "${other_expr}" ... ;; absolute) actions="relative" another_command "${actions}" "${other_expr}" ... ;; *) error "Unexpected expression '${expression}'" ;; esac
只要整个表达式可读,简单的命令可以跟模式和 ;;
写在同一行。这通常适用于单字母选项的处理。当单行容不下操作时,请将模式单独放一行,然后是操作,最后结束符 ;;
也单独一行。当操作在同一行时,模式的右括号之后和结束符 ;;
之前请使用一个空格分隔。
verbose='false' aflag='' bflag='' files='' while getopts 'abf:v' flag; do case "${flag}" in a) aflag='true' ;; b) bflag='true' ;; f) files="${OPTARG}" ;; v) verbose='true' ;; *) error "Unexpected option ${flag}" ;; esac done
2.6.变量扩展
- 按优先级顺序:保持跟你所发现的一致;引用你的变量;推荐用
${var}
而不是$var
,详细解释如下。
这些仅仅是指南,因为作为强制规定似乎饱受争议。
以下按照优先顺序列出。
- 与现存代码中你所发现的保持一致。
- 引用变量参阅下面一节,引用。
- 除非绝对必要或者为了避免深深的困惑,否则不要用大括号将单个字符的shell特殊变量或定位变量括起来。推荐将其他所有变量用大括号括起来。
# Section of recommended cases. # Preferred style for 'special' variables: echo "Positional: $1" "$5" "$3" echo "Specials: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..." # Braces necessary: echo "many parameters: ${10}" # Braces avoiding confusion: # Output is "a0b0c0" set -- a b c echo "${1}0${2}0${3}0" # Preferred style for other variables: echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}" while read f; do echo "file=${f}" done < <(ls -l /tmp) # Section of discouraged cases # Unquoted vars, unbraced vars, brace-quoted single letter # shell specials. echo a=$avar "b=$bvar" "PID=${$}" "${1}" # Confusing use: this is expanded as "${1}0${2}0${3}0", # not "${10}${20}${30} set -- a b c echo "$10$20$30"
2.7.引用
- 除非需要小心不带引用的扩展,否则总是引用包含变量、命令替换符、空格或shell元字符的字符串。
- 推荐引用是单词的字符串(而不是命令选项或者路径名)。
- 千万不要引用整数。
- 注意
[[
中模式匹配的引用规则。 - 请使用
$@
除非你有特殊原因需要使用$*
。
# 'Single' quotes indicate that no substitution is desired. # "Double" quotes indicate that substitution is required/tolerated. # Simple examples # "quote command substitutions" flag="$(some_command and its args "$@" 'quoted separately')" # "quote variables" echo "${flag}" # "never quote literal integers" value=32 # "quote command substitutions", even when you expect integers number="$(generate_number)" # "prefer quoting words", not compulsory readonly USE_INTEGER='true' # "quote shell meta characters" echo 'Hello stranger, and well met. Earn lots of $$$' echo "Process $$: Done making \$\$\$." # "command options or path names" # ($1 is assumed to contain a value here) grep -li Hugo /dev/null "$1" # Less simple examples # "quote variables, unless proven false": ccs might be empty git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"} # Positional parameter precautions: $1 might be unset # Single quotes leave regex as-is. grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"} # For passing on arguments, # "$@" is right almost everytime, and # $* is wrong almost everytime: # # * $* and $@ will split on spaces, clobbering up arguments # that contain spaces and dropping empty strings; # * "$@" will retain arguments as-is, so no args # provided will result in no args being passed on; # This is in most cases what you want to use for passing # on arguments. # * "$*" expands to one argument, with all args joined # by (usually) spaces, # so no args provided will result in one empty string # being passed on. # (Consult 'man bash' for the nit-grits ;-) set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$*"; echo "$#, $@") set -- 1 "2 two" "3 three tres"; echo $# ; set -- "$@"; echo "$#, $@")
3.特性及错误
3.1.命令替换
- 使用
$(command)
而不是反引号。
嵌套的反引号要求用反斜杠转义内部的反引号。而 $(command)
形式嵌套时不需要改变,而且更易于阅读。
例如:
# This is preferred: var="$(command "$(command1)")" # This is not: var="`command \`command1\``"
3.2.test,[和[[
- 推荐使用
[[ ... ]]
,而不是[
,test
, 和/usr/bin/ [
。
因为在 [[
和 ]]
之间不会有路径名称扩展或单词分割发生,所以使用 [[ ... ]]
能够减少错误。而且 [[ ... ]]
允许正则表达式匹配,而 [ ... ]
不允许。
# This ensures the string on the left is made up of characters in the # alnum character class followed by the string name. # Note that the RHS should not be quoted here. # For the gory details, see # E14 at http://tiswww.case.edu/php/chet/bash/FAQ if [[ "filename" =~ ^[[:alnum:]]+name ]]; then echo "Match" fi # This matches the exact pattern "f*" (Does not match in this case) if [[ "filename" == "f*" ]]; then echo "Match" fi # This gives a "too many arguments" error as f* is expanded to the # contents of the current directory if [ "filename" == f* ]; then echo "Match" fi
3.3.测试字符串
尽可能使用引用,而不是过滤字符串。
Bash足以在测试中处理空字符串。所以,请使用空(非空)字符串测试,而不是过滤字符,使得代码更易于阅读。
# Do this: if [[ "${my_var}" = "some_string" ]]; then do_something fi # -z (string length is zero) and -n (string length is not zero) are # preferred over testing for an empty string if [[ -z "${my_var}" ]]; then do_something fi # This is OK (ensure quotes on the empty side), but not preferred: if [[ "${my_var}" = "" ]]; then do_something fi # Not this: if [[ "${my_var}X" = "some_stringX" ]]; then do_something fi
为了避免对你测试的目的产生困惑,请明确使用`-z`或者`-n`
# Use this if [[ -n "${my_var}" ]]; then do_something fi # Instead of this as errors can occur if ${my_var} expands to a test # flag if [[ "${my_var}" ]]; then do_something fi
3.4.文件名的通配符扩展
- 当进行文件名的通配符扩展时,请使用明确的路径。
因为文件名可能以 -
开头,所以使用扩展通配符 ./*
比 *
来得安全得多。
# Here's the contents of the directory: # -f -r somedir somefile # This deletes almost everything in the directory by force psa@bilby$ rm -v * removed directory: `somedir' removed `somefile' # As opposed to: psa@bilby$ rm -v ./* removed `./-f' removed `./-r' rm: cannot remove `./somedir': Is a directory removed `./somefile'
3.5.Eval
- 应该避免使用eval。
当用于给变量赋值时,Eval解析输入,并且能够设置变量,但无法检查这些变量是什么。
# What does this set? # Did it succeed? In part or whole? eval $(set_my_variables) # What happens if one of the returned values has a space in it? variable="$(eval some_function)"
3.6.管道导向while循环
- 请使用过程替换或者for循环,而不是管道导向while循环。在while循环中被修改的变量是不能传递给父shell的,因为循环命令是在一个子shell中运行的。
管道导向while循环中的隐式子shell使得追踪bug变得很困难。
last_line='NULL' your_command | while read line; do last_line="${line}" done # This will output 'NULL' echo "${last_line}"
如果你确定输入中不包含空格或者特殊符号(通常意味着不是用户输入的),那么可以使用一个for循环。
total=0 # Only do this if there are no spaces in return values. for value in $(command); do total+="${value}" done
使用过程替换允许重定向输出,但是请将命令放入一个显式的子shell中,而不是bash为while循环创建的隐式子shell。
total=0 last_file= while read count filename; do total+="${count}" last_file="${filename}" done < <(your_command | uniq -c) # This will output the second field of the last line of output from # the command. echo "Total = ${total}" echo "Last one = ${last_file}"
当不需要传递复杂的结果给父shell时可以使用while循环。这通常需要一些更复杂的“解析”。请注意简单的例子使用如awk这类工具可能更容易完成。当你特别不希望改变父shell的范围变量时这可能也是有用的。
# Trivial implementation of awk expression: # awk '$3 == "nfs" { print $2 " maps to " $1 }' /proc/mounts cat /proc/mounts | while read src dest type opts rest; do if [[ ${type} == "nfs" ]]; then echo "NFS ${dest} maps to ${src}" fi done