Bash 使用常见错误以及规避措施

2022-01-04 bash language

简单介绍下 Linux 中 Bash 编程所需要注意的内容,以及常见的规避措施。

防止未初始化

对于如下场景,通常会导致预想不到的错误。

chroot=$may_not_exist
rm -rf $chroot/usr

那么上面的脚本可能会导致 /usr 目录被删除。

可以在脚本中使用 set -u 或者 set -o nounset,或者在命令行中使用 bash -u your.sh

执行错误退出

通常对于一些命令的返回值,可以通过如下的方式检查返回值,如果有异常则退出。

#----- 示例1
command
if [ "$?"-ne 0]; then echo "command failed"; exit 1; fi
#----- 示例2
command || { echo "command failed"; exit 1; }
#----- 示例3
if ! command; then echo "command failed"; exit 1; fi

可以通过 set -e 或者 set -o errexit 明确告知 Bash ,一但有任何一个语句返回非真的值则退出,避免错误滚雪球般的变成严重错误。

如果必须使用返回非 0 值的命令,或者对返回值不敏感,那么可以使用 command || true;如果有一段很长的代码,可以暂时关闭错误检查功能,当然需要谨慎使用。

set +e
# some commands
set -e

另外,需要注意管道的使用,Bash 默认返回管道中最后一个命令的值,例如 false | true 将会被认为命令执行成功,如果想让这样的命令被认为是执行失败,可以使用 set -o pipefail 命令进行设置。

设置陷阱

当程序异常退出时,通常需要处理一些处于中间状态的变量,例如锁文件、临时文件等等。

Bash 提供了一种方法,当收到一个 UNIX 信号时,可以运行一个命令或者一个函数;此时就需要使用 trap 命令。

trap command signal [signal ...]

所有的信号量可以通过 kill -l 查看,通常使用较多的是三个:INT、TERM 和 EXIT。

INT   使用Ctrl-C终止脚本时被触发
TERM  使用kill杀死脚本进程时被触发
EXIT  伪信号,当脚本正常退出或者set -e后因为出错而退出时被触发

锁文件

当你使用锁文件时,可以这样写:

if [ ! -e $lockfile ]; then
    touch $lockfile
    critical-section
    rm $lockfile
else
    echo "critical-section is already running"
fi

如果 critical-section 在运行时被杀死了,那么锁文件仍然存在,但是在删除之前该脚本就无法运行了;此时就需要通过如下的方式进行设置。

if [ ! -e $lockfile ]; then
    trap " rm -f $lockfile; exit" INT TERM EXIT
    touch $lockfile
    critical-section
    rm $lockfile
    trap - INT TERM EXIT
else
    echo "critical-section is already running"
fi

这样,即使在 critical-section 运行时被杀死,锁文件会一同被删除。

其它

通常不同的命令在执行时,不同的参数可能会有不同的行为,例如 mkdir -p 可以防止父目录不存在时报错;rm -f 可以防止文件不存在时报错等等。

入参校验

对于所有的入参都需要经过参数校验,同时需要检测入参是否为空,例如 SQL 语句的拼装,对于一些命令也可以用设置黑名单的方式,示例如下:

if [[ "x${name}" == "x" || ! "${name}" =~ ^[A-Za-z][A-Za-z0-9_]*$ ]]; then
	return 1
fi

禁用调试选项

Bash 可以通过命令行 bash -x foobar.sh 或者在脚本中设置 set -x 打开调试模式,虽然方便调试,但是可能会导致敏感信息的泄露。

空格处理

一定要注意处理好文件中可能出现空格的场景,也就是要用引号包围变量,示例如下:

#----- 变量中有空格时会导致异常
if [ $filename = "foo" ];
#----- 正常应该使用如下的方式
if [ "$filename" = "foo" ];

另外,使用命令行参数时同样需要注意,例如 $@ 变量,因为空格隔开的两个参数会被解释成两个独立的部分。

$ foo() { for i in $@; do echo $i; done }; foo bar "hello world"
bar
hello
world
$ foo() { for i in "$@"; do echo $i; done }; foo bar "hello world"
bar
hello world

还有,在同时使用 findxargs 命令时,应该使用 -print0 来让字符分割文件名,而不是通过换行符分割。

$ touch "foo bar"
$ find | xargs ls
ls: ./foo: No such file or directory
ls: bar: No such file or directory
$ find -print0 | xargs -0 ls
./foo bar

参考

这是参考 Bash Pitfalls 的一篇文章,虽说是参考,大部分都是翻译,建议初学者好好看看这篇文章,避免一些常见的错误。