谷歌Shell编程指南,整洁代码风格,代码规范整理参考大全

目录

Shell 风格指南

1.目录

2.背景

2.1 使用哪种 Shell

2.2 何时使用 Shell

3.Shell 文件和解释器调用

3.1 文件扩展名

3.2 SUID/SGID

4.环境

4.1 STDOUT vs STDERR

5.注释

5.1 文件头

5.2 函数注释

5.3 代码注释

5.4 TODO 注释

6.格式化

6.1 缩进

6.2 行长和长字符串

6.3 管道

6.4 流程控制

6.5 Case 语句

6.6 变量扩展

6.7 引用

7.特性和 Bug

7.1 ShellCheck

7.2 命令替换

7.3 Test, [ … ], 和 [[ … ]]

7.4 判断字符串

7.5 文件名的通配符扩展

7.6 Eval

7.7 数组

7.8 管道到 While

7.9 算术

7.10 别名

8. 命名约定

8.1 函数名

8.2 变量名

8.3 常量、环境变量名

8.4 源文件名

8.5 只读变量

8.6 使用局部变量

8.7 函数位置

8.8 main函数

9.调用命令

9.1 检查返回值

9.2 内置命令 vs. 外部命令

10.结束语


Shell 风格指南

styleguide | Style guides for Google-originated open-source projects

由众多 Google 员工撰写、修订和维护。

1.目录

章节内容
背景使用哪种 Shell - 何时使用 Shell
Shell 文件和解释器调用文件扩展名 - SUID/SGID
环境STDOUT vs STDERR
注释文件头 - 函数注释 - 实现注释 - TODO 注释
格式化缩进 - 行长和长字符串 - 管道 - 控制流 - Case 语句 - 变量扩展 - 引用
特性和 BugShellCheck - 命令替换 - Test, [… ], 和 [[… ]] - 测试字符串 - 文件名的通配符扩展 - Eval - 数组 - 管道到 While - 算术 - 别名
命名约定函数名 - 变量名 - 常量和环境变量名 - 源文件名 - 使用局部变量 - 函数位置 - main
调用命令检查返回值 - 内置命令 vs 外部命令
结束语

2.背景

2.1 使用哪种 Shell

Bash 是唯一允许用于可执行文件的 Shell 脚本语言。

可执行文件必须以 #!/bin/bash 开头,并使用最少的标志。使用 set 来设置 Shell 选项,以便在调用脚本时不会破坏其功能。

将所有可执行 Shell 脚本限制为 Bash 为我们提供了一种在所有机器上安装的一致 Shell 语言。特别是,这意味着通常不需要追求 POSIX 兼容性或避免“Bash 特性”。

唯一的例外是,当你所编写的代码被迫使用其他 Shell 时。例如,某些遗留操作系统或受限的执行环境可能要求某些脚本使用纯 Bourne Shell。

2.2 何时使用 Shell

Shell 应仅用于小型实用程序或简单的包装脚本。

虽然 Shell 脚本不是一种开发语言,但它在 Google 中用于编写各种实用程序脚本。本风格指南更多的是对其使用的认可,而不是建议将其用于广泛部署。

一些指导原则:

  • 如果你主要调用其他实用程序并且进行相对较少的数据操作,Shell 是该任务的可接受选择。

  • 如果性能很重要,请使用 Shell 以外的其他语言。

  • 如果你编写的脚本超过 100 行,或者使用了非直接的控制流逻辑,你应该立即将其重写为更结构化的语言。请记住,脚本会增长。尽早重写脚本,以避免日后更耗时的重写。

  • 在评估代码的复杂性时(例如决定是否切换语言),请考虑代码是否容易被作者以外的人维护。

3.Shell 文件和解释器调用

3.1 文件扩展名

可执行文件应具有 .sh 扩展名或没有扩展名。

  • 如果可执行文件将有一个重命名源文件的构建规则,则优先使用 .sh 扩展名。这使你能够使用推荐的命名约定,源文件如 foo.sh,构建规则名为 foo

  • 如果可执行文件将直接添加到用户的 PATH 中,则优先不使用扩展名。执行程序时不需要知道它使用哪种语言编写,Shell 也不要求扩展名,因此我们更倾向于不对直接由用户调用的可执行文件使用扩展名。同时,考虑是否部署构建规则的输出比直接部署源文件更可取。

  • 如果上述情况均不适用,则两种选择均可接受。

库必须具有 .sh 扩展名,并且不应是可执行的。

3.2 SUID/SGID

Shell 脚本中禁止使用 SUID 和 SGID。

Shell 存在太多安全问题,几乎不可能足够安全地允许 SUID/SGID。虽然 Bash 使得运行 SUID 变得困难,但在某些平台上仍然可能,这就是为什么我们明确禁止它。

如果需要提升访问权限,请使用 sudo

4.环境

4.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

5.注释

5.1 文件头

每个文件的开头应包含其内容的描述。

每个文件必须有一个顶级注释,简要概述其内容。版权声明和作者信息是可选的。

示例:

 #!/bin/bash
 #
 #******************************************************************************************
 #Author:                QianSong
 #QQ:                    xxxxxxxxxx
 #Date:                  2022-09-13
 #FileName:              handshakes.sh
 #URL:                   https://github.com
 #Description:           The handshake wifi cap info script
 #Copyright (C):         QianSong 2022 All rights reserved
 #******************************************************************************************
 

5.2 函数注释

任何不既明显又简短的函数都必须有函数头注释。库中的任何函数无论长度或复杂性如何,都必须有函数头注释。

其他人应该能够通过阅读注释(以及自述文件,如果提供)来学习如何使用你的程序或库中的函数,而无需阅读代码。

所有函数头注释都应使用以下内容描述预期的 API 行为:

  • 函数的功能描述。

  • 全局变量:使用和修改的全局变量列表。

  • 参数:接受的位置参数或详细描述需要传入的参数类型。

  • 输出:输出到 STDOUT 或 STDERR。

  • 返回值:除最后一个命令运行的默认退出状态之外的返回值。

示例:

 #######################################
 # 清理备份目录中的文件。
 # Globals:
 #   BACKUP_DIR
 #   ORACLE_SID
 # Arguments:
 #   none
 #######################################
 function cleanup() {
   …
 }
 ​
 #######################################
 # 获取配置目录。
 # Globals:
 #   SOMEDIR
 # Arguments:
 #   none
 # Outputs:
 #   将位置写入 stdout
 #######################################
 function get_dir() {
   echo "${SOMEDIR}"
 }
 ​
 #######################################
 # 以复杂的方式删除文件。
 # Arguments:
 #   $1: 要删除的文件,路径。
 # Returns:
 #   如果删除成功则为 0,错误时为非零。
 #######################################
 function del_thing() {
   rm "$1"
 }
 

5.3 代码注释

注释代码中复杂、不明显、有趣或重要的部分。

这遵循 Google 的一般编码注释实践。不要注释所有内容。如果有复杂的算法或你在做一些不寻常的事情,请在代码中放置简短的注释。

5.4 TODO 注释

使用 TODO 注释来标记临时代码、短期解决方案或足够好但不完美的代码。

这与 C++ 指南中的约定一致。

TODO 应全部大写,后跟具有最佳上下文的人的名字、电子邮件地址或其他标识符。主要目的是有一个一致的 TODO,可以在需要时搜索以获取更多详细信息。TODO 不是被引用的人将解决问题的承诺。因此,当你创建 TODO 时,几乎总是使用你的名字。

示例:

 # TODO(mrmonkey): 处理不太可能的边缘情况(bug ####)
 

6.格式化

虽然你应该遵循正在修改的文件的现有风格,但以下要求适用于任何新代码。

6.1 缩进

缩进 2 个空格。不要使用 tab 制表符。

使用空行分隔代码块以提高可读性。缩进为两个空格。无论你做什么,都不要使用 tab 制表符。对于现有文件,请忠实于现有的缩进。

例外:唯一使用 tab 制表符的例外是用于 <<- 制表符缩进的 here-document 文档的正文。

6.2 行长和长字符串

最大行长为 80 个字符。

如果你必须编写超过 80 个字符的字面字符串,应尽可能使用 here 文档或嵌入换行符。

超过 80 个字符且无法合理拆分的单词是可以的,但尽可能将这些项目放在单独的行上,或将其分解为变量。示例包括文件路径和 URL,特别是在字符串匹配(如 grep)对维护有价值时。

 # 使用 'here document's
 cat <<END
 我是一个非常长的
 字符串。
 END
 ​
 # 嵌入换行符也可以
 long_string="我是一个非常
 长的字符串。"
 ​
 long_file="/i/am/an/exceptionally/loooooooooooooooooooooooooooooooooooooooooooooooooooong_file"
 ​
 long_string_with_long_file="我在这里包含了一个非常 \
 /very/long/file\
  在这个长字符串中。"
 ​
 # 将长文件转换为较短的变量名,并使用更清晰的换行。
 long_string_alt="我在这里包含了一个非常 ${long_file} 在这个长\
  字符串"
 # 仅仅因为一行包含一个异常并不意味着该行的其余部分不应该像通常一样换行。
 ​
 bad_long_string_with_long_file="我在这里包含了一个非常 /very/long/file 在这个长字符串中。"
 

6.3 管道

如果管道不能全部放在一行上,则应每行拆分一个管道。

如果管道全部放在一行上,则应放在一行上。

如果没有,则应在每个管道段的新行上拆分,管道放在新行上,下一部分管道缩进 2 个空格。\ 应一致用于指示行继续。这适用于使用管道 | 组合的命令链,也适用于使用 ||&& 的逻辑组合。

 # 全部放在一行上
 command1 | command2
 ​
 # 长命令
 command1 \
   | command2 \
   | command3 \
   | command4

这有助于在区分管道与常规长命令延续时提高可读性,特别是在该行同时使用两者时。

注释需要放在整个管道之前。如果注释和管道很大且复杂,则值得考虑通过使用辅助函数将它们的低级细节移开。

6.4 流程控制

; then; do 放在与 ifforwhile 相同的行上。

Shell 中的控制流语句有些不同,但我们遵循与声明函数时使用大括号相同的原则。即:; then; do 应与 if/for/while/until/select 在同一行上。else 应在其自己的行上,并且关闭语句(fidone)应在其自己的行上,与打开语句垂直对齐。

示例:

 # 如果在函数内,请记住将循环变量声明为局部变量,
 # 以避免泄漏到全局环境中:
 local dir
 for dir in "${dirs_to_cleanup[@]}"; do
   if [[ -d "${dir}/${SESSION_ID}" ]]; then
     log_date "清理 ${dir}/${SESSION_ID} 中的旧文件"
     rm "${dir}/${SESSION_ID}/"* || error_message
   else
     mkdir -p "${dir}/${SESSION_ID}" || error_message
   fi
 done

尽管可以省略 for 循环中的 in "$@",但我们建议始终包含它以保持清晰。

 for arg in "$@"; do
   echo "参数: ${arg}"
 done
 

6.5 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

6.6 变量扩展

按优先顺序:保持与现有代码一致;引用你的变量;优先使用 "${var}" 而不是 "$var"

这些是强烈推荐的指南,但不是强制性的规定。尽管如此,这并不意味着应该轻视或贬低、淡化它。

它们按优先顺序列出。

  1. 保持与现有代码一致。

  2. 引用变量,参见下面的引用部分。

  3. 不要对单个字符的 Shell 特殊字符/位置参数使用大括号 ${} 分隔,除非绝对必要或避免深度混淆。

  4. 优先对所有其他变量使用大括号 ${} 分隔。

这种是推荐的。

# 推荐的情况示例。

# 首选风格用于 '特殊' 变量:
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 "file=${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} 中使用大括号不是一种引用形式。必须同时使用“双引号”。

6.7 引用

  1. 始终引用包含变量、命令替换、空格或 Shell 元字符的字符串,除非需要仔细的不需要引用的扩展或它是 Shell 内部的整数(参见下一点)。

  2. 使用数组来安全地引用元素列表,特别是命令行标志。参见下面的数组部分。

  3. 可选择性地引用 Shell 内部的、只读的特殊变量,这些变量被定义为整数:$?, $#, $$, $!。为了保持一致,优先引用“命名”的内部整数变量,例如 PPID 等。

  4. 优先引用“单词”字符串如:user_name="TOM"(与命令选项或路径名相对)。

  5. 可以选择性的引用数字整数如:wifi_number="15"

  6. 当变量的值定义为正则表达式时,切勿加双引号引用该变量

  7. 当变量的值定义为一组数字,字符串,且要将其用于循环条件时,切勿加双引号引用该变量

  8. 当变量拼接时,特别是拼接文件/目录路径字符串时,通配符号扩展 * 不要加双引号引用,而是必须放在双引号外边

  9. 当变量的值定义为一组数字,字符串,且要将其赋值给一个数组,则双引号包裹时将视为整体赋值,数组元素长度1个;没有双引号包裹时将视为单个单个的元素赋值,数组元素长度与字符串一致

  10. 当命令替换表达式用于循环条件时如:for i in $(seq -w 10); do,切勿加双引号包裹引用该命令替换表达式

  11. 注意 [[ … ]] 中模式匹配的引用规则。参见下面的 Test, [ … ], 和 [[ … ]] 部分。

  12. 除非你有特定理由使用 $*,例如简单地将参数附加到消息或日志中的字符串,否则使用 "$@"

# '单引号' 表示不会发生替换,所见即所得。
# "双引号" 表示需要/允许替换,识别变量然后替换为该变量的值。

# 简单示例

# "引用命令替换"
# 注意嵌套在 "$()" 中的引号不需要转义。
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 'Hello stranger, and well met. Earn lots of $$$'
echo "Process $$: Done making \$\$\$."

# "命令选项或路径名"
# (假设 $1 包含一个值)
grep -li Hugo /dev/null "$1"

# 不太简单的示例
# "引用变量,除非证明为假":ccs 可能为空
git send-email --to "${reviewers}" ${ccs:+"--cc" "${ccs}"}

# 位置参数预防:$1 可能未设置
# 单引号保持正则表达式不变。
grep -cP '([Ss]pecial|\|?characters*)$' ${1:+"$1"}

# 为了传递参数,
# "$@" 几乎总是正确的,
# $* 几乎总是错误的:
#
# * $* 和 $@ 将在空格上拆分,破坏包含空格的参数并丢弃空字符串;
# * "$@" 将保留参数不变,因此不提供参数将导致不传递任何参数;
#   在大多数情况下,这是你想要用于传递参数的方式。
# * "$*" 扩展为一个参数,所有参数由(通常)空格连接,
#   因此不提供参数将导致传递一个空字符串。
#
# 参考
# https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html
# 和
# https://mywiki.wooledge.org/BashGuide/Arrays
# 了解更多

(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$*"; echo "$#, $@")
(set -- 1 "2 two" "3 three tres"; echo $#; set -- "$@"; echo "$#, $@")

7.特性和 Bug

7.1 ShellCheck

ShellCheck 项目识别 Shell 脚本中的常见错误和警告。建议所有脚本,无论大小,都使用它。

7.2 命令替换

使用 $(command) 而不是反引号``来进行命令替换。

嵌套反引号需要 \ 转义内部的反引号。而使用 $(command) 格式在嵌套时不会改变,并且更易于阅读。

示例:

# 这是首选的:
var="$(command "$(command1)")"
# 这不是:
var="`command \`command1\``"

7.3 Test, [ … ], 和 [[ … ]]

优先使用 [[ … ]] 而不是 [ … ]test/usr/bin/[

[[ … ]] 减少了错误,因为在 [[]] 之间不会发生路径名扩展或单词拆分。此外,[[ … ]] 允许模式匹配和正则表达式匹配,然而 [ … ] 则不支持。

# 这确保左侧的字符串由 alnum 字符类中的字符组成,后跟字符串 name。
# 注意 RHS 不应该在这里引用。
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
  echo "Match"
fi

# 这与精确模式 "f*" 匹配(在此情况下不匹配)
if [[ "filename" == "f*" ]]; then
  echo "Match"
fi
# 这会引发“参数过多”错误,因为 f* 被扩展为当前目录的内容。
# 也可能触发“意外操作符”错误,因为 `[` 不支持 `==`,只支持 `=`。
if [ "filename" == f* ]; then
  echo "Match"
fi

有关详细信息,请参阅 Bash FAQ 中的 E14:http://tiswww.case.edu/php/chet/bash/FAQ

7.4 判断字符串

尽可能使用引号而不是填充字符。

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

7.5 文件名的通配符扩展

在进行文件名的通配符扩展时,请使用显式路径。

由于文件名可以以 - 开头,使用 ./* 而不是 * 扩展通配符要安全得多。

# 假如这是目录的内容:
# -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'

7.6 Eval

应避免使用 eval

eval 在用于变量赋值时会混淆输入,并且可以在不检查这些变量是什么的情况下设置变量。

# 这设置了什么?
# 它成功了吗?部分还是全部?
eval $(set_my_variables)

# 如果返回值之一包含空格会发生什么?
variable="$(eval some_function)"

7.7 数组

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 脚本;参见上文

7.8 管道到 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 内置命令将文件读入数组,然后遍历数组的内容。注意,出于与上述相同的原因,你需要使用进程替换与 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 通常更安全和清晰。

7.9 算术

始终使用 (( … ))$(( … )),而不是 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}(和 $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 符合预期

7.10 别名

尽管在 .bashrc 文件中常见,但在脚本中应避免使用别名。正如 Bash 手册所述:

对于几乎所有目的,Shell 函数都优于别名。

别名使用起来很麻烦,因为它们需要仔细引用和转义其内容,并且错误可能难以察觉。

# 这在定义别名时对 $RANDOM 求值一次,
# 因此每次调用时 echo 的字符串都相同。
alias random_name="echo some_prefix_${RANDOM}"

函数提供了别名功能的超集,应始终优先使用。

random_name() {
  echo "some_prefix_${RANDOM}"
}

# 请注意,与别名不同,函数的参数通过 $@ 访问。
fancy_ls() {
  ls -lh "$@"
}

8. 命名约定

8.1 函数名

小写,用下划线分隔单词。使用 :: 分隔库。函数名后必须跟括号 () 。关键字 function 是可选的,但必须在整个项目中一致使用。

如果你编写单个函数,请使用小写并用下划线分隔单词。如果你编写包,请使用 :: 分隔包名。然而,用于交互式使用的函数可能会选择避免使用冒号,因为它可能会混淆 bash 自动补全。

大括号必须与函数名在同一行(与 Google 的其他语言一样),函数名和括号 () 之间不能有空格。

# 单个函数
my_func() {
  …
}

# 包的一部分
mypackage::my_func() {
  …
}

当函数名后跟 () 时,function 关键字是多余的,但它有助于快速识别函数。

8.2 变量名

与函数名相同。

循环变量名应与任何你正在遍历的变量类似命名。

for zone in "${zones[@]}"; do
  something_with "${zone}"
done

8.3 常量、环境变量名

常量和任何导出到环境的变量应大写,用下划线分隔,并在文件顶部声明。

# 常量
readonly PATH_TO_FILES='/some/path'

# 既是常量又导出到环境
declare -xr ORACLE_SID='PROD'

为了清晰起见,推荐使用 readonlyexport,而不是等效的 declare 命令。你可以像这样依次使用:

# 常量
readonly PATH_TO_FILES='/some/path'
export PATH_TO_FILES

在运行时或条件中设置常量是可以的,但应立即使其成为 readonly

ZIP_VERSION="$(dpkg --status zip | sed -n 's/^Version: //p')"
if [[ -z "${ZIP_VERSION}" ]]; then
  ZIP_VERSION="$(pacman -Q --info zip | sed -n 's/^Version *: //p')"
fi
if [[ -z "${ZIP_VERSION}" ]]; then
  handle_error_and_quit
fi
readonly ZIP_VERSION

8.4 源文件名

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

这是为了与 Google 的其他代码风格保持一致:maketemplatemake_template,但不要使用 make-template

8.5 只读变量

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

8.6 使用局部变量

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

  …
}

8.7 函数位置

将所有函数放在文件中的常量下方。不要在函数之间隐藏、插入可执行代码。这样做会使代码难以跟踪,并在调试时导致不愉快的意外。

如果你有函数,请将它们全部放在文件的顶部。在声明函数之前,只能包含 includeset 语句和设置常量。

8.8 main函数

对于包含至少一个其他函数的脚本,需要一个名为 main 的函数。

为了轻松找到程序的起点,将主程序放在一个名为 main 的函数中,作为文件中最底部的函数。这提供了与代码库其余部分的一致性,并允许你将更多变量定义为局部变量(如果主代码不是函数,则无法做到这一点)。文件的最后一行(非注释行)应是对 main 的调用:

main "$@"

显然,对于短脚本,如果它只是线性流程,main 是多余的,因此不需要。

例子:

╭─░▒▓    /tmp                                                      ✔  31m 19s   root@wifi-hack  12:01:33  ▓▒░
╰─ cat opt_test.sh
#!/bin/bash
#
#******************************************************************************************
#Author:                QianSong
#QQ:                    xxxxxxxxxx
#Date:                  2023-11-06
#FileName:              opt_test.sh
#URL:                   https://github.com
#Description:           The test script
#Copyright (C):         QianSong 2023 All rights reserved
#******************************************************************************************

#######################################
# 打印位置参数
# Globals:
#   none
# Arguments:
#   "$@"
# Outputs:
#   none
# Returns:
#   none
#######################################
function print_info() {

        echo "$@"
}

#######################################
# 准备执行循环,调用exec_loop()函数,传递参数为上级调用本函数传递的位置参数
# 实际上这里继承传递了脚本位置参数
# Globals:
#   none
# Arguments:
#   "$@"
# Outputs:
#   none
# Returns:
#   none
#######################################
function loop_script_opt() {

        exec_loop "$@"
}

#######################################
# 执行循环,循环打印位置参数
# 实际上这里继承了脚本位置参数,从而循环打印脚本位置参数
# Globals:
#   none
# Arguments:
#   "$@"
# Outputs:
#   none
# Returns:
#   none
#######################################
function exec_loop() {

        local i
        for i in "$@"; do
                echo "${i}"
                sleep 1
        done
}

#######################################
# 主函数,调用print_info()函数分别打印脚本位置参数、函数位置参数.
# 然后调用loop_script_opt()函数,传递参数为脚本位置参数
# Globals:
#   none
# Arguments:
#   "$@"
# Outputs:
#   none
# Returns:
#   none
#######################################
function main() {

        print_info "$@"
        echo -e "\033[36m----------\033[0m"
        print_info "this" "is" "function" "option" "paramater"
        echo -e "\033[36m----------\033[0m"
        loop_script_opt "$@"
}

main "$@"

#执行效果
╭─░▒▓    /tmp                                                                  ✔  root@wifi-hack  12:01:40  ▓▒░
╰─ bash opt_test.sh "这" "是" "脚" "本" "位" "置" "参" "数"
这 是 脚 本 位 置 参 数
----------
this is function option paramater
----------
这
是
脚
本
位
置
参
数

#当参数为有空格的字符串时,需要加引号包裹使其成为整体
#相应的,bash将其解释为一个包含空格的单个参数整体,循环次数变为1次
╭─░▒▓    /tmp                                                           ✔  8s   root@wifi-hack  12:03:46  ▓▒░
╰─ bash opt_test.sh "这 是 脚 本 位 置 参 数"
这 是 脚 本 位 置 参 数
----------
this is function option paramater
----------
这 是 脚 本 位 置 参 数

9.调用命令

9.1 检查返回值

始终检查返回值并提供信息丰富的返回值。

对于非管道命令,使用 $? 或直接通过 if 语句进行检查以保持简单。

示例:

if ! mv "${file_list[@]}" "${dest_dir}/"; then
  echo "Unable to move ${file_list[*]} to ${dest_dir}" >&2
  exit 1
fi

# 或者
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

9.2 内置命令 vs. 外部命令

在调用 Shell 内置命令和调用单独进程之间,优先选择内置命令。

我们更倾向于使用 Bash 提供的参数扩展功能等内置命令,因为它们更高效、健壮且可移植(特别是与 sed 等工具相比)。另请参阅 =~ 操作符

示例:

# 首选这个:
addition="$(( X + Y ))"
substitution="${string/#foo/bar}"
if [[ "${string}" =~ foo:(\d+) ]]; then
  extraction="${BASH_REMATCH[1]}"
fi
# 而不是这个:
addition="$(expr "${X}" + "${Y}")"
substitution="$(echo "${string}" | sed -e 's/^foo/bar/')"
extraction="$(echo "${string}" | sed -e 's/foo:\([0-9]\)/\1/')"

10.结束语

在整个代码库中一致地使用一种风格,让我们能够专注于其他(更重要的)问题。一致性还允许自动化。在许多情况下,归因于“保持一致”的规则可以归结为“只需选择一个,不要再担心它”;在这些问题上允许灵活性的潜在价值被人们争论它们的成本所抵消。

然而,一致性也有其局限性。当没有明确的技术论点或长期方向时,它是一个很好的决胜局。一致性通常不应被用作在不考虑新风格的好处或代码库随着时间的推移趋向于新风格的情况下以旧风格做事的理由。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值