Skip to Content
Shell特性和 Bug

特性和 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
Last updated on