文章目录
修订版本 2.02
由许多谷歌员工创作、修订和维护。
注:翻译内容可能因语言特性而略有调整,但力求保持准确、流畅和地道。
目录
背景
选择使用哪种 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
。
环境
标准输出 vs 标准错误
所有错误消息应该输出到 STDERR
。
这样可以更容易地区分正常状态和实际问题。
建议使用一个函数来打印错误消息以及其他状态信息。
err() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2
}
if ! do_something; then
err "无法执行某事"
exit 1
fi
注释
文件头部注释
每个文件都应该以描述其内容的说明开头。
每个文件必须有一个顶级注释,包括对其内容的简要概述。版权声明和作者信息是可选的。
示例:
#!/bin/bash
#
# 执行 Oracle 数据库的热备份。
函数注释
任何既不明显又不简短的函数都必须进行注释。库中的任何函数都必须进行注释,无论其长度或复杂性如何。
通过阅读注释(和提供的自助信息,如果有的话),他人应该能够学习如何使用你的程序或使用你的库中的函数,而无需阅读代码。
所有函数注释都应该使用以下方式描述预期的 API 行为:
- 函数的描述。
- Globals(全局变量):所使用和修改的全局变量列表。
- Arguments(参数):所使用的参数。
- Outputs(输出):输出到 STDOUT 或 STDERR。
- Returns(返回值):除了最后一个运行的命令的默认退出状态以外的返回值。
示例:
#######################################
# 清理备份目录中的文件。
# Globals:
# BACKUP_DIR
# ORACLE_SID
# Arguments:
# 无
#######################################
function cleanup() {
…
}
#######################################
# 获取配置目录。
# Globals:
# SOMEDIR
# Arguments:
# 无
# Outputs:
# 将位置写入标准输出
#######################################
function get_dir() {
echo "${SOMEDIR}"
}
#######################################
# 以复杂的方式删除文件。
# Arguments:
# 要删除的文件路径。
# Returns:
# 如果成功删除,则返回 0;出错时返回非零值。
#######################################
function del_thing() {
rm "$1"
}
实现注释
对于代码中复杂、不明显、有趣或重要的部分进行注释。
这遵循了谷歌的通用编码注释实践。不要对所有内容都进行注释。如果存在复杂的算法或者你正在做一些不寻常的事情,只需在其附上简短的注释。
待办注释
对于临时的、短期的解决方案或足够好但不完美的代码,使用 TODO 注释。
这与 C++ 指南 中的约定相匹配。
TODO
注释应该使用全大写的字符串 TODO
,后面跟着具有与 TODO
引用的问题最佳上下文的人的姓名、电子邮件地址或其他标识符。主要目的是拥有一个一致的 TODO
,可以通过搜索找出如何在请求时获取更多详细信息。TODO
不是一个承诺,表明被引用的人会修复问题。因此,当创建 TODO
时,通常会使用你自己的名字。
示例:
# TODO(mrmonkey): 处理不太可能的边界情况(错误 ####)
格式化
尽管在修改文件时应遵循已有的样式,但对于任何新的代码,需要遵循以下要求。
缩进
缩进2个空格。不使用制表符。
在块之间使用空行以提高可读性。缩进是两个空格。无论如何,都不要使用制表符。对于现有文件,请保持对现有缩进的忠实。
行长度和长字符串
最大行长度为80个字符。
如果必须编写超过80个字符的字符串,则应使用here文档或内嵌换行符进行操作,如果可能的话。必须超过80个字符并且不能明智地拆分的字面字符串是可以的,但强烈建议寻找缩短字符串的方法。
# 使用 'here document'
cat <
管道
如果管道不全都适合一行,则将其拆分为每行一个。
如果整个管道适合一行,则应在一行上。
如果不适合,则应在每行一个管道分段上拆分,换行处放置管道,并在下一个管道段的开头缩进2个空格。这适用于使用 |
组合的命令链,以及使用 ||
和 &&
的逻辑组合。
# 适合一行
command1 | command2
# 长命令
command1
| command2
| command3
| command4
循环
将 ; do
和 ; then
放在与 while
、for
或 if
同一行。
在Shell中的循环与大括号的声明函数类似,因此在声明函数时遵循相同的原则。即 ; then
和 ; do
应与 if/for/while 放在同一行上。else
应单独一行,关闭语句应该单独一行,并与开放语句在垂直方向上对齐。
示例:
# 如果在函数内部,请考虑将循环变量声明为
# 本地变量,以避免其泄漏到全局环境中:
# local dir
for dir in "${dirs_to_cleanup[@]}"; do
if [[ -d "${dir}/${ORACLE_SID}" ]]; then
log_date "在 ${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语句
- 用2个空格缩进选项。
- 一行选项需要在模式的右括号之后和
;;
之前加一个空格。
- 长或多命令选项应该拆分为多行,模式、操作和
;;
放在单独的行上。
;;
之前加一个空格。;;
放在单独的行上。匹配表达式从 case
和 esac
缩进一个级别。多行操作再缩进一个级别。通常情况下,不需要引用匹配表达式。模式表达式不应该在开括号之前。避免使用 ;&
和 ;;&
符号。
case "${expression}" in
a)
variable="…"
some_command "${variable}" "${other_expr}" …
;;
absolute)
actions="relative"
another_command "${actions}" "${other_expr}" …
;;
*)
error "意外的表达式 '${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 "意外的选项 ${flag}" ;;
esac
done
变量扩展
按优先级顺序:保持与现有代码一致;对变量加引号;优先使用 "${var}"
而不是 "$var"
。
这些是强烈推荐的准则,但并非强制性规定。尽管如此,这并不意味着这只是一个建议,不是强制性的,所以不能轻视或忽视它。
它们按优先级顺序列出。
-
保持与现有代码的一致性。
-
引用变量,参见下面的引用。
-
不要使用花括号将单字符shell特殊符号/位置参数括起来,除非绝对必要或避免深度混淆。
优先使用花括号将所有其他变量括起来。
# 推荐的用例部分。
# 优选的风格,用于 'special' 变量:
echo "位置参数: $1" "$5" "$3"
echo "特殊变量: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ $=$$ …"
# 需要花括号的情况:
echo "许多参数: ${10}"
# 避免混淆的花括号用法:
# 输出为 "a0b0c0"
set -- a b c
echo "${1}0${2}0${3}0"
# 其他变量的首选风格:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read -r f; do
echo "文件=${f}"
done < <(find /tmp)
# 不推荐的用例部分。
# 未引用的变量、未使用花括号的变量、花括号将单字符shell特殊变量括起来。
echo a=$avar "b=$bvar" "PID=${$}" "${1}"
# 混淆的用法:这会被展开为 "${1}0${2}0${3}0",
# 而不是 "${10}${20}${30}
set -- a b c
echo "$10$20$30"
注意:在 ${var}
中使用花括号 不是 引用的一种形式。引号必须同时使用。
引用
- 总是引用包含变量、命令替换、空格或shell元字符的字符串,除非需要小心不带引号的扩展,或者是一个Shell内部整数(见下一点)。
- 使用数组来安全地引用列表元素,尤其是命令行标志。参见下面的数组。
- 可选地引用Shell内部、只读的特殊整数变量:
$?
、$#
、$$
、$!
(man bash)。出于一致性考虑,更喜欢引用“命名”的内部整数变量,例如 PPID 等。
- 更喜欢引用是“单词”(而不是命令选项或路径名)的字符串。
- 绝不引用文字整数。
- 要注意在
[[ … ]]
中进行模式匹配的引用规则。参见下面的Test、[… ]
和 [[… ]]
部分。
- 使用
"$@"
,除非有特定原因使用 $*
,例如在消息或日志中简单地附加参数。
# '单引号' 表示不需要替换。
# "双引号" 表示需要替换。
# 简单的例子
# "引用命令替换"
# 请注意,在 "$()" 中嵌套的引号无需转义。
flag="$(some_command and its args "$@" 'quoted separately')"
# "引用变量"
echo "${flag}"
# 使用数组与引用扩展以安全引用元素列表。
declare -a FLAGS
FLAGS=( --foo --bar='baz' )
readonly FLAGS
mybinary "${FLAGS[@]}"
# 对内部整数变量不需要引用,这是可以的。
if (( $# > 3 )); then
echo "ppid=${PPID}"
fi
# "不引用文字整数"
value=32
# "引用命令替换",即使您期望是整数
number="$(generate_number)"
# "优先引用单词",不是必须的
readonly USE_INTEGER='true'
# "引用Shell元字符"
echo '你好,陌生人,很高兴认识你。赚很多 $$$'
echo "进程 $$:完成制作 $$$。"
# "命令选项或路径名"
# (假设 $1 在这里包含一个值)
grep -li Hugo /dev/null "$1"
# 较少简单例子
# "引用变量,除非明确为假":ccs 可能为空
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}
# 位置参数的预防措施:$1 可能未设置
# 单引号保留正则表达式。
grep -cP '([Ss]pecial||?characters*)$' ${1:+"$1"}
# 对于传递参数,
# "$@" 几乎每次都是正确的选择,而
# $* 几乎每次都是错误的选择:
#
# * $* 和 $@ 会在空格上拆分,覆盖带有空格的参数
# 并丢弃空字符串;
# * "$@" 会保留参数,所以不提供参数将导致不传递参数;
# 这在大多数情况下是传递参数时想要使用的方式。
# * "$*" 扩展为一个参数,其中所有参数都由(通常)空格连接,
# 所以不提供参数将导致传递一个空字符串。
# (请查阅 `man bash` 以了解详细情况;- 注:指的是 `man bash` 中的相关内容)
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$*"; echo "$#, $@")
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$@"; echo "$#, $@")
特性和错误
ShellCheck
$?
、$#
、$$
、$!
(man bash)。出于一致性考虑,更喜欢引用“命名”的内部整数变量,例如 PPID 等。[[ … ]]
中进行模式匹配的引用规则。参见下面的Test、[… ]
和 [[… ]]
部分。"$@"
,除非有特定原因使用 $*
,例如在消息或日志中简单地附加参数。# '单引号' 表示不需要替换。
# "双引号" 表示需要替换。
# 简单的例子
# "引用命令替换"
# 请注意,在 "$()" 中嵌套的引号无需转义。
flag="$(some_command and its args "$@" 'quoted separately')"
# "引用变量"
echo "${flag}"
# 使用数组与引用扩展以安全引用元素列表。
declare -a FLAGS
FLAGS=( --foo --bar='baz' )
readonly FLAGS
mybinary "${FLAGS[@]}"
# 对内部整数变量不需要引用,这是可以的。
if (( $# > 3 )); then
echo "ppid=${PPID}"
fi
# "不引用文字整数"
value=32
# "引用命令替换",即使您期望是整数
number="$(generate_number)"
# "优先引用单词",不是必须的
readonly USE_INTEGER='true'
# "引用Shell元字符"
echo '你好,陌生人,很高兴认识你。赚很多 $$$'
echo "进程 $$:完成制作 $$$。"
# "命令选项或路径名"
# (假设 $1 在这里包含一个值)
grep -li Hugo /dev/null "$1"
# 较少简单例子
# "引用变量,除非明确为假":ccs 可能为空
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}
# 位置参数的预防措施:$1 可能未设置
# 单引号保留正则表达式。
grep -cP '([Ss]pecial||?characters*)$' ${1:+"$1"}
# 对于传递参数,
# "$@" 几乎每次都是正确的选择,而
# $* 几乎每次都是错误的选择:
#
# * $* 和 $@ 会在空格上拆分,覆盖带有空格的参数
# 并丢弃空字符串;
# * "$@" 会保留参数,所以不提供参数将导致不传递参数;
# 这在大多数情况下是传递参数时想要使用的方式。
# * "$*" 扩展为一个参数,其中所有参数都由(通常)空格连接,
# 所以不提供参数将导致传递一个空字符串。
# (请查阅 `man bash` 以了解详细情况;- 注:指的是 `man bash` 中的相关内容)
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$*"; echo "$#, $@")
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$@"; echo "$#, $@")
ShellCheck 项目 可以识别您的Shell脚本中的常见错误和警告。无论脚本是大是小,都建议使用它。
命令替换
使用 $(command)
而不是反引号。
嵌套的反引号需要用
转义内部的反引号。$(command)
格式在嵌套时不会改变,并且更容易阅读。
示例:
# 这是首选方式:
var="$(command "$(command1)")"
# 这不是:
var="`command `command1``"
测试,[ … ] 和 [[ … ]]
[[ … ]]
优于 [ … ]
,test
和 /usr/bin/[
。
[[ … ]]
减少了错误,因为在 [[
和 ]]
之间不会发生路径名扩展或单词分割。此外,[[ … ]]
允许正则表达式匹配,而 [ … ]
不允许。
# 这确保左边的字符串由 alnum 字符类中的字符组成,后跟字符串 name。
# 请注意,RHS 在这里不应该被引用。
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
echo "匹配"
fi
# 这匹配精确的模式 "f*"(在这种情况下不匹配)
if [[ "filename" == "f*" ]]; then
echo "匹配"
fi
# 这会导致 "参数过多" 错误,因为 f* 被扩展为当前目录的内容
if [ "filename" == f* ]; then
echo "匹配"
fi
要了解更多详细信息,请参阅 http://tiswww.case.edu/php/chet/bash/FAQ 中的 E14 部分。
测试字符串
在可能的情况下,请使用引号而不是填充字符。
Bash 足够聪明,可以处理测试中的空字符串。因此,鉴于代码更容易阅读,对空/非空字符串或空字符串使用测试,而不是填充字符。
# 使用这种方式:
if [[ "${my_var}" == "some_string" ]]; then
do_something
fi
# -z(字符串长度为零)和 -n(字符串长度不为零)优于测试空字符串
if [[ -z "${my_var}" ]]; then
do_something
fi
# 这是可以的(确保空边上的引号),但不是首选方式:
if [[ "${my_var}" == "" ]]; then
do_something
fi
# 不要这样:
if [[ "${my_var}X" == "some_stringX" ]]; then
do_something
fi
为避免对正在测试的内容产生困惑,明确地使用 -z
或 -n
。
# 使用这个
if [[ -n "${my_var}" ]]; then
do_something
fi
# 而不是这个
if [[ "${my_var}" ]]; then
do_something
fi
为了清晰起见,对于相等性使用 ==
而不是 =
,尽管两者都可以工作。前者鼓励使用 [[]
,而后者可能会与赋值混淆。然而,在 [[ … ]]
中使用 <
和 >
时要小心,它执行词典比较。对于数字比较,请使用 (( … ))
或 -lt
和 -gt
。
# 使用这个
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
# 而不是这个
if [[ "${my_var}" = "val" ]]; then
do_something
fi
# 可能意外的词典比较。
if [[ "${my_var}" > 3 ]]; then
# 对于 4 为真,对于 22 为假。
do_something
fi
文件名的通配符扩展
在进行文件名的通配符扩展时,请使用显式路径。
由于文件名可以以 -
开头,因此使用 ./*
而不是 *
扩展通配符更安全。
# 下面是目录的内容:
# -f -r somedir somefile
# 错误地强制删除了目录中的几乎所有内容
psa@bilby$ rm -v *
removed directory: `somedir'
removed `somefile'
# 相反:
psa@bilby$ rm -v ./*
removed `./-f'
removed `./-r'
rm: cannot remove `./somedir': Is a directory
removed `./somefile'
Eval
应避免使用 eval
。
eval
在用于变量赋值时会修改输入,并且可以设置变量而无法检查这些变量是什么。
# 这会设置什么?
# 它是否成功?部分或全部成功?
eval $(set_my_variables)
# 如果返回的值中有一个空格会发生什么?
variable="$(eval some_function)"
数组
应使用 Bash 数组来存储元素列表,以避免引号问题。这特别适用于参数列表。不应使用数组来实现更复杂的数据结构(请参见上面的何时使用Shell)。
数组存储有序的字符串集合,并且可以安全地扩展为命令或循环的单独元素。
应避免使用单个字符串作为多个命令参数,因为这最终会导致作者使用 eval
或尝试在字符串中嵌套引号,这不会产生可靠或可读的结果,而且会导致不必要的复杂性。
# 使用括号分配数组,可以使用 +=( … ) 追加。
declare -a flags
flags=(--foo --bar='baz')
flags+=(--greeting="Hello ${name}")
mybinary "${flags[@]}"
# 不要使用字符串表示序列。
flags='--foo --bar=baz'
flags+=' --greeting="Hello world"' # 这不会按预期工作。
mybinary ${flags}
# 命令扩展返回单个字符串,而不是数组。避免在数组分配中使用未引用的扩展,因为如果命令输出包含特殊字符或空格,它将无法正常工作。
# 这将把列表输出扩展为字符串,然后执行特殊关键字扩展,然后进行空格分割。然后它才会变成一系列单词。ls 命令还可能根据用户的活动环境而改变行为!
declare -a files=($(ls /directory))
# get_arguments 将所有内容写入 STDOUT,然后经过上述的扩展过程,在变成一系列参数之前,经历了同样的过程。
mybinary $(get_arguments)
数组的优点
- 使用数组可以存储事物的列表,而不会引起混淆的引号语义。相反,不使用数组会导致试图在字符串中嵌套引号。
- 数组可以安全地存储任意字符串的序列/列表,包括包含空格的字符串。
数组的缺点
使用数组可能会增加脚本的复杂性。
数组的决策
应使用数组来安全地创建和传递列表。特别是在构建一组命令参数时,请使用数组来避免混淆的引号问题。使用带引号的扩展 – "${array[@]}"
– 来访问数组。但是,如果需要更高级的数据操作,则应完全避免使用Shell脚本;请参见上面。
使用管道到While
首选使用进程替代或 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
# 这将始终输出 '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)
# 这将输出 your_command 的最后一个非空行
echo "${last_line}"
或者,使用 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 退出。
# 用作文本的简单计算 - 注意在字符串内使用 $(( … ))。
echo "$(( 2 + 2 )) is 4"
# 在进行算术比较测试时
if (( a < b )); then
…
fi
# 将一些计算结果赋给变量。
(( i = 10 * j + 400 ))
# 这种形式不可移植且已过时
i=$[2 * 10]
# 尽管外观上看,'let' 不是声明性关键字之一,
# 但未引用的赋值会受到单词分割的影响。
# 为了简化起见,避免使用 'let',改用 (( … ))
let i="2 + 2"
# expr 实用程序是一个外部程序而不是 shell 内置命令。
i=$( expr 4 + 4 )
# 使用 expr 时引号可能会导致错误。
i=$( expr 4 '*' 4 )
尽管在风格上有考虑因素,但是 shell 的内置算术要比 expr
快得多。
在使用变量时,不需要在 $(( … ))
中使用 ${var}
(和 $var
)形式。Shell 会自动为您查找 var
,省略 ${…}
可以使代码更加简洁。尽管这与之前关于始终使用花括号的规则稍有不同,但这仅仅是一些建议。
# 注意:在可能的情况下,请记得将变量声明为整数,并优先使用局部变量而不是全局变量。
local -i hundred=$(( 10 * 10 ))
declare -i five=$(( 10 / 2 ))
# 将变量 "i" 增加三个。
# 注意:
# - 我们不写 ${i} 或 $i。
# - 我们在 (( 前后加上空格。
(( i += 3 ))
# 要将变量 "i" 减少五个:
(( i -= 5 ))
# 进行一些复杂的计算。
# 请注意,正常的算术运算符优先级会被遵循。
hr=2
min=5
sec=30
echo $(( hr * 3600 + min * 60 + sec )) # 预期输出 7530
命名约定
函数名
小写,用下划线分隔单词。使用 ::
分隔库。函数名后必须加上括号。function
关键字是可选的,但在整个项目中必须保持一致使用。
如果你只是写单个函数,使用小写并用下划线分隔单词。如果你在写一个包,用 ::
分隔包名。大括号必须与函数名在同一行(与 Google 的其他语言一样),函数名与括号之间不要留空格。
# 单个函数
my_func() {
…
}
# 包的一部分
mypackage::my_func() {
…
}
当函数名后跟有“()”时,function
关键字是多余的,但它有助于快速识别函数。
变量名
与函数名相同。
循环中的变量名应与要循环的任何变量具有相似的命名。
for zone in "${zones[@]}"; do
something_with "${zone}"
done
常量和环境变量名
全部大写,用下划线分隔,放在文件顶部声明。
常量和任何导出到环境的内容都应使用大写字母。
# 常量
readonly PATH_TO_FILES='/some/path'
# 同时是常量和环境变量
declare -xr ORACLE_SID='PROD'
有些内容在首次设置时变成常量(例如,通过 getopts)。因此,可以在 getopts 中或基于条件设置常量,但之后应立即将其设置为只读。出于清晰起见,建议使用 readonly
或 export
,而不是等效的 declare
命令。
VERBOSE='false'
while getopts 'v' flag; do
case "${flag}" in
v) VERBOSE='true' ;;
esac
done
readonly VERBOSE
源文件名
小写,如果需要,用下划线分隔单词。
为了与 Google 的其他代码风格保持一致,使用小写的文件名,并用下划线分隔单词,如 maketemplate
或 make_template
,而不是 make-template
。
只读变量
使用 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
使用局部变量
使用 local
声明函数特定的变量。声明和赋值应位于不同的行。
在声明局部变量时使用 local
,以确保局部变量只能在函数及其子函数中访问。这样可以避免污染全局命名空间,并无意中设置可能在函数外部具有意义的变量。
当赋值值由命令替换提供时,声明和赋值必须分开成两个语句,因为 local
内置命令不会传播命令替换的退出代码。
my_func2() {
local name="$1"
# 分开的声明和赋值行:
local my_var
my_var="$(my_func)"
(( $? == 0 )) || return
…
}
my_func2() {
# 不要这样做:
# $? 总是为零,因为它包含 'local' 的退出代码,而不是 my_func 的退出代码
local my_var="$(my_func)"
(( $? == 0 )) || return
…
}
函数位置
将所有函数放在文件的常量下面。不要在函数之间隐藏可执行代码。这样做会使代码难以跟踪,并在调试时产生令人讨厌的意外。
如果你有多个函数,将它们全部放在文件顶部附近。只有包含、set
语句和设置常量可以在声明函数之前执行。
main
对于至少包含一个其他函数的较长脚本,需要一个名为 main
的函数。
为了方便找
到程序的起始点,将主要代码放在一个名为 main
的函数中,作为最底部的函数。这与代码库的其余部分保持一致,同时还允许您将更多变量定义为 local
(如果主要代码不是函数,则无法实现此功能)。文件中的最后一行非注释行应调用 main
:
main "$@"
当然,在只有线性流程的短脚本中,使用 main
是不必要的。
调用命令
检查返回值
始终检查返回值并给出有信息的返回值。
对于未使用管道的命令,使用 $?
或通过 if
语句直接检查,以保持简单。
示例:
if ! mv "${file_list[@]}" "${dest_dir}/"; then
echo "无法将 ${file_list[*]} 移动到 ${dest_dir}" >&2
exit 1
fi
# 或者
mv "${file_list[@]}" "${dest_dir}/"
if (( $? != 0 )); then
echo "无法将 ${file_list[*]} 移动到 ${dest_dir}" >&2
exit 1
fi
Bash 还有 PIPESTATUS
变量,允许检查管道的所有部分的返回代码。如果仅需要检查整个管道的成功或失败,那么以下内容是可以接受的:
tar -cf - ./* | ( cd "${dir}" && tar -xf - )
if (( PIPESTATUS[0] != 0 || PIPESTATUS[1] != 0 )); then
echo "无法将文件打包到 ${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
内置命令 vs. 外部命令
在选择调用内置命令和调用单独的进程之间,选择内置命令。
我们更喜欢使用内置命令,例如 bash(1)
中的 参数扩展 函数,因为它更健壮和可移植(特别是与像 sed
这样的东西相比)。
示例:
# 更喜欢这种写法:
addition=$(( X + Y ))
substitution="${string/#foo/bar}"
# 而不是这种写法:
addition="$(expr "${X}" + "${Y}")"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
结论
理性思考并保持一致性。
请花几分钟阅读 C++ Guide 底部的 Parting Words 部分。
修订版本 2.02
最新评论
5211314
能不能教我 一点不会