Comment on BASH error handling — Do 22 August 2024

你好,世界!

命令行解释器 shell 是 UNIX 系统一个非常重要的理念和工具。GNU's Not UNIX,所以大多数 GNU 系统都带有至少一个 shell 命令行解释器,其中最著名的是 GNU BASH,简称 BASH 或 bash。

bash 是如此流行,如此好用,所以无论是资深黑客和系统管理员,还是普通用户和编程新手,都会经常打开命令行输入一段 bash 代码来执行一些任务。他们有时也会编写 bash 脚本用来更灵活地执行各种经常操作的任务。然而,由于 bash 对运行错误的处理并不那么直接明了,它也时常为使用者带来调试困难。其实,如果有良好的编程风格和统一的错误处理,我们还是能够提早发现很多 bash 脚本中的错误并及时中断执行中的脚本。

自由软件基金会的技术团队有一个关于 bash 的 编程风格建议,里面针对没有函数定义的 bash 脚本给出了一个错误处理的建议代码。我们就来详细分析一下这段简短的错误处理代码:

if ! test "$BASH_VERSION"; then echo "error: shell is not bash" >&2; exit 1; fi
shopt -s inherit_errexit 2>/dev/null ||: # ignore fail in bash < 4.4
set -eE -o pipefail
trap 'echo "$0:$LINENO:error: \"$BASH_COMMAND\" returned $?" >&2' ERR

使用说明:

这段代码的使用非常简单。如果你有一个 bash 脚本,并且脚本里没有函数定义,那么你可以直接把上述代码复制到你的脚本最前面。如果你的脚本运行时出现错误,那么脚本会在错误发生时中止,并打印出相关的错误信息以供调试分析。

如果你是一个 bash 初学者,虽然可以使用这段代码,但是也希望能够对代码有比较好的理解。那么我们就一起来看一下。

第一行——if

从 if 开始到 fi 结束是一个完整的 bash 的条件语句,其格式如下:

if test-commands; then
  consequent-commands;
[elif more-test-commands; then
  more-consequents;]
[else alternate-consequents;]
fi

if 按顺执行判断条件。如果一个 test-commands; 的值为零(表示逻辑真),则相应的 consequent-commands; 会执行,并结束条件语句。

!

! 表示对表达式结果取反,其格式如下:

! expression

如果表达式 expression 的结果是零,那么上述表达式的结果就是非零(表示逻辑假)。

test

test 是 bash 的一个内置函数,其格式如下:

test expression

test 根据 expression 的结果返回零或非零。如果 expression 是一个长度为零的字符串,那么 test 返回值为非零(假)。

"$BASH_VERSION"

这是 bash 设置的一个变量,它返回当前 bash 的版本号,通常是一个字符串。如果该变量没有设置,那么这是一个长度为零的空字符串。

echo

echo 是 bash 的一个内置函数,其格式如下:

echo string

echo 执行的结果就是在终端输出字符串 string。

>&2

">&" 是重定向操作符,">&2" 把前一个命令(此处是 echo)的标准输出(1)和标准错误输出(2)都定向到标准错误输出(2),即都默认输出到终端。

exit

exit 是 bash 的一个内置函数,其格式如下:

exit [number]

number 是 exit 的返回值。如果 number 是零,表示执行成功;如果是非零,则执行失败。

综合上述,这个 if 语句的意思就是:如果环境变量 $BASH_VERSION 未定义(为空),即当前命令行解释器不是 bash,则在终端输出错误信息:

error: shell is not bash

然后,退出脚本,返回值为1(失败)。

第二行——shopt

shopt

shopt 是 bash 的又一个内置函数,其格式如下:

shopt [-pqsu] [-o] [optname …]

其后续参数 "-s inherit_errexit" 设置确保子命令行进程会继承父命令行进程的 errexit 选项,即子命令行对

exit [number]

的 number 为非零值时的处理方式和父命令行一致。

2>/dev/null

这是把 shopt 命令的标准错误输出(2)丢弃(/dev/null 相当于垃圾桶),不在终端显示。

||

是命令行的逻辑或,其格式如下:

command1 || command2

表示只有在 command1 返回非零值时,command2 才执行。

:

":" 也是 bash 的内置命令,它实际什么都不做,它的返回值为零。

"#" 后面都是 bash 的注释。

第三行——set

set 是 bash 的又一个内置函数,它可以用来设置 bash 的各种可选项。"-eE -o pipefail" 是常见的把 bash 设置为一旦出现错误(比如某个命令返回非零)就停止执行下面的命令。这能保护脚本不在错误发生后继续执行。

第四行——trap

trap 'echo "$0:$LINENO:error: "$BASH_COMMAND" returned $?" >&2' ERR

trap

trap 是 bash 的一个内置函数,它的格式如下:

trap [-lp] [arg] [sigspec …]

第四行中的 arg 是:

'echo "$0:$LINENO:error: \"$BASH_COMMAND\" returned $?" >&2'

而 sigspec 是 ERR。它们放在一起的意思是一旦脚本中有命令、管道等返回非零值,那么 arg 就会执行。我们来看看 arg 究竟做些什么。

'echo "$0:$LINENO:error: "$BASH_COMMAND" returned $?" >&2'

首先,两个单引号之间的是一条完整的 bash 命令,它使用 echo 把双引号之间的命令输出到标准错误通道(2),即终端显示。

其次,$0 会翻译成 bash 执行时出错的命令或脚本名称。$LINENO 会翻译成出错时的源代码文件的行号。

再次," 表示不解释双引号。$BASH_COMMAND 翻译成出错正在执行的命令名称。

最后,$? 会翻译成最后命令或管道的返回值,即错误状态。

所以,整个第四行的输出结果就是

脚本名称:行号:error: 命令名称 return 数字

把这段 bash 代码放在你的脚本前面就会起到脚本执行发生返回状态错误时,脚本会中止执行,并且你会在终端看到比较具体的错误信息。建议你在你每个 bash 脚本里都加上这一段。

最后,需要指出 GNU BASH 是自由软件,你可以下载它的源代码来学习、修改,并分享你的心得。

如果你对 GNU BASH 还有问题,立伯乐或许可以帮你。

用 GNU BASH 会让你在开发中游刃有余、灵活自由!