错误处理

本文前面部分内容摘录自阮一峰老师的:Bash 脚本 set 命令教程,主要是文章写得太好了。

开启错误处理

使用 shell 中的错误处理有助于我们发现错误,更好的调试代码。

检测未定义变量

首先,set -u 可以在遇到未定义变量时抛出错误,而不是忽略它。比如:

echo $bar

这里的变量 bar 没有定义,shell 的默认方案是忽略掉它。这就可能带来隐藏的问题,所以通过 set -u 选项来强制报错:

set -u
echo $bar

此时会得到报错 ./test.sh: line 2: bar: unbound variable

报错时退出

如果某个命令执行错了,可能会导致后续一系列命令执行出错。既不利于调试,也会导致很多意想不到的结果,所以可以用 set -e 选项来强制报错时退出执行脚本。

set -e
bbbb
ssss

如果不加上 set -e 会得到两行报错,因为 bbbbssss 都是不存在的指令。而加上以后,这里只会有一个报错就立刻 exit 了。

需要注意的是,如果我们用管道的写法,得到的返回值是最后一个命令的返回值,如果中间的命令出错,是不能被 set -e 捕获的,比如:

set -e

bs | ls
echo 'reach here'

得到的输出结果将是:

aaa.sh: line 3: bs: command not found
test.sh
reach here

可见 bs 这个指令虽然不存在,但程序还是没有退出,而是执行到了结尾。因此 set -e 通常需要配合 set -o pipfail 来使用,这样管道中的任何一个指令出错,都会导致程序退出。

调试执行

如果想知道每一行都执行了什么代码,可以用 set -x 选项,通常我们在 Jenkins 等工具里可以这么用,方便追查问题。比如:

set -x
ls

我们会得到:

+ ls
test.sh

以加号开头的行就是文件的原始内容了。

exit 钩子

总结一下第一段的内容,我们在任何 shell 脚本的开头都应该加上这行标记:

set -euo pipefail

表示遇到错误指令或未定义的变量时立刻退出。当然,如果需要调试,可以改成 set -euxo pipefail

我们知道退出是靠 exit 命令来实现的,也就是说上述错误最终都会调用到 exit 命令,有没有办法捕获这个退出呢?

最简单做法当然是封装 exit,比如:

function bs_exit() {
    echo "exit" && exit $1
}

但如果项目中已有大量的 exit,就需要我们手动替换。虽然成本能接受,但如果可以用 AOP 的方式来 hook exit 命令,肯定是最理想的。

这也是本文的重点,经过查阅资料,我们可以这样写:

function finish {
  err=$?
  if [[ $err == 1 ]]; then
    echo '1'
  fi
}

trap finish EXIT

这里的 trap 是一个内置命令,用来捕捉发送给程序的信号。它接受两个参数,第一个是处理信号的方式,第二个则是信号名。

比如当我们使用 exit 命令来退出脚本时,实际上是发送了 EXIT 信号,于是会被捕获,并调用 finish 函数。函数内部可以拿到 exit 后面的状态,因此可以区分用户是通过 exit 1 还是 exit 2 来退出的,方便执行对应的操作。

这种写法的另一个好处在于它是全局的,比如当我的 shell 脚本存在嵌套调用关系时,只要在入口处定义一次就好,它可以自动捕获 subshell 的退出状态。如果用之前 bs_exit 这种封装,就需要在所有脚本里面都把这个函数 source 进来,成本也更高。

代码调试

如果只想检查脚本的语法但不执行,可以用 sh -n 命令。如果你的脚本是一个有破坏性或者很耗时的操作,可以用这个技巧来调试语法。比如:

bash -n test.sh

此外,我们还可以增强 set -x 指令的效果,上文说过被执行的指令前面会有 + 的前缀,它其实是是通过一个叫做 PS4 的环境变量来控制的。我们可以修改这个变量:

export PS4='+{$LINENO:${FUNCNAME[0]}} '

这里会显示代码所在行数(LINENO)和当前函数名(FUNCNAME[0]),输出效果如下:

+{11:} trap finish EXIT
+{13:} fff
./test.sh: line 13: fff: command not found
+{13:} finish
+{2:finish} err=127
+{3:finish} [[ 127 == 1 ]]
+{6:finish} echo 127

这个 PS4 变量的修改还是很有用的,因此可以放到 .zshrc 里面去。

results matching ""

    No results matching ""