特性和 Bug
ShellCheck
ShellCheck 项目 可以识别你的 Shell 脚本中的常见 Bug 和警告。建议所有脚本都使用它,无论大小。
命令替换(Command Substitution)
使用 $(command) 而不是反引号。
嵌套的反引号需要用 \ 转义内部的反引号。
$(command) 格式在嵌套时不会改变,而且更易阅读。
示例:
# This is preferred:
var="$(command "$(command1)")"# This is not:
var="`command \`command1\``"Test、[ … ] 和 [[ … ]]
[[ … ]] 优先于 [ … ]、test 和 /usr/bin/[。
[[ … ]] 可以减少错误,因为在 [[ 和 ]] 之间不会发生路径名展开(Pathname
Expansion)或单词分割(Word Splitting)。此外,[[ … ]] 允许模式匹配和正则表达式
匹配,而 [ … ] 不支持。
# 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。
字符串测试(Testing Strings)
尽可能使用引号而不是填充字符。
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为了清晰起见,使用 == 进行相等比较而非 =,尽管两者都可以工作。
前者鼓励使用 [[,后者可能与赋值混淆。
但是,在 [[ … ]] 中使用 < 和 > 时要小心,它们执行的是字典序比较
(Lexicographical Comparison)。
对于数值比较,请使用 (( … )) 或 -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文件名通配符展开(Wildcard Expansion)
在做文件名通配符展开时使用显式路径。
由于文件名可以以 - 开头,使用 ./* 而不是 * 来展开通配符会安全得多。
# 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。
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)"数组(Array)
应使用 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 脚本;
参见上文。
管道到 While(Pipes to While)
优先使用进程替换(Process Substitution)或 readarray 内建命令(Bash 4+),
而不是通过管道传递给 while。管道会创建子 Shell(Subshell),
因此在管道中修改的任何变量不会传播到父 Shell。
管道到 while 中的隐式子 Shell 可能引入难以追踪的微妙 Bug。
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通常更安全、更清晰。
算术运算(Arithmetic)
始终使用 (( … )) 或 $(( … )),而不是 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