由许多 Google 员工编写、修订和维护。
章节 | 内容 |
---|---|
背景 | 使用哪个 Shell - 何时使用 Shell |
Shell 文件和解释器调用 | 文件扩展名 - SUID/SGID |
环境 | STDOUT vs STDERR |
注释 | 文件头 - 函数注释 - 实现注释 - TODO 注释 |
格式化 | 缩进 - 行长度和长字符串 - 管道 - 控制流 - Case 语句 - 变量扩展 - 引用 |
特性和 Bug | ShellCheck - 命令替换 - Test、[… ] 和 [[… ]] - 测试字符串 - 文件名的通配符扩展 - Eval - 数组 - 管道到 While - 算术运算 - 别名 |
命名约定 | 函数名称 - 变量名称 - 常量和环境变量名称 - 源文件名 - 使用局部变量 - 函数位置 - main |
调用命令 | 检查返回值 - 内置命令 vs. 外部命令 |
如有疑问:保持一致 |
Bash 是可执行文件唯一允许的 Shell 脚本语言。
可执行文件必须以 #!/bin/bash
和最少的标志开始。 使用 set
设置 shell 选项,以便将脚本作为 bash script_name
调用不会破坏其功能。
将所有可执行 shell 脚本限制为 *bash* 为我们提供了一种一致的 shell 语言,该语言安装在我们所有的机器上。 特别是,这意味着通常不需要努力实现 POSIX 兼容性或以其他方式避免“bashism”。
上述唯一的例外是您被您所编码的内容所迫。 例如,某些遗留操作系统或受限的执行环境可能需要纯 Bourne shell 用于某些脚本。
Shell 应该只用于小型实用程序或简单的包装脚本。
虽然 shell 脚本不是一种开发语言,但它用于在 Google 中编写各种实用程序脚本。 本风格指南更多地是对其使用的认可,而不是建议将其用于广泛的部署。
一些指导原则
可执行文件应具有 .sh
扩展名或无扩展名。
.sh
扩展名。 这使您可以使用推荐的命名约定,使用类似 foo.sh
的源文件和名为 foo
的构建规则。PATH
,则最好不要使用扩展名。 没有必要知道程序是用什么语言编写的,当执行它时,并且 shell 不需要扩展名,因此我们更喜欢不使用扩展名,对于那些将直接被用户调用的可执行文件。 同时,请考虑部署构建规则的输出是否优于直接部署源文件。库必须具有 .sh
扩展名,并且不应是可执行文件。
在 shell 脚本上禁止 SUID 和 SGID。
shell 有太多的安全问题,使得它几乎不可能充分保护以允许 SUID/SGID。 虽然 bash 确实使运行 SUID 变得困难,但在某些平台上仍然可以,这就是我们明确禁止它的原因。
如果您需要提升的访问权限,请使用 sudo
。
所有错误消息都应发送到 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 行为
示例
#######################################
# 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 注释。
这与 C++ 指南中的约定相匹配。
TODO
s 应包含全大写的字符串 TODO
,后跟对 TODO
引用的问题具有最佳上下文的人员的姓名、电子邮件地址或其他标识符。 主要目的是拥有一个一致的 TODO
,可以搜索以查找如何根据要求获取更多详细信息。 TODO
并不是承诺所引用的人员会解决问题。 因此,当您创建 TODO
时,几乎总是给出您的姓名。
例子
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
虽然您应该遵循您正在修改的文件的现有样式,但以下是任何新代码所必需的。
缩进 2 个空格。 没有制表符。
在块之间使用空行以提高可读性。 缩进是两个空格。 无论你做什么,都不要使用制表符。 对于现有文件,请忠实于现有的缩进。
例外:使用制表符的唯一例外是 <<-
制表符缩进的 here-document 的正文。
最大行长度为 80 个字符。
如果您必须编写超过 80 个字符的文字字符串,则应使用 here document 或嵌入式换行符(如果可能)。
超过 80 个字符且无法合理拆分的单词是可以的,但如果可能,这些项目应该位于其自己的行中,或者分解成一个变量。 示例包括文件路径和 URL,尤其是在对它们进行字符串匹配(例如 grep
)对于维护很有价值时。
# 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."
long_file="/i/am/an/exceptionally/loooooooooooooooooooooooooooooooooooooooooooooooooooong_file"
long_string_with_long_file="i am including an exceptionally \
/very/long/file\
in this long string."
# Long file converted into a shorter variable name with cleaner line breaking.
long_string_alt="i am an including an exceptionally ${long_file} in this long\
string"
# Just because a line contains an exception doesn't mean the rest of the
# line shouldn't be wrapped like usual.
bad_long_string_with_long_file="i am including an exceptionally /very/long/file in this long string."
如果管道不能全部放在一行上,则应每行拆分一个。
如果管道全部放在一行上,则应该放在一行上。
如果不是,则应在每行的管道段处拆分,管道位于换行符上,下一个管道部分缩进 2 个空格。 应始终如一地使用 \
来指示行继续。 这适用于使用 |
组合的命令链以及使用 ||
和 &&
的逻辑复合。
# All fits on one line
command1 | command2
# Long commands
command1 \
| command2 \
| command3 \
| command4
当区分管道与常规长命令继续时,这有助于提高可读性,尤其是在该行同时使用两者时。
注释需要在整个管道之前。 如果注释和管道很大且复杂,那么值得考虑使用辅助函数将它们的低级细节放在一边。
将 ; then
和 ; do
放在与 if
、for
或 while
相同的行上。
shell 中的控制流语句有些不同,但我们遵循与声明函数时使用花括号相同的原则。 也就是说:; then
和 ; do
应该与 if
/for
/while
/until
/select
在同一行。else
应该单独占一行,结束语句(fi
和 done
)应该单独占一行,并与起始语句垂直对齐。
示例
# If inside a function remember to declare 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}/${SESSION_ID}" ]]; then
log_date "Cleaning up old files in ${dir}/${SESSION_ID}"
rm "${dir}/${SESSION_ID}/"* || error_message
else
mkdir -p "${dir}/${SESSION_ID}" || error_message
fi
done
虽然可以省略 in "$@"
在 for 循环中,但我们建议始终包含它以提高清晰度。
for arg in "$@"; do
echo "argument: ${arg}"
done
;;
之前需要一个空格。;;
各占一行。匹配表达式从 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"
。
这些是强烈推荐的指导原则,但不是强制性规定。 尽管如此,这是一个建议而不是强制性的事实并不意味着应该轻视或淡化它。
它们按优先级顺序列出。
优先使用花括号分隔所有其他变量。
# 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}
中使用花括号不是一种引用形式。 必须同时使用“双引号”。
$?
、$#
、$$
、$!
。为了保持一致性,优先引用“命名”内部整数变量,例如 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
# https://gnu.ac.cn/software/bash/manual/html_node/Special-Parameters.html and
# https://mywiki.wooledge.org/BashGuide/Arrays for more
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$*"; echo "$#, $@")
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$@"; echo "$#, $@")
ShellCheck 项目可以识别 shell 脚本中的常见错误和警告。 建议用于所有脚本,无论大小。
使用 $(command)
而不是反引号。
嵌套的反引号需要使用 \
转义内部的反引号。 $(command)
格式在嵌套时不会改变,并且更易于阅读。
示例
# This is preferred:
var="$(command "$(command1)")"
# This is not:
var="`command \`command1\``"
[ … ]
和 [[ … ]]
与 [ … ]
、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. It might also trigger the
# "unexpected operator" error because `[` does not support `==`, only `=`.
if [ "filename" == f* ]; then
echo "Match"
fi
有关更多详细信息,请参见 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
在执行文件名的通配符扩展时,使用显式路径。
由于文件名可以以 -
开头,因此使用 ./*
而不是 *
扩展通配符更安全。
# Here's the contents of the directory:
# -f -r somedir somefile
# Incorrectly 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'
应避免使用 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)"
Bash 数组应用于存储元素列表,以避免引用复杂化。 这尤其适用于参数列表。 数组不应用于促进更复杂的数据结构(请参见上面的何时使用 Shell)。
数组存储字符串的有序集合,并且可以安全地扩展为命令或循环的各个元素。
应避免对多个命令参数使用单个字符串,因为它不可避免地会导致作者使用 eval
或尝试将引号嵌套在字符串中,这不会产生可靠或可读的结果,并导致不必要的复杂性。
# An array is assigned using parentheses, and can be appended to
# with +=( … ).
declare -a flags
flags=(--foo --bar='baz')
flags+=(--greeting="Hello ${name}")
mybinary "${flags[@]}"
# Don’t use strings for sequences.
flags='--foo --bar=baz'
flags+=' --greeting="Hello world"' # This won’t work as intended.
mybinary ${flags}
# Command expansions return single strings, not arrays. Avoid
# unquoted expansion in array assignments because it won’t
# work correctly if the command output contains special
# characters or whitespace.
# This expands the listing output into a string, then does special keyword
# expansion, and then whitespace splitting. Only then is it turned into a
# list of words. The ls command may also change behavior based on the user's
# active environment!
declare -a files=($(ls /directory))
# The get_arguments writes everything to STDOUT, but then goes through the
# same expansion process above before turning into a list of arguments.
mybinary $(get_arguments)
使用数组可能会增加脚本的复杂性。
应使用数组安全地创建和传递列表。 特别是,在构建一组命令参数时,使用数组以避免混淆引用问题。 使用带引号的扩展 – "${array[@]}"
– 访问数组。 但是,如果需要更高级的数据操作,则应完全避免使用 shell 脚本编写; 请参见上文。
优先使用进程替换或 readarray
内置命令 (bash4+),而不是通过管道传递到 while
。 管道会创建一个子 shell,因此在管道中修改的任何变量都不会传播到父 shell。
通过管道传递到 while
的隐式子 shell 可能会引入难以追踪的细微错误。
last_line='NULL'
your_command | while read -r line; do
if [[ -n "${line}" ]]; then
last_line="${line}"
fi
done
# This will always output 'NULL'!
echo "${last_line}"
使用进程替换也会创建一个子 shell。 但是,它允许从子 shell 重定向到 while
,而无需将 while
(或任何其他命令)放入子 shell 中。
last_line='NULL'
while read line; do
if [[ -n "${line}" ]]; then
last_line="${line}"
fi
done < <(your_command)
# This will output the last non-empty line from your_command
echo "${last_line}"
或者,使用 readarray
内置命令将文件读入数组,然后循环访问数组的内容。 请注意(由于与上面相同的原因),您需要将进程替换与 readarray
一起使用,而不是管道,但好处是循环的输入生成位于循环之前,而不是之后。
last_line='NULL'
readarray -t lines < <(your_command)
for line in "${lines[@]}"; do
if [[ -n "${line}" ]]; then
last_line="${line}"
fi
done
echo "${last_line}"
注意:谨慎使用 for 循环来迭代输出,如
for var in $(...)
中所示,因为输出由空格而不是行分割。 有时您会知道这是安全的,因为输出不能包含任何意外的空格,但是如果这不明显或没有提高可读性(例如$(...)
中的长命令),则while read
循环或readarray
通常更安全和更清晰。
始终使用 (( … ))
或 $(( … ))
,而不是 let
或 $[ … ]
或 expr
。
永远不要使用 $[ … ]
语法、expr
命令或 let
内置命令。
<
和 >
不会在 [[ … ]]
表达式中执行数值比较(它们而是执行词典比较;请参见测试字符串)。 最好完全不要使用 [[ … ]]
进行数值比较,而是使用 (( … ))
。
建议避免将 (( … ))
用作独立语句,并且要小心其表达式求值为零。
set -e
的情况下。 例如,set -e; i=0; (( i++ ))
将导致 shell 退出。# Simple calculation used as text - note the use of $(( … )) within
# a string.
echo "$(( 2 + 2 )) is 4"
# When performing arithmetic comparisons for testing
if (( a < b )); then
…
fi
# Some calculation assigned to a variable.
(( i = 10 * j + 400 ))
# This form is non-portable and deprecated
i=$[2 * 10]
# Despite appearances, 'let' isn't one of the declarative keywords,
# so unquoted assignments are subject to globbing wordsplitting.
# For the sake of simplicity, avoid 'let' and use (( … ))
let i="2 + 2"
# The expr utility is an external program and not a shell builtin.
i=$( expr 4 + 4 )
# Quoting can be error prone when using expr too.
i=$( expr 4 '*' 4 )
撇开风格上的考虑,shell 的内置算术运算比 expr
快得多。
当使用变量时,在 $(( … ))
中不需要 ${var}
(和 $var
)形式。 shell 知道为您查找 var
,并且省略 ${…}
可以使代码更简洁。 这与之前关于始终使用大括号的规则略有不同,因此这仅是一个建议。
# N.B.: Remember to declare your variables as integers when
# possible, and to prefer local variables over globals.
local -i hundred="$(( 10 * 10 ))"
declare -i five="$(( 10 / 2 ))"
# Increment the variable "i" by three.
# Note that:
# - We do not write ${i} or $i.
# - We put a space after the (( and before the )).
(( i += 3 ))
# To decrement the variable "i" by five:
(( i -= 5 ))
# Do some complicated computations.
# Note that normal arithmetic operator precedence is observed.
hr=2
min=5
sec=30
echo "$(( hr * 3600 + min * 60 + sec ))" # prints 7530 as expected
虽然在 .bashrc
文件中很常见,但应避免在脚本中使用别名。 正如 Bash 手册 指出的那样
几乎在所有情况下,shell 函数都优于别名。
别名使用起来很麻烦,因为它们需要仔细引用和转义其内容,并且错误可能难以发现。
# this evaluates $RANDOM once when the alias is defined,
# so the echo'ed string will be the same on each invocation
alias random_name="echo some_prefix_${RANDOM}"
函数提供了别名功能的超集,应始终首选函数。
random_name() {
echo "some_prefix_${RANDOM}"
}
# Note that unlike aliases function's arguments are accessed via $@
fancy_ls() {
ls -lh "$@"
}
小写,用下划线分隔单词。 使用 ::
分隔库。 函数名称后需要括号。 关键字 function
是可选的,但必须在整个项目中保持一致。
如果您正在编写单个函数,请使用小写并用下划线分隔单词。 如果您正在编写一个包,请使用 ::
分隔包名称。 但是,用于交互使用的函数可以选择避免使用冒号,因为它可能会使 bash 自动完成混淆。
大括号必须与函数名称位于同一行(与 Google 的其他语言一样),并且函数名称和括号之间没有空格。
# Single function
my_func() {
…
}
# Part of a package
mypackage::my_func() {
…
}
当函数名称后存在“()”时,function
关键字是多余的,但可以增强对函数的快速识别。
与函数名称相同。
循环的变量名称应与您正在循环的任何变量类似地命名。
for zone in "${zones[@]}"; do
something_with "${zone}"
done
常量和任何导出到环境的内容都应大写,用下划线分隔,并在文件顶部声明。
# Constant
readonly PATH_TO_FILES='/some/path'
# Both constant and exported to the environment
declare -xr ORACLE_SID='PROD'
为了清晰起见,建议使用 readonly
或 export
,而不是等效的 declare
命令。 你可以一个接一个地做,比如
# Constant
readonly PATH_TO_FILES='/some/path'
export PATH_TO_FILES
可以在运行时或在条件中设置常量,但之后应立即将其设置为只读。
ZIP_VERSION="$(dpkg --status zip | sed -n 's/^Version: //p')"
if [[ -z "${ZIP_VERSION}" ]]; then
ZIP_VERSION="$(pacman -Q --info zip | sed -n 's/^Version *: //p')"
fi
if [[ -z "${ZIP_VERSION}" ]]; then
handle_error_and_quit
fi
readonly ZIP_VERSION
小写,如果需要,用下划线分隔单词。
这是为了与 Google 中的其他代码样式保持一致:maketemplate
或 make_template
但不是 make-template
。
使用 local
声明特定于函数的变量。
通过在声明局部变量时使用 local
,确保局部变量仅在函数及其子函数中可见。 这样可以避免污染全局命名空间,并无意中设置可能在函数外部具有意义的变量。
当赋值值由命令替换提供时,声明和赋值必须是单独的语句;因为 local
内置函数不会传播命令替换的退出代码。
my_func2() {
local name="$1"
# Separate lines for declaration and assignment:
local my_var
my_var="$(my_func)"
(( $? == 0 )) || return
…
}
my_func2() {
# DO NOT do this:
# $? will always be zero, as it contains the exit code of 'local', not my_func
local my_var="$(my_func)"
(( $? == 0 )) || return
…
}
将所有函数放在文件中常量下面的位置。 不要在函数之间隐藏可执行代码。 这样做会使代码难以理解,并在调试时导致令人讨厌的意外。
如果你有函数,请将它们放在文件顶部的附近。 只有 includes,set
语句和设置常量可以在声明函数之前完成。
对于足够长的脚本,需要一个名为 main
的函数,以便至少包含另一个函数。
为了轻松找到程序的开头,请将主程序放在名为 main
的函数中作为最底层的函数。 这提供了与代码库其余部分的一致性,并允许您将更多变量定义为 local
(如果主代码不是函数,则无法完成)。 文件中的最后一行非注释行应该是对 main
的调用。
main "$@"
显然,对于只有线性流程的短脚本,main
是多余的,因此不是必需的。
始终检查返回值并提供有用的返回值。
对于非管道命令,使用 $?
或直接通过 if
语句检查以保持简单。
示例
if ! mv "${file_list[@]}" "${dest_dir}/"; then
echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
exit 1
fi
# Or
mv "${file_list[@]}" "${dest_dir}/"
if (( $? != 0 )); then
echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
exit 1
fi
Bash 还有 PIPESTATUS
变量,该变量允许检查管道所有部分的返回代码。 如果只需要检查整个管道的成功或失败,则以下是可以接受的
tar -cf - ./* | ( cd "${dir}" && tar -xf - )
if (( PIPESTATUS[0] != 0 || PIPESTATUS[1] != 0 )); then
echo "Unable to tar files to ${dir}" >&2
fi
但是,由于 PIPESTATUS
会在您执行任何其他命令后立即被覆盖,因此如果您需要根据管道中发生错误的位置采取不同的操作,则需要立即在运行命令后将 PIPESTATUS
分配给另一个变量(不要忘记 [
是一个命令,它将清除 PIPESTATUS
)。
tar -cf - ./* | ( cd "${DIR}" && tar -xf - )
return_codes=( "${PIPESTATUS[@]}" )
if (( return_codes[0] != 0 )); then
do_something
fi
if (( return_codes[1] != 0 )); then
do_something_else
fi
如果在调用 shell 内置命令和调用单独的进程之间进行选择,请选择内置命令。
我们更喜欢使用内置函数,例如 bash
提供的 参数扩展 功能,因为它更高效、更健壮和更便携(尤其是与 sed
之类的东西相比)。 另请参见 =~
运算符。
例子
# Prefer this:
addition="$(( X + Y ))"
substitution="${string/#foo/bar}"
if [[ "${string}" =~ foo:(\d+) ]]; then
extraction="${BASH_REMATCH[1]}"
fi
# Instead of this:
addition="$(expr "${X}" + "${Y}")"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
extraction="$(echo "${string}" | sed -e 's/foo:\([0-9]\)/\1/')"
在整个代码库中始终如一地使用一种风格,使我们可以专注于其他(更重要)的问题。 一致性还允许自动化。 在许多情况下,归因于“保持一致”的规则归结为“只需选择一个并停止担心它”; 允许在这些要点上保持灵活性的潜在价值,被人们争论它们的成本所抵消。
但是,一致性有限制。 当没有明确的技术论据,也没有长期方向时,它是一个很好的决胜局。 一般来说,不应将一致性用作以旧风格做事而不考虑新风格的好处或代码库随着时间的推移趋向于新风格的理由。