【译文】Shell 编码指南
修订版 2.02 由许多 Google 员工撰写、修订和维护。
背景
使用哪种 Shell
Bash 是可执行文件唯一允许的 Shell 脚本语言。
可执行文件必须以 #!/bin/bash
和最少的标志开头。使用 set
来设置 Shell 程序选项,以便以 bash_script_name
调用脚本而不会破坏其功能。
将所有可执行的 Shell 脚本限制为 bash,可以使我们在所有计算机上都安装一致的 Shell 语言环境。
唯一的例外是,无论您使用哪种编码方式,都会被迫进入。其中一个示例是 Solaris SVR4 软件包,该软件包需要使用纯 Bourne Shell 编写任何脚本。
何时使用 Shell
Shell 仅应用于小型使用程序或简单的包装器脚本。
虽然 Shell 脚本并不是一种开发语言,但可用于在整个 Google 上编写各种实用程序脚本。该规范更多地是对其使用的认可,而不是建议将其广泛使用。
一些指南:
- 如果您主要是在调用其他使用程序,并且进行的数据操作相对较少,那么 Shell 是执行次任务的可接受选择。
- 如果性能很重要,请使用非 Shell 的东西。
- 如果编写的脚本长度超过 100 行,或使用非直接控制流逻辑,则应立即以结构化的语言重写它。请记住,脚本会不断增长。尽早重写脚本,以避免以后花费更多时间进行重写。
- 在评估代码的复杂性时(例如,决定是否切换语言),请考虑改代码是否易于由作者之外的其他人维护。
Shell 文件和解释器调用
文件扩展名
可执行文件不应具有扩展名(强烈推荐)或 .sh
扩展名。库必须具有 .sh
扩展名,并且不能执行。
不必在执行程序时知道用哪种语言编写程序,而且 Shell 不需要扩展名,因此我们不希望对可执行文件使用扩展名。
但是,对于库来说,重要的是要知道它是什么语言,有时还需要在相似的库中使用不同的语言。这允许用途相同但语言不同的库文件以相同的名称命名(除了特定于语言的后缀)。
SUID/SGID
Shell 脚本上禁止 SUID 和 SGID。
Shell 存在太多的安全问题,几乎不可能完全安全地允许 SUID/SGID。
虽然 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 1
fi
注释
文件头
在每个文件的开头加上其内容的描述。
每个文件都必须具有顶部注释,包括其内容的简要概述。版权声明和作者信息是可选的。
例如:
#!/bin/bash
#
# Perform hot backups of Oracle databases.
函数注释
任何既不明显也不简短的功能必须加以注释。无论长度或复杂性如何,都必须对库中的所有函数进行注释。
他人无需阅读代码即可通过阅读注释(和自助服务,如果提供)来学习如何使用您的程序或在库中使用函数。
所有函数注释都应使用一下方式描述预期的 API 行为:
- 功能说明。
- 全局变量:使用和修改的全局变量列表。
- 参数:采用的参数。
- 输出:输出到 STDOUT 或 STDERR。
- 返回值:返回的值不是上一次运行命令的默认退出状态。
示例:
#######################################
# Cleanup files from the backup directory.
# Globals:
# BACKUP_DIR
# ORACLE_SID
# Arguments:
# None
#######################################
function cleanup() {
…
}
#######################################
# Get configuration directory.
# Globals:
# SOMEDIR
# Arguments:
# None
# Outputs:
# Writes location to stdout
#######################################
function get_dir() {
echo "${SOMEDIR}"
}
#######################################
# Delete a file in a sophisticated manner.
# Arguments:
# File to delete, a path.
# Returns:
# 0 if thing was deleted, non-zero on error.
#######################################
function del_thing() {
rm "$1"
}
实施注释
注释代码中棘手、不明显、有趣或重要的部分。
这遵循一般的 Google 编码注释惯例。不要注释所有内容。如果算法复杂,或者您要执行的操作与众不同,请在此处简短说明。
TODO 注释
对于临时的、短期的解决方案或足够好但不完美的代码,请使用 TODO 注释。
这符合 C++ 指南中的约定。
TODO
应在所有大写字母中包含字符串 TODO
,然后是与该 TODO
所引用问题最相关的人员的姓名、电子邮件地址或其他标识符。主要目的是要有一个一致的 TODO
,可以对其进行搜索以找到如何根据请求获取更多详细信息。TODO
并不表示相关人员将解决问题。因此,在创建 TODO 时,几乎总是给出您的名字。
示例:
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
格式化
虽然您应该遵循要修改的文件已存在的样式,但是任何新代码都需要遵循以下规范。
缩进
缩进 2 个空格。不使用 tab。
在块之间使用空行以提高可读性。缩进是两个空格。无论您做什么,都不要使用标签。对于现有文件,请遵循现有缩进。
长行和长字符串
行的最大长度为 80 个字符。
如果必须编写长度唱过 80 个字符的字符串,则应使用 here 文档或嵌入的换行符来完成。长度必须超过 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 中的循环有些不同,但是在声明函数时,我们遵循与花括号相同的原则。也就是说,; then
和 ; do
应该和 if/for/while
在同一行。else
则应独占一行,并且闭合语句应与打开语句垂直对齐。
例如:
# If inside a function, consider declaring the loop variable as
# a local to avoid it leaking into the global environment:
# local dir
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 (( $? != 0 )); then
error_message
fi
else
mkdir -p "${dir}/${ORACLE_SID}"
if (( $? != 0 )); then
error_message
fi
fi
done
Case 语句
- 选项缩进两个空格。
- 单行选项在匹配项的圆括号后和
;;
之前需要一个空格。 - 长命令或命令选项应将匹配项、操作和
;;
分成多行,分别占据一行。
匹配的表达式比 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 -r f; do
echo "file=${f}"
done < <(find /tmp)
# Section of *discouraged* cases
# Unquoted vars, unbraced vars, brace-delimited 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"
注意:在 {var}
中使用大括号不是引用形式。还必须使用双引号。
引用
- 始终引用包含变量、命令替换、空格或 shell 元字符的字符串,除非需要仔细的无引号扩展或它是 shell 程序内部的证书(请参阅下一点)。
- 使用数组来安全引用元素列表,尤其是命令行标志。请参阅下面的数组。
- (可选)引用定义为整数的 shell 内部制度特殊变量:
$?
、$#
、$$
、$!
(man bash)。最好引用“命名”内部整数变量,例如 PPID 以保持一致性。 - 最好用引号引起来的字符串是“单词”(与命令选项或路径名相反)。
- 请勿引用文字整数。
- 请注意
[[...]]
中模式匹配的引用规则。请参见下面的测试,[...]
和[[...]]
部分。 - 除非您有特定的原因要使用
$*
,否则请使用"$@"
,例如仅将参数附加到消息或日志中的字符串上。
# 'Single' quotes indicate that no substitution is desired.
# "Double" quotes indicate that substitution is required/tolerated.
# Simple examples
# "quote command substitutions"
# Note that quotes nested inside "$()" don't need escaping.
flag="$(some_command and its args "$@" 'quoted separately')"
# "quote variables"
echo "${flag}"
# Use arrays with quoted expansion for lists.
declare -a FLAGS
FLAGS=( --foo --bar='baz' )
readonly FLAGS
mybinary "${FLAGS[@]}"
# It's ok to not quote internal integer variables.
if (( $# > 3 )); then
echo "ppid=${PPID}"
fi
# "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 every time, and
# $* is wrong almost every time:
#
# * $* 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 "$#, $@")
特性和 Bug
ShellCheck
ShellCheck 项目为您的 Shell 脚本识别常见的错误和警告。建议所有脚本都使用,无论大小。
命令替换
使用 $(command)
代替反引号。
嵌套的反引号要求使用 \
来转义内部的反引号。$(command)
格式在嵌套时无需更改,且便于阅读。
例如:
# This is preferred:
var="$(command "$(command1)")"
# This is not:
var="`command \`command1\``"
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.
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
有关详细信息,请参阅 http://tiswww.case.edu/php/chet/bash/FAQ 上的E14。
检测字符串
尽可能使用引号而不是填充字符。
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
if [[ "${my_var}" ]]; then
do_something
fi
为了清除起见,请使用 ==
表示相等性,而不要使用 =
,即使两者都可行。前者鼓励使用 [[
,而后者会与赋值混淆。但是,在 [[ ... ]]
中使用 <
和 >
进行字典比较时要小心。使用 (( ... ))
或 -lt
和 -gt
进行数值比较。
# Use this
if [[ "${my_var}" == "val" ]]; then
do_something
fi
if (( my_var > 3 )); then
do_something
fi
if [[ "${my_var}" -gt 3 ]]; then
do_something
fi
# Instead of this
if [[ "${my_var}" = "val" ]]; then
do_something
fi
# Probably unintended lexicographical comparison.
if [[ "${my_var}" > 3 ]]; then
# True for 4, false for 22.
do_something
fi