何时使用set -e

34

我有些时间前接触到set -e,现在回来写一些bash脚本。

我的问题是,在什么时候使用set -e和何时不使用它(例如在小型/大型脚本中等),或者是否应该使用像cmd || exit 1这样的模式来跟踪错误的一些最佳实践?


请参阅Server Fault上的“What does 'set -e' do, and why might it be considered dangerous?”(“set -e是什么,为什么它被认为是危险的?”)尤其是Debian bug(Debian漏洞)在我看来相当棘手。 - Kontrollfreak
4个回答

78

是的,你应该始终使用它。人们经常嘲笑Visual Basic,称其不是真正的编程语言,部分原因是因为其"On Error Resume Next"语句。然而,在shell中这是默认设置!set -e 应该是默认设置。潜在的灾难风险太高了。


在那些允许命令失败的地方,你可以使用|| true或其简写形式||:,例如:

grep Warning build.log ||:

实际上,你应该再进一步,并且


set -eu
set -o pipefail

在每个 bash 脚本的顶部。

-u 会使得引用不存在的环境变量(比如${HSOTNAME})成为错误,但需要在引用${1}${2}等之前使用${#}进行一些操作。

pipefail 会使得类似于 misspeled-command | sed -e 's/^WARNING: //' 的命令引发错误。


这里的每个人都有自己的偏好。你的答案最接近我的想法,所以我接受了它。 - dimba

5
如果您的脚本代码能够仔细地检查错误并在必要时以适当的方式处理它们,那么您可能永远不需要或者不想使用set -e
另一方面,如果您的脚本是一系列顺序运行的简单命令,并且如果您希望脚本在其中任何一个失败时终止,则将set -e放在顶部就是使您的脚本简单和清晰的方法。一个完美的例子是,如果您正在创建一个编译源文件的脚本,并且希望在遇到第一个出现错误的文件后停止编译。
更复杂的脚本可以结合这些方法,因为您可以使用set +e将其效果关闭,并返回显式的错误检查。
请注意,虽然set -e应该在任何未经测试的命令失败时导致shell退出,但当您的代码进行自己的错误处理时最好将其关闭,因为可能会出现意外情况,即一个命令将返回一个您没有预料到的非零退出状态,甚至可能在测试中无法捕获这种情况,而突然的致命终止将使您的脚本处于糟糕的状态。因此,除非您确实知道需要它,否则不要使用set -e或者在短暂使用后将其保持打开状态。
请注意,即使在使用set -e的情况下仍然可以定义错误处理程序trap ERR,以在发生错误条件时执行某些操作,因为该处理程序仍将在退出shell之前运行。

5
我不同意。我认为你应该默认使用 'set -e',只有在确实需要时才删除它。最后你想看到的是未处理的错误引发一系列更多的错误或脚本出现某种不正确的行为。 - Mike Weller
@MikeWeller 很高兴听到其他的意见 :) - dimba
2
阅读和编写用于生产的脚本超过25年的经验表明,set -e 很少有用。 - Greg A. Woods
1
@MikeWeller - 我必须同意Greg的观点。如果你在编写shell脚本时有忽略重要故障的风险,那么也许你应该雇用顾问或承包商来帮助编写它们。例如Greg。 - ghoti
抱歉@TapasweniPathak,我错过了你的问题。一个“未经测试”的命令是指shell自己不使用退出代码来决定短路列表控制运算符(&&||)的结果,或在流程控制语句(ifwhile)的条件项中。 - Greg A. Woods
显示剩余3条评论

4

您喜欢这个吗?

对我而言,我更喜欢在我的.bashrc中添加以下行:

trap '/usr/games/fortune /usr/share/games/fortunes/bofh-excuses' ERR

(在Debian系统中安装BOFH借口的方法:apt-get install fortunes-bofh-excuses :-)

但这只是我的个人偏好;-)

更加严肃些:

lastErr() {
    local RC=$?
    history 1 |
         sed '
  s/^ *[0-9]\+ *\(\(["'\'']\)\([^\2]*\)\2\|\([^"'\'' ]*\)\) */cmd: \"\3\4\", args: \"/;
  s/$/", rc: '"$RC/"
}
trap "lastErr" ERR

Gna
bash: Gna : command not found
cmd: "Gna", args: "", rc: 127

Gna gna
cmd: "Gna", args: "gna", rc: 127

"Gna gna" foo
cmd: "Gna gna", args: "foo", rc: 127

好的,从那里开始,你可以做以下操作:
trap "lastErr >>/tmp/myerrors" ERR
"Gna gna" foo

cat /tmp/myerrors 
cmd: "Gna gna", args: "foo", rc: 1

更好的做法是:
lastErr() {
local RC=$?
history 1 |
     sed '
  s/^ *[0-9]\+ *\(\(["'\'']\)\([^\2]*\)\2\|\([^"'\'' ]*\)\) */cmd: \"\3\4\", args: \"/;
  s/$/", rc: '"$RC/
  s/^/$(date +"%a %d %b %T ")/"
}
"Gna gna" foo

cat /tmp/myerrors 
cmd: "Gna gna", args: "foo", rc: 1
Tue 20 Nov 18:29:18 cmd: "Gna gna", args: "foo", rc: 127

您甚至可以添加其他信息,如$$、$PPID、$PWD或者您的其他环境变量。


注意使用/tmp!这可能会成为安全漏洞!也许使用另一个地方,如$HOME/.bash_errors将是更合适的选择! - F. Hauri - Give Up GitHub

0

当此选项打开时,如果简单命令由于 Shell 错误的任何原因之一而失败或返回一个大于 0 的退出状态值,并且不是 while、until 或 if 关键字后面的复合列表的一部分,也不是 AND 或 OR 列表的一部分,也不是由 ! 保留字前导的管道,则 shell 将立即退出。


谢谢,但我的问题是什么时候推荐使用它?例如,我没有看到它在init脚本(/etc/init.d/*)中使用。 - dimba
3
在创建init.d脚本时使用"set -e"可能会很危险:在编写init.d脚本时,请注意不要使用set -e。因为当守护进程在不中止init.d脚本的情况下运行或停止时,需要相应的退出状态。所以只有在您不关心程序发生错误时是否中止时才使用set,否则不要使用。 - Saddam Abu Ghaida

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接