每一个问题都是机会

Google 团队 Shell 编码风格指南-中文版

文章目录
  1. 目录
  2. 背景
  3. Shell 文件和解释器调用
  4. 环境
  5. 注释
  6. 格式化
  7. 特性和错误
  8. 文件名的通配符扩展
  9. 命名约定
  10. 调用命令
  11. 结论

样式指南

Shell 编码风格指南

修订版本 2.02

由许多谷歌员工创作、修订和维护。

注:翻译内容可能因语言特性而略有调整,但力求保持准确、流畅和地道。

目录

章节 内容
背景 选择使用哪种 Shell何时使用 Shell
Shell 文件和解释器调用 文件扩展名SUID/SGID
环境 标准输出 vs 标准错误
注释 文件头部注释函数注释实现注释待办注释
格式化 缩进行长度和长字符串管道循环Case 语句变量扩展引号
特性和错误 ShellCheck命令替换测试,[… \][[… ]]测试字符串通配符扩展文件名Eval数组管道到 While算术
命名约定 函数名变量名常量和环境变量名源文件名只读变量使用局部变量函数位置main 函数
调用命令 检查返回值内建命令 vs 外部命令
结论

背景

选择使用哪种 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 <<END
我是一个异常长的
字符串。
END

# 也可以使用内嵌换行符
long_string="我是一个异常
长的字符串。"

管道

如果管道不全都适合一行,则将其拆分为每行一个。

如果整个管道适合一行,则应在一行上。

如果不适合,则应在每行一个管道分段上拆分,换行处放置管道,并在下一个管道段的开头缩进2个空格。这适用于使用 | 组合的命令链,以及使用 ||&& 的逻辑组合。

# 适合一行
command1 | command2

# 长命令
command1 \
  | command2 \
  | command3 \
  | command4

循环

; do; then 放在与 whileforif 同一行。

在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个空格缩进选项。
  • 一行选项需要在模式的右括号之后和 ;; 之前加一个空格。
  • 长或多命令选项应该拆分为多行,模式、操作和 ;; 放在单独的行上。

匹配表达式从 caseesac 缩进一个级别。多行操作再缩进一个级别。通常情况下,不需要引用匹配表达式。模式表达式不应该在开括号之前。避免使用 ;&;;& 符号。

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

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 中或基于条件设置常量,但之后应立即将其设置为只读。出于清晰起见,建议使用 readonlyexport,而不是等效的 declare 命令。

VERBOSE='false'
while getopts 'v' flag; do
  case "${flag}" in
    v) VERBOSE='true' ;;
  esac
done
readonly VERBOSE

源文件名

小写,如果需要,用下划线分隔单词。

为了与 Google 的其他代码风格保持一致,使用小写的文件名,并用下划线分隔单词,如 maketemplatemake_template,而不是 make-template

只读变量

使用 readonlydeclare -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

赞(0)
未经允许不得转载:技术好学屋 » Google 团队 Shell 编码风格指南-中文版
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址