原帖:http://zhangbin.cc/2011/04/23/shell-exit-on-error/
最近写了一个 shell 脚本, 里面有类似这样的语句:
cd $SOME_PLACE
mv * $ANOTHER_PLACE
有一次运行的时候, $SOME_PLACE
没有创建成功, 因此 cd 失败, 但 mv 仍然被执行了, 结果就是所有脚本文件都被移动到了不该去的地方. ls 的结果空荡荡的, 吓出一身冷汗. 之后就想, 要是某些关键的语句没有执行成功的话能记个日志自动终止脚本的执行就好了. 于是很自然的就写了这样一个函数:
function _do()
{
$@ || ( alert "exec failed: $@"; exit -1; )
}
_do statement # alert and exit when failed
alert 是另一个函数, 作用就是记录日志并发送报警短信把熟睡的程序员叫醒. 在脚本的关键语句前面加上 _do, 就能在出错时报警并终止整个脚本执行.
这个函数实现乍一看貌似没什么问题, 其实埋下了新的陷阱.
陷阱一: statement 出错时, 报警了, 可脚本没有像预期那样退出. 原因出在括号上:
(list)
list is executed in a subshell environment
因此, 括号里的 exit 只是退出了 subshell 而已, 对上层 shell (脚本) 没有影响. 换成大括号就可以了:
{ list; }
list is simply executed in the current shell environment.
陷阱二: 若 statement 中的参数包含带空格的字符串, 例如 _do cat "a b" ("a b" 是一个已存在的文件), 那么实际的行为会变成 cat a b. 解决方案: 把函数中的 $@ 用双引号包起来.
@
: Expands to the positional parameters, starting from one. When the expansion occurs within double quotes, each parameter expands to a separate word. That is, "$@
" is equivalent to "$1
" "$2
" ...
于是函数变成了这个样子:
function _do()
{
"$@" || { alert "exec failed: ""$@"; exit -1; }
}
陷阱三: statement 中包含管道操作的话, 如果写成 _do statementA | statementB
或者 statementA | _do statementB
都只对后面的一个 statement 有效, 且出错时不能终止脚本的执行, 原因是管道的两端都在各自的 subshell 中, exit 仅影响 subshell. 而 _do ( statementA | statementB )
或者_do { statementA | statementB }
都会报语法错误. 无奈, 只好又写了一个函数, 单独处理带管道的情形:
function _do_ex()
{
eval "$1" || { alert "exec failed: ""$1"; exit -1; }
}
_do_ex "statementA | statementB"
PS:
其实一般情况下, 只要在脚本中打开 set -e, 就能在遇到错误时终止脚本的执行. 但这样一来不便于选择性的进行错误控制 (脚本中部分语句允许执行失败), 也不方便在出错时加上自己的处理逻辑 (日志, 报警).
另外, 报警时可以加上出错的行号, 方法可参考 solrex 的这篇博客.
对于篇幅不长的脚本, 也可以使用 cd $SOME_PLACE && mv * $ANOTHER_PLACE || exit -1
类似的方法在出错时终止脚本的执行.