简单介绍下 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
还有,在同时使用 find
和 xargs
命令时,应该使用 -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 的一篇文章,虽说是参考,大部分都是翻译,建议初学者好好看看这篇文章,避免一些常见的错误。