背景
使用哪一种Shell
Bash是唯一被允许执行的shell脚本语言。
可执行文件必须以 #!/bin/bash 和最小数量的标志开始。请使用 set 来设置shell的选项,使得用 bash <script_name> 调用你的脚本时不会破坏其功能。
限制所有的可执行shell脚本为bash使得我们安装在所有计算机中的shell语言保持一致性。
无论你是为什么而编码,对此唯一例外的是当你被迫时可以不这么做的。其中一个例子是Solaris SVR4包,编写任何脚本都需要用纯Bourne shell。
什么时候使用Shell
Shell应该仅仅被用于小功能或者简单的包装脚本。
尽管Shell脚本不是一种开发语言,但在整个谷歌它被用于编写多种实用工具的脚本。这个风格指南更多的是认同它的使用,而不是一个建议,即它可被用于广泛部署。
以下是一些准则:
- 如果你主要是在调用其他的工具并且做一些相对很小数据量的操作,那么使用shell来完成任务是一种可接受的选择。
- 如果你在乎性能,那么请选择其他工具,而不是使用shell。
- 如果你发现你需要使用数据而不是变量赋值(如 ${PHPESTATUS} ),那么你应该使用Python脚本。
- 如果你将要编写的脚本会超过100行,那么你可能应该使用Python来编写,而不是Shell。请记住,当脚本行数增加,尽早使用另外一种语言重写你的脚本,以避免之后花更多的时间来重写。
Shell文件和解释器调用
文件扩展名
可执行文件应该没有扩展名(强烈建议)或者使用.sh扩展名。库文件必须使用.sh作为扩展名,而且应该是不可执行的。
当执行一个程序时,并不需要知道它是用什么语言编写的。而且shell脚本也不要求有扩展名。所以我们更喜欢可执行文件没有扩展名。
然而,对于库文件,知道其用什么语言编写的是很重要的,有时候会需要使用不同语言编写的相似的库文件。使用.sh这样特定语言后缀作为扩展名,就使得用不同语言编写的具有相同功能的库文件可以采用一样的名称。
SUID / SGID
SUID(Set User ID)和SGID(Set Group ID)在shell脚本中是被禁止的。
shell存在太多的安全问题,以致于如果允许SUID/SGID会使得shell几乎不可能足够安全。虽然bash使得运行SUID非常困难,但在某些平台上仍然有可能运行,这就是为什么我们明确提出要禁止它。
如果你需要较高权限的访问请使用 sudo 。
环境
STDOUT vs STDERR
所有的错误信息都应该被导向STDERR。
这使得从实际问题中分离出正常状态变得更容易。
推荐使用类似如下函数,将错误信息和其他状态信息一起打印出来。
err() { echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2 } if ! do_something; then err "Unable to do_something" exit "${E_DID_NOTHING}" fi
注释
文件头
每个文件的开头是其文件内容的描述。
每个文件必须包含一个顶层注释,对其内容进行简要概述。版权声明和作者信息是可选的。
例如:
#!/bin/bash # # Perform hot backups of Oracle databases.
功能注释
任何不是既明显又短的函数都必须被注释。任何库函数无论其长短和复杂性都必须被注释。
其他人通过阅读注释(和帮助信息,如果有的话)就能够学会如何使用你的程序或库函数,而不需要阅读代码。
所有的函数注释应该包含:
- 函数的描述
- 全局变量的使用和修改
- 使用的参数说明
- 返回值,而不是上一条命令运行后默认的退出状态
例如:
#!/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() { ... }
实现部分的注释
注释你代码中含有技巧、不明显、有趣的或者重要的部分。
这部分遵循谷歌代码注释的通用做法。不要注释所有代码。如果有一个复杂的算法或者你正在做一些与众不同的,放一个简单的注释。
TODO注释
使用TODO注释临时的、短期解决方案的、或者足够好但不够完美的代码。
这与C++指南中的约定相一致。
TODOs应该包含全部大写的字符串TODO,接着是括号中你的用户名。冒号是可选的。最好在TODO条目之后加上 bug或者ticket 的序号。
例如:
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
格式
缩进
缩进两个空格,没有制表符。
在代码块之间请使用空行以提升可读性。缩进为两个空格。无论你做什么,请不要使用制表符。对于已有文件,保持已有的缩进格式。
行的长度和长字符串
行的最大长度为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个空格。这适用于使用管道符’|’的合并命令链以及使用’||’和’&&’的逻辑运算链。
# All fits on one line command1 | command2 # Long commands command1 \ | command2 \ | command3 \ | command4
循环
请将 ; 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
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
变量扩展
按优先级顺序:保持跟你所发现的一致;引用你的变量;推荐用 ${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"
引用
- 除非需要小心不带引用的扩展,否则总是引用包含变量、命令替换符、空格或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 $