- 1 Background
- 2 Shell Files and Interpreter Invocation
- 2.1 File Extensions
- 2.2 SUID/SGID
- 3 Environment
- 3.1 STDOUT vs STDERR
- 4 Comments
- 4.1 File Header
- 4.2 Function Comments
- 4.3 Implementation Comments
- 4.4 TODO Comments
- 5 Formatting
- 5.1 Indentation
- 5.2 Line Length and Long Strings
- 5.3 Pipelines
- 5.4 Loops
- 5.5 Case statement
- 5.6 Variable expansion
- 5.7 Quoting
- 6 Features and Bugs
- 6.1 ShellCheck
- 6.2 Command Substitution
- 6.3 Test,
[ … ]
, and\[\[ … \]\]
- 6.4 Testing Strings
- 6.5 Wildcard Expansion of Filenames
- 6.6 Eval
- 6.7 Arrays
- 6.8 Pipes to While
- 6.9 Arithmetic
- 7 Naming Conventions
- 8 Calling Commands
- 9 Conclusion
本文为摘录,原文为: https://google.github.io/styleguide/shellguide.html
1 Background
Bash is the only shell scripting language permitted for executables.
2 Shell Files and Interpreter Invocation
2.1 File Extensions
- 可执行文件: 应该没有扩展名(强烈推荐), 或者使用 .sh 扩展名。
- 库文件: 必须使用 .sh 扩展名,且 不应该是可执行 的。
在执行一个程序时,不需要知道它使用的是哪种语言, 而且 shell 不需要扩展名,所以我们更倾向于不给可执行文件使用扩展名。
然而,对于库文件,知道使用的是哪种语言很重要, 有时候需要使用不同语言的相似库文件。这样, 具有相同功能但不同语言的库文件可以有相同的文件名,只是在文件名末尾加上特定于语言的后缀。
2.2 SUID/SGID
在 shell 脚本中禁止使用 SUID 和 SGID。
由于 shell 存在许多安全问题,几乎不可能足够安全地使用 SUID/SGID。虽然 bash 确实使得运行 SUID 变得困难,但在某些平台上仍然有可能实现,这就是为什么我们明确禁止使用它的原因。
如果需要提供特权访问,请使用 sudo。
3 Environment
3.1 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
4 Comments
4.1 File Header
每个文件都要以其内容的描述开始。
每个文件必须包含一个顶级注释,其中包含其内容的简要概述。版权声明和作者信息是可选的。
示例:
#!/bin/bash
#
# Perform hot backups of Oracle databases.
4.2 Function Comments
- 任何不明显且不简短的函数必须注释。
- 无论长度或复杂性如何,库中的每个函数都必须注释。
通过阅读注释(和提供的自助信息)而无需阅读代码,他人应能够了解如何使用您的程序或使用库中的函数。
所有函数的注释应描述预期的 API 行为,包括:
- 函数的描述。
- 全局变量:使用和修改的全局变量列表。
- 参数:接受的参数。
- 输出:STDOUT 或 STDERR 的输出。
- 返回值:除最后一条运行命令的默认退出状态之外的返回值。
Example:
#######################################
# Cleanup files from the backup directory.
# Globals:
# BACKUP_DIR
# ORACLE_SID
# Arguments:
# None
#######################################
function cleanup() {
…
}
4.3 Implementation Comments
- 对复杂、不明显、有趣或重要的部分加以注释
这遵循了注释的一般惯例。不要对每个地方都进行评论。如果有复杂的算法或者你正在做一些与平常 不同的事情,可以简短地加上注释。
TODO 4.4 Comments
- 对于临时的、短期的解决方案或者是足够好但不完美的代码,请使用 TODO 注释。
这与 C++指南的约定一致。
TODO 应该包含全大写的 TODO 字符串,后面跟着在此问题上有最好理解背景的人的名字、电子邮件 地址或其他标识符。主要目的是拥有一致的 TODO,可以通过搜索找到如何根据请求获取更多细节的 方法。TODO 并不是一个承诺,指定的人会解决这个问题。因此,当您创建一个 TODO 时,通常会使 用您自己的名字。
# TODO(mrmonkey): Handle the unlikely edge cases (bug ####)
5 Formatting
在修改文件时,应遵循已有的样式,但以下规定适用于任何新代码。
5.1 Indentation
缩进两个空格,不要使用制表符。
- 使用空行来分隔不同的块以提高可读性。缩进为两个空格。
- 无论你做什么,都不要使用制表符。
对于现有的文件,请保持忠于现有的缩进方式。
5.2 Line Length and Long Strings
最大行长度是 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."
5.3 Pipelines
- 如果一个管道能全部放在一行上,应该放在一行上。
- 如果一个管道不能:
- 应该在每个管道段落上分开一行,
- 管道放在新的一行上,
- 下一个管道段落应有两个空格的缩进。
- 这适用于使用“|”连接的一系列命令以及使用“||”和“&&”进行逻辑连接的组合。
# All fits on one line
command1 | command2
# Long commands
command1 \
| command2 \
| command3 \
| command4
5.4 Loops
将 ; 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
5.5 Case statement
- 替代缩进使用 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
5.6 Variable expansion
按照优先顺序:
- 保持与您发现的代码一致;
- 引用您的变量 (使用
"
); - 优先使用
"${var}"
而非"$var"
。
这些是强烈推荐的准则,但并非强制性规定。然而,推荐而非强制并不意味着可以轻视或低估。
5.7 Quoting
- 无论如何都要引用包含变量、命令替换、空格或 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 "$#, $@")
6 Features and Bugs
6.1 ShellCheck
ShellCheck 项目可帮助您识别 shell 脚本中的常见错误和警告。无论是大型还是小型脚本,都推荐 使用该工具。
6.2 Command Substitution
使用 $(command)
代替反引号。
嵌套的反引号需要用 \
来转义内部的反引号。 $(command)
的格式在嵌套时不会改变,并且更容易读取。
Example:
# This is preferred:
var="$(command "$(command1)")"
# This is not:
var="`command \`command1\``"
6.3 Test, [ … ]
, and \[\[ … \]\]
[[ ... ]]
is preferred over [ … ]
, test
and /usr/bin/[
.
[[ ... ]]
减少错误的发生,因为在 [[ ... ]]
之间没有路径名展开或者单词分割。
此外, [[ ... ]]
可以进行正则表达式匹配,而 [ … ]
不可以。
6.4 Testing Strings
在可能的情况下,请使用引号而不是填充字符 (filler character)。
Bash 足够聪明,能够处理测试中的空字符串。因此,考虑到代码更易读性,应该使用 test 来 处理空字符串或非空字符串,而不是使用填充字符。
# 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
To avoid confusion about what you’re testing for, explicitly use -z or -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
6.5 Wildcard Expansion of Filenames
当使用通配符扩展文件名时,请使用显式路径。
由于文件名可能以 - 开头,使用 ./*
而不是 *
来扩展通配符会更安全。
# 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'
6.6 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)"
6.7 Arrays
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)
6.7.1 Arrays Pros
- 使用数组可以在不混淆引用语义的情况下创建列表。相反地,不使用数组会导致在字符串内部错误地嵌套引用的尝试。
- 数组使得可以安全地存储包含空格的任意字符串的序列/列表。
6.7.2 Arrays Cons
使用数组可能会增加脚本的复杂性增加的风险。
6.7.3 Arrays Decision
应该使用数组来安全地创建和传递列表。特别是在构建一组命令参数时,应该使用数组来避免引号的混乱问题。使用引用扩展 - “${array[@]}” - 来访问数组。然而,如果需要更高级的数据操作,应该完全避免使用 shell 脚本;参见上面的内容。
6.8 Pipes to While
使用进程替换或者优先使用 bash4+里的 readarray 命令,而不是使用管道传递给 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 中。
6.9 Arithmetic
- 始终使用(( … ))或\((( … ))而不是 let 或\)[ … ]或 expr。
- 永远不要使用$[ … ]语法,expr 命令或 let 内置命令。
- <and>在\[[]]表达式内部不执行数字比较(而执行字典比较;请参见字符串测试)。
- 不要对数值比较使用[\[]],而是使用
(( … ))
。 - 特别是在启用 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 )
7 Naming Conventions
7.1 Function Names
- 小写字母,用下划线分隔单词。
- 使用双冒号来分隔库。
- 在函数名后面需要使用括号。
- 关键字 function 是可选的,但必须在整个项目中保持一致使用。
- 如果你正在编写单个函数,请使用小写字母,并使用下划线分隔单词。
- 如果你正在编写一个包,应使用双冒号分隔包名。
- 大括号必须与函数名在同一行(与 Google 的其他语言一样),函数名与括号之间不要有空格。
# Single function
my_func() {
…
}
# Part of a package
mypackage::my_func() {
…
}
当函数名后面跟着“()”时,“function”关键字是多余的,但它能够加强对函数的快速识别。
7.2 Variable Names
关于函数名称。
循环变量的变量名称应与您正在遍历的任何变量命名方式相似。
for zone in "${zones[@]}"; do
something_with "${zone}"
done
7.3 Constants and Environment Variable Names
- 所有字母大写,用下划线分隔,在文件顶部声明。
- 常量和任何导出到环境的内容都应大写。
# Constant
readonly PATH_TO_FILES='/some/path'
# Both constant and environment
declare -xr ORACLE_SID='PROD'
有些东西在它们第一次设置后变得恒定(例如通过 getopts)。因此,在 getopts 或基于条件设置一个 constant 是可以的,但是之后应立即将其设置为 readonly。为了清晰起见,建议使用 readonly 或 export 而不是等效的 declare 命令。
VERBOSE='false'
while getopts 'v' flag; do
case "${flag}" in
v) VERBOSE='true' ;;
esac
done
readonly VERBOSE
7.4 Source Filenames
使用小写字母,并在需要时用下划线分隔单词。
这是为了与 Google 中的其他代码风格保持一致:maketemplate 或 make_template,而不是 make-template。
7.5 Read-only Variables
使用 readonly 或 declare -r 来确保它们是只读的。
由于全局变量在 shell 中被广泛使用,因此在处理它们时捕捉错误是很重要的。当你声明一个变量 是只读的时候,要明确表示这一点。
zip_version="$(dpkg --status zip | grep Version: | cut -d ' ' -f 2)"
if [[ -z "${zip_version}" ]]; then
error_message
else
readonly zip_version
fi
7.6 Use Local Variables
使用 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
…
}
7.7 Function Location
将所有函数放在常量的下方,不要在函数之间隐藏可执行代码。这样做会使代码难以跟踪,并在调试 时会产生令人讨厌的意外。
如果您有函数,请将它们都放在文件的顶部附近。只有 includes、set 语句和设置常量可以在声明 函数之前完成。
7.8 main
如果脚本足够长,至少包含一个其他函数,则需要一个名为 main 的函数。
为了方便找到程序的起始点,将主程序放在一个名为 main 的函数中,作为最底层的函数。这样可以保 持与代码库其余部分的一致性,同时还允许您将更多变量定义为本地变量(如果主代码不是函数,则 无法完成此操作)。文件中最后一个非注释行应该是对 main 的调用:
8 Calling Commands
8.1 Checking Return Values
始终检查返回值并提供有信息的返回值。
对于未使用管道的命令,请使用 $? 或直接通过 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
8.2 Builtin Commands vs. External Commands
在选择调用内置 shell 和调用独立进程之间,选择内置 shell。
我们更喜欢使用内置的函数,例如 bash(1) 中的参数扩展函数,因为它更健壮和可移植(特别是与类似 sed 的工具相比较)。
# Prefer this:
addition=$(( X + Y ))
substitution="${string/#foo/bar}"
# Instead of this:
addition="$(expr "${X}" + "${Y}")"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
9 Conclusion
Use common sense and BE CONSISTENT.