让bash脚本变得健壮的技术。
1、使用set -u
chroot=$1
... rm -rf $chroot/usr/share/doc
如果以上代码没有给参数就运行,不会仅仅删除掉chroot中的文档,而是将系统的所有文档都删除。
也可以使用可读性更强一点的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% 2、使用set -e
每一个脚本的开始都应该包含set -e。这告诉bash一但有任何一个语句返回非真的值,则退出bash。
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值的命令,或对返回值并不感兴趣呢?
set +e
command1 command2 set -e 相关文档指出,bash默认返回管道中最后一个命令的值,也许是你不想要的那个。比如执行 false | true 将会被认为命令成功执行。如果你想让这样的命令被认为是执行失败,可以使用 set -o pipefail
3、程序防御 - 考虑意料之外的事
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
4、设置的陷阱 bash提供了一种方法,当bash接收到一个UNIX信号时,运行一个命令或者一个函数。可以使用trap命令。
trap command signal [signal ...]
5、信号描述
当使用锁文件时:
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后面的命令。
6、竟态条件 (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
当磁盘空间不足或者进程中途被杀死,这个脚本就会出现问题。
rollback() {
del_from_passwd $user if [ -e /home/$user ]; then rm -rf /home/$user fi exit } trap rollback INT TERM EXIT add_to_passwd $user cp -a /etc/skel /home/$user chown $user /home/$user -R trap - INT TERM EXIT 在脚本最后需要使用trap关闭rollback调用,否则当脚本正常退出时rollback将会被调用,那么脚本等于什么都没做。
7、保持原子化
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是不一致的。
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而不用真正的复制所有的数据。
对于apache服务器来说这不是问题,因为它每次都重新打开文件。 做点:有了一个先前的备份,当你需要还原时,它就派上用场了。 (责任编辑:IT) |