linuxshell语句出错⾃动退出调试检查脚本
许多⼈⽤shell脚本完成⼀些简单任务,⽽且变成了他们⽣命的⼀部分。不幸的是,shell脚本在运⾏异常时会受到⾮常⼤的影响。在写脚本时将这类问题最⼩化是⼗分必要的。本⽂中我将介绍⼀些让bash脚本变得健壮的技术。
使⽤set -u
你因为没有对变量初始化⽽使脚本崩溃过多少次?对于我来说,很多次。
chroot=$1
...
rm -rf $chroot/usr/share/doc
如果上⾯的代码你没有给参数就运⾏,你不会仅仅删除掉chroot中的⽂档,⽽是将系统的所有⽂档都删除。那你应该做些什么呢?好在bash提供了set -u,当你使⽤未初始化的变量时,让bash⾃动退出。你也可以使⽤可读性更强⼀点的set -o nounset。
#bash /tmp/shrink-chroot.sh
$chroot=
#bash -u /tmp/shrink-chroot.sh
/tmp/shrink-chroot.sh: line 3: $1: unbound variable
#
使⽤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" ];
使⽤$@变量时,你也需要使⽤引号,因为空格隔开的两个参数会被解释成两个独⽴的部分。
# foo() { for i in $@; do echo $i; done }; foo bar "baz quux"
bar
baz
quux
# foo() { for i in "$@"; do echo $i; done }; foo bar "baz quux"
bar
baz quux
我没有想到任何不能使⽤"$@"的时候,所以当你有疑问的时候,使⽤引号就没有错误。
如果你同时使⽤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提供了⼀种⽅法,当bash接收到⼀个UNIX信号时,运⾏⼀个 命令或者⼀个函数。可以使⽤trap命令。
trap command signal [signal ...]
你可以链接多个信号(列表可以使⽤kill -l获得),但是为了清理残局,我们只使⽤其中的三个:INT,TERM和EXIT。你可以使⽤-as来让traps恢复到初始状态。
信号描述
INT Interrupt - 当有⼈使⽤Ctrl-C终⽌脚本时被触发
TERM Terminate - 当有⼈使⽤kill杀死脚本进程时被触发shell最简单脚本
EXIT 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
现在当你杀死进程时,锁⽂件⼀同被删除。注意在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
当磁盘空间不⾜或者进程中途被杀死,这个脚本就会出现问题。在这种情况下,你也许希望⽤户账户不存在,⽽且他的⽂件也应该被删除。
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将会被调⽤,那么脚本等于什么都没做。
保持原⼦化
⼜是你需要⼀次更新⽬录中的⼀⼤堆⽂件,⽐如你需要将URL重写到另⼀个⽹站的域名。你也许会写:
for file in $(find /var/www -type f -name "*.html"); do
perl -pi -e 'ample/' $file
done
如果修改到⼀半是脚本出现问题,⼀部分使⽤ample,⽽另⼀部分使⽤ample。你可以使⽤备份和trap解决,但在升级过程中你的⽹站URL是不⼀致的。
解决⽅法是将这个改变做成⼀个原⼦操作。先对数据做⼀个副本,在副本中更新URL,再⽤副本替换掉现在⼯作的版本。你需要确认副本和⼯作版本⽬录在同⼀个磁盘分区上,这样你就可以利⽤Linux系统的优势,它移动⽬录仅仅是更新⽬录指向的inode节点。
cp -a /var/www /var/www-tmp
for file in $(find /var/www-tmp -type -f -name "*.html"); do
perl -pi -e 'ample/' $file
done
mv /var/www /var/www-old
mv /var/www-tmp /var/www

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。