在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
shell脚本在运行异常时会受到非常大的影响。 本文介绍一些让bash脚本变得健壮的技术。 使用set -u 因为没有对变量初始化而使脚本崩溃过多少次?对于我来说,很多次。 也可以使用可读性更强一点的set -o nounset。 复制代码 代码如下: david% bash /tmp/shrink-chroot.sh $chroot= david% bash -u /tmp/shrink-chroot.sh /tmp/shrink-chroot.sh: line 3: $1: unbound variable david% 使用set -e 写的每一个脚本的开始都应该包含set -e。这告诉bash一但有任何一个语句返回非真的值,则退出bash。使用-e的好处是避免错误滚雪球般的变成严重错误,能尽早的捕获错误。更加可读的版本:set -o errexit 使用-e把从检查错误中解放出来。如果忘记了检查,bash会替做这件事。不过也没有办法使用$?来获取命令执行状态了,因为bash无法获得任何非0的返回值。可以使用另一种结构: command if [ "$?"-ne 0]; then echo "command failed"; exit 1; fi 可以替换成: command || { echo "command failed"; exit 1; } 或者使用: if ! command; then echo "command failed"; exit 1; fi 如果必须使用返回非0值的命令,或者对返回值并不感兴趣呢?可以使用 command || true ,或者有一段很长的代码,可以暂时关闭错误检查功能,不过我建议谨慎使用。 set +e command1 command2 set -e 相关文档指出,bash默认返回管道中最后一个命令的值,也许是不想要的那个。比如执行 false | true 将会被认为命令成功执行。如果想让这样的命令被认为是执行失败,可以使用 set -o pipefail 程序防御 - 考虑意料之外的事 的脚本也许会被放到“意外”的账户下运行,像缺少文件或者目录没有被创建等情况。可以做一些预防这些错误事情。比如,当创建一个目录后,如果父目录不存在,mkdir 命令会返回一个错误。如果创建目录时给mkdir命令加上-p选项,它会在创建需要的目录前,把需要的父目录创建出来。另一个例子是rm 命令。如果要删除一个不存在的文件,它会“吐槽”并且的脚本会停止工作。(因为使用了-e选项,对吧?)可以使用-f选项来解决这个问题,在文件不存在的时候让脚本继续工作。 准备好处理文件名中的空格 有些人从在文件名或者命令行参数中使用空格,需要在编写脚本时时刻记得这件事。需要时刻记得用引号包围变量。 if [ $filename = "foo" ]; 当$filename变量包含空格时就会挂掉。可以这样解决: if [ "$filename" = "foo" ]; 使用$@变量时,也需要使用引号,因为空格隔开的两个参数会被解释成两个独立的部分。 复制代码 代码如下: david% foo() { for i in $@; do echo $i; done }; foo bar "baz quux" bar baz quux david% foo() { for i in "$@"; do echo $i; done }; foo bar "baz quux" bar baz quux 我没有想到任何不能使用"$@"的时候,所以当有疑问的时候,使用引号就没有错误。 如果同时使用find和xargs,应该使用 -print0 来让字符分割文件名,而不是换行符分割。 复制代码 代码如下: david% touch "foo bar" david% find | xargs ls ls: ./foo: No such file or directory ls: bar: No such file or directory david% find -print0 | xargs -0 ls ./foo bar 设置的陷阱 当编写的脚本挂掉后,文件系统处于未知状态。比如锁文件状态、临时文件状态或者更新了一个文件后在更新下一个文件前挂掉。如果能解决这些问题,无论是 删除锁文件,又或者在脚本遇到问题时回滚到已知状态,都是非常棒的。幸运的是,bash提供了一种方法,当bash接收到一个UNIX信号时,运行一个 命令或者一个函数。可以使用trap命令。 trap command signal [signal ...] 可以链接多个信号(列表可以使用kill -l获得),但是为了清理残局,我们只使用其中的三个:INT,TERM和EXIT。可以使用-as来让traps恢复到初始状态。 信号描述 TERM EXIT 当使用锁文件时,可以这样写: 复制代码 代码如下: 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 现在当杀死进程时,锁文件一同被删除。注意在trap命令中明确地退出了脚本,否则脚本会继续执行trap后面的命令。 竟态条件 (wikipedia) 在上面锁文件的例子中,有一个竟态条件是不得不指出的,它存在于判断锁文件和创建锁文件之间。一个可行的解决方法是使用IO重定向和bash的noclobber(wikipedia)模式,重定向到不存在的文件。 可以这么做: 复制代码 代码如下: if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT critical-section rm -f "$lockfile" trap - INT TERM EXIT else echo "Failed to acquire lockfile: $lockfile" echo "held by $(cat $lockfile)" fi 更复杂一点儿的问题是要更新一大堆文件,当它们更新过程中出现问题时,是否能让脚本挂得更加优雅一些。想确认那些正确更新了,哪些根本没有变化。比如需要一个添加用户的脚本。 复制代码 代码如下: add_to_passwd $user cp -a /etc/skel /home/$user chown $user /home/$user -R 当磁盘空间不足或者进程中途被杀死,这个脚本就会出现问题。在这种情况下,也许希望用户账户不存在,而且他的文件也应该被删除。 复制代码 代码如下:
cp -a /etc/skel /home/$user trap - INT TERM EXIT 在脚本最后需要使用trap关闭rollback调用,否则当脚本正常退出的时候rollback将会被调用,那么脚本等于什么都没做。 保持原子化 又是需要一次更新目录中的一大堆文件,比如需要将URL重写到另一个网站的域名。 复制代码 代码如下: for file in $(find /var/www -type f -name "*.html"); do perl -pi -e 's/www.example.net/www.example.com/' $file done 如果修改到一半是脚本出现问题,一部分使用www.example.com,而另一部分使用www.example.net。可以使用备份和trap解决,但在升级过程中的网站URL是不一致的。 解决方法: 将这个改变做成一个原子操作。先对数据做一个副本,在副本中更新URL,再用副本替换掉现在工作的版本。 复制代码 代码如下: cp -a /var/www /var/www-tmp for file in $(find /var/www-tmp -type -f -name "*.html"); do perl -pi -e 's/www.example.net/www.example.com/' $file done mv /var/www /var/www-old mv /var/www-tmp /var/www 这意味着如果更新过程出问题,线上系统不会受影响。线上系统受影响的时间降低为两次mv操作的时间,这个时间非常短,因为文件系统仅更新inode而不用真正的复制所有的数据。 缺点: 需要两倍的磁盘空间,而且那些长时间打开文件的进程需要比较长的时间才能升级到新文件版本,建议更新完成后重新启动这些进程。 |
请发表评论