在Bash脚本中,set -e是什么意思?

1104

我正在学习这个preinst文件的内容,这个文件在从Debian归档(.deb)文件中解包这个软件包之前被脚本执行。

脚本有以下代码:

#!/bin/bash
set -e
# Automatically added by dh_installinit
if [ "$1" = install ]; then
   if [ -d /usr/share/MyApplicationName ]; then
     echo "MyApplicationName is just installed"
     return 1
   fi
   rm -Rf $HOME/.config/nautilus-actions/nautilus-actions.conf
   rm -Rf $HOME/.local/share/file-manager/actions/*
fi
# End automatically added section

我的第一个问题是关于这行代码:

set -e

我认为剩下的脚本很简单:它检查Debian/Ubuntu软件包管理器是否正在执行安装操作。如果是,则检查我的应用程序是否刚刚安装在系统上。如果是,则脚本会打印消息"MyApplicationName is just installed"并结束(return 1表示以一个“错误”结束,不是吗?)。

如果用户要求Debian/Ubuntu软件包系统安装我的软件包,脚本还将删除两个目录。

这样正确吗?或者我有遗漏什么吗?


61
set -e 是一个 Bash shell 命令,表示在脚本执行时,如果发生错误就停止执行。 - Anders Lindahl
86
你找不到在Google上的原因是:你查询中的“-e”被解释为否定。请尝试以下查询:bash set“-e”。 - Maleev
4
当我问自己同样的问题时,我正在查看 man set 的内容。 - Sedat Kilinc
11
如果您想要关闭它,可以将破折号替换为加号前缀:set +e - Tom Saleeba
2
@twalberg 但是询问真实的人比仅仅向机器发出请求更有趣;-)。 - vdegenne
显示剩余2条评论
10个回答

1118

help set 命令中:

  -e  Exit immediately if a command exits with a non-zero status.

但是一些人认为这是不好的做法(bash FAQ和irc freenode #bash FAQ的作者)。建议使用以下方法:

trap 'do_something' ERR

当出现错误时运行do_something函数。

参见http://mywiki.wooledge.org/BashFAQ/105


22
如果我想要与“如果命令以非零状态退出,则立即退出”的语义相同,那么do_something应该是什么? - CMCDragonkai
13
ERR 陷阱不会被 shell 函数所继承,因此如果你有函数,使用 set -o errtraceset -E 可以让你只需设置一次陷阱并全局应用。 - ykay
39
trap 'exit' ERRset -e 有任何不同吗? - Andy
31
如果这是一种不好的做法,那为什么它会被用在Debian软件包中?请参考链接:http://unix.stackexchange.com/q/325705/44425。 - phuclv
11
这并不被普遍认为是一种不好的习惯。就像许多不受欢迎的语言结构一样,它也有其适用场合。主要问题在于,在边缘情况下的行为有些难以理解。 - William Pursell
显示剩余8条评论

201

set -e 命令会在脚本中的命令或管道出现错误时停止执行,这与默认shell行为相反,后者会忽略脚本中的错误。在终端中键入 help set 可以查看此内置命令的文档。


72
只有当管道中的最后一个命令出错时才停止执行。Bash 有一个特定选项 set -o pipefail,可以用于传播错误,以便在前面的命令中有任何一个退出状态为非零时,管道命令的返回值也是非零。 - Anthony Geoghegan
6
请记住,-o pipefail 的意思仅是将管道中第一个非零(也就是在 -o errexit 中出现错误的命令)的退出状态传播到末尾。 即使启用了 set -o errexit,管道中剩余的命令仍然会运行。例如:echo success | cat - <(echo piping); echo continues,其中 echo success 表示成功但可失败的命令,将打印 successpipingcontinues,但 false | cat - <(echo piping); echo continues,其中的 false 代表现在静默错误的命令,仍将在退出之前打印 piping - bb010g

108

我在尝试查找由于set -e而中止的脚本的退出状态时发现了这篇文章。答案对我来说并不明显,因此有了这个答案。基本上,set -e会中止命令(例如shell脚本)的执行,并返回失败命令(即内部脚本而非外部脚本)的退出状态码。

例如,假设我有一个名为outer-test.sh的shell脚本:

#!/bin/sh
set -e
./inner-test.sh
exit 62;

inner-test.sh的代码如下:

#!/bin/sh
exit 26;
当我在命令行中运行outer-script.sh时,我的外部脚本将以内部脚本的退出代码终止:
$ ./outer-test.sh
$ echo $?
26

感谢您详细解释返回状态码的信息 :+1:。 - Eric McLachlan

73
根据bash - The Set Builtin手册,如果设置了-e/errexit,则如果由一个管道组成的简单命令列表复合命令返回非零状态,则shell立即退出。
默认情况下,管道的退出状态是管道中最后一个命令的退出状态,除非启用了pipefail选项(默认情况下禁用)。
如果启用,则管道的返回状态为最后(最右侧)一个以非零状态退出的命令的返回状态,如果所有命令都成功退出,则返回0。
如果您想在退出时执行某些操作,请尝试定义trap,例如:
trap onexit EXIT

其中onexit是您在退出时执行某些操作的函数,例如下面的示例打印简单的堆栈跟踪

onexit(){ while caller $((n++)); do :; done; }

有一个类似的选项-E/errtrace, 它会在 ERR 发生时触发陷阱,例如:

trap onerr ERR

示例

零状态示例:

$ true; echo $?
0

非零状态示例:

$ false; echo $?
1

否定状态示例:

$ ! false; echo $?
0
$ false || true; echo $?
0

测试禁用pipefail

$ bash -c 'set +o pipefail -e; true | true | true; echo success'; echo $?
success
0
$ bash -c 'set +o pipefail -e; false | false | true; echo success'; echo $?
success
0
$ bash -c 'set +o pipefail -e; true | true | false; echo success'; echo $?
1

启用 pipefail 进行测试:

$ bash -c 'set -o pipefail -e; true | false | true; echo success'; echo $?
1

31

这是一个旧问题,但是这里的回答都没有讨论在Debian软件包处理脚本中使用set -e或者set -o errexit。在这些脚本中,使用此选项是强制性的,根据Debian政策;其意图显然是为了避免任何未处理的错误情况。

实际上,这意味着您必须理解运行的命令在什么条件下可能返回错误,并明确处理每个错误。

常见的坑点是例如diff(当存在差异时返回错误)和grep(当没有匹配项时返回错误)。您可以通过明确处理来避免错误:

diff this that ||
  echo "$0: there was a difference" >&2
grep cat food ||
  echo "$0: no cat in the food" >&2

请注意,我们如何小心地在消息中包含当前脚本的名称,并将诊断消息写入标准错误而不是标准输出。

如果确实没有必要或有用的明确处理,请明确不执行任何操作:

diff this that || true
grep cat food || :

(使用shell的:空操作命令略微晦涩,但相当常见。)

再次强调一下:

something || other

是...的简写

if something; then
    : nothing
else
    other
fi

即我们明确表示只有当something失败时,才会明确指定运行other。使用长格式的if语句(以及其他像whileuntil之类的shell流控制语句)也是处理错误的有效方法(事实上,如果不这样做,带有set -e的shell脚本将无法包含流控制语句!)。

而且,为了明确起见,在没有此类处理程序的情况下,set -e会在diff发现差异或grep未找到匹配项时立即导致整个脚本失败并出现错误。

另一方面,某些命令在您希望它们出现错误退出状态时并不会产生该状态。常见的问题命令是find(退出状态不反映是否实际上找到了文件)和sed(退出状态不会显示脚本是否收到任何输入或成功执行任何命令)。在某些情况下,一个简单的防护措施是将其管道传输到会在没有输出时报错的命令:

find things | grep .
sed -e 's/o/me/' stuff | grep ^

需要注意的是,管道的退出状态是该管道中最后一个命令的退出状态。因此,上述命令实际上完全掩盖了findsed的状态,并且只告诉您grep是否最终成功。

(当然,Bash有set -o pipefail;但Debian软件包脚本不能使用Bash功能。政策坚定地规定这些脚本使用POSIX的sh,尽管这并不总是如此。)

在许多情况下,在编写防御性代码时需要特别注意这一点。有时,您需要通过临时文件来查看产生该输出的命令是否已成功完成,即使成语和便利性会指导您使用shell管道。


5
这是一个很好的答案,它提倡最佳实践。我遇到了和使用GREP命令时完全相同的问题,我真的不想删除"set -e"。 - soMuchToLearnAndShare
当然,使用“$?”来测试命令是否成功并不是一个好的做法,这在使用set -e时根本行不通;但是,已经有很多其他原因要避免这种反模式。 - tripleee

28

set -e 选项指示 bash 如果任何命令 [1] 出现非零退出状态,则立即退出。你不会希望在命令行 shell 中设置这个选项,但在脚本中它非常有用。在所有广泛使用的通用编程语言中,一个未处理的运行时错误-无论是 Java 中的抛出异常,还是 C 中的段错误,或者是 Python 中的语法错误-都会立即停止程序的执行;后续行将不再执行。

  • 默认情况下,bash 不会这样做。如果你在命令行上使用 bash,则这是你想要的默认行为
  • 你不希望因为打字错误而被登出!但是在脚本中,你真的希望相反。
  • 如果脚本中的一行失败了,但最后一行成功了,整个脚本将具有成功的退出代码。这使得很容易错过错误。
  • 同样,当你在命令行 shell 中使用 bash,并在脚本中使用它时,你所需要的功能是互相矛盾的。在脚本中对错误不容忍要好得多,这正是 set -e 所给予你的。

摘自:https://gist.github.com/mohanpedala/1e2ff5661761d3abd0385e8223e16425

这可能会对你有所帮助。


2
如果您在命令行上使用bash,则此默认行为正是您想要的。更明确地说,如果您在运行bash命令时使用set -e,那么一个简单的打字错误可能会导致您的bash会话立即退出。尝试运行新终端,set -e,然后运行lsd。再见终端。 - jcollum

26

我认为所说的脚本意图是快速失败。

要亲自测试,请在bash提示符下键入set -e。现在尝试运行ls。您将获得一个目录列表。现在,键入lsd。该命令未被识别,将返回错误代码,因此您的bash提示符将关闭(由于set -e)。

现在,为了理解这个在“脚本”上下文中的含义,请使用以下简单脚本:

#!/bin/bash 
# set -e

lsd 

ls

如果您按原样运行它,将从最后一行的ls中获得目录列表。 如果您取消注释set -e并再次运行,则不会看到目录列表,因为一旦遇到lsd的错误,Bash就会停止处理。


这个回答是否提供了其他回答中没有给出的见解或信息? - Charles Duffy
13
我认为它提供了一个清晰简洁的解释,这个功能在其他答案中并不存在。没有其他额外的内容,只比其他答案更加专注。 - Kallin Nagelberg
@CharlesDuffy 我认为它确实如此。它比仅仅说“看手册页”要有用得多。 - KansaiRobot
另一个答案并不只是说“看手册”,它提取了与问题相关且重要的手册部分。没有具体说明(以及要求读者自己进行研究)使得“看手册”变得无益。 - Charles Duffy
我也认为CharlesDuffy提供的答案很有帮助。 - jcollum

13
脚本1:不设置-e
#!/bin/bash
decho "hi"
echo "hello"

这将在decho中引发错误,并使程序继续到下一行。
脚本2:使用设置-e
#!/bin/bash
set -e
decho "hi" 
echo "hello"

在执行decho "hi"之前,shell会处理并退出程序,不会继续执行。


6

如果一条命令失败,它会停止脚本的执行。

一个值得注意的例外是 if 语句。例如:

set -e
false
echo never executed

set -e
if false; then
  echo never executed
fi

echo executed

false

echo never executed

这似乎只是之前许多答案的又一次重复。 - undefined

2
cat a.sh
#! /bin/bash

#going forward report subshell or command exit value if errors
#set -e
(cat b.txt)
echo "hi"

./a.sh; echo $?
cat: b.txt: No such file or directory
hi
0

如果注释掉 set -e,我们可以看到报告了 echo "hi" 的退出状态并打印了 hi。

cat a.sh
#! /bin/bash

#going forward report subshell or command exit value if errors
set -e
(cat b.txt)
echo "hi"

./a.sh; echo $?
cat: b.txt: No such file or directory
1

现在我们看到报告了b.txt错误,没有打印出hi。因此,shell脚本的默认行为是忽略命令错误并继续处理,并报告最后一个命令的退出状态。如果您想在出现错误时退出并报告其状态,我们可以使用-e选项。

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