Bash 脚本中的“set -e”是什么意思?
- 2024-09-30 14:02:00
- admin 原创
- 135
问题描述:
我正在研究该文件的内容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 刚刚安装”并结束(return 1
这意味着脚本以错误退出,不是吗?)。
如果用户要求 Debian/Ubuntu 包系统安装我的包,该脚本还会删除两个目录。
这是正确的吗或者我遗漏了什么?
解决方案 1:
Fromhelp set
和Bash 参考文档:内置 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
发生错误时运行函数。
请参阅为什么 set -e(或 set -o errexit 或 trap ERR)没有按照我的预期执行?
解决方案 2:
set -e
如果命令或管道出现错误,则停止执行脚本 - 这与默认的 shell 行为相反,默认行为是忽略脚本中的错误。help set
在终端中输入以查看此内置命令的文档。
解决方案 3:
我在尝试弄清楚由于 而中止的脚本的退出状态时发现了这篇文章set -e
。答案对我来说似乎并不明显;因此有了这个答案。基本上,set -e
中止命令(例如 shell 脚本)的执行并返回失败命令的退出状态代码(即内部脚本,而不是外部脚本)。
例如,假设我有以下 shell 脚本outer-test.sh
:
#!/bin/sh
set -e
./inner-test.sh
exit 62;
代码为inner-test.sh
:
#!/bin/sh
exit 26;
当我outer-script.sh
从命令行运行时,我的外部脚本以内部脚本的退出代码终止:
$ ./outer-test.sh
$ echo $?
26
解决方案 4:
按照bash - Set Builtin手册,如果设置了-e
/ ,则当由单个简单命令、列表或复合命令组成的管道返回非零状态errexit
时,shell 将立即退出。
默认情况下,管道的退出状态是管道中最后一个命令的退出状态,除非pipefail
启用该选项(默认情况下是禁用的)。
如果是,则管道的最后一个(最右边)命令的返回状态将以非零状态退出,如果所有命令都成功退出,则返回零。
如果您想在退出时执行某些操作,请尝试定义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
解决方案 5:
这里的其他答案都没有讨论在 Debian 包处理脚本中使用set -e
aka的问题。根据 Debian 政策set -o errexit
,强烈建议在这些脚本中使用此选项;目的显然是为了避免出现任何未处理的错误情况。
实际上,这意味着您必须了解在什么条件下运行的命令可能返回错误,并明确处理每个错误。
常见的陷阱有,例如,diff
(当存在差异时返回错误)和grep
(当不匹配时返回错误)。您可以通过显式处理来避免错误:
diff this that ||
echo "$0: there was a difference" >&2
grep cat food ||
echo "$0: no cat in the food" >&2
(还请注意我们如何在消息中包含当前脚本的名称,以及将诊断消息写入标准错误而不是标准输出。)
也许还注意到算术评估可以产生这种效果。
((variable++))
variable
如果为零则会失败。
如果确实不需要或没有有用的显式处理,则明确不执行任何操作:
diff this that || true
grep cat food || :
(shell 的 no-op 命令的用法:
有点晦涩,但相当常见。)
再次重申,
something || other
是简写
if something; then
: nothing
else
other
fi
即,我们明确表示other
当且仅当失败时才应运行something
。普通写法if
(以及其他 shell 流控制语句,如while
、until
)也是处理错误的有效方法(事实上,如果不是这样,shell 脚本set -e
就永远不会包含流控制语句!)
而且,明确地说,如果没有这样的处理程序,如果发现差异或者没有找到匹配项,set -e
就会导致整个脚本立即失败并出现错误。diff
`grep`
另一方面,有些命令不会在您希望它们产生错误退出状态时产生错误退出状态。常见的问题命令是find
(退出状态不反映是否实际找到文件)和sed
(退出状态不会显示脚本是否收到任何输入或实际成功执行任何命令)。在某些情况下,一个简单的保护是将管道连接到一个命令,如果没有输出,该命令会发出尖叫声:
find things | grep .
sed -n 's/o/me/p' stuff | grep ^
find
需要注意的是,管道的退出状态是该管道中最后一个命令的退出状态。因此上述命令实际上完全屏蔽了和 的状态sed
,只告诉你grep
最终是否成功。
(Bash 当然有set -o pipefail
,如果管道中的任何命令失败,它就会抛出错误;但是 Debian 包脚本不能使用 Bash 功能。该政策明确规定这些脚本必须使用 POSIX sh
,尽管情况并非总是如此。)
在许多情况下,这是防御性编码时需要单独注意的事情。有时您必须查看临时文件,以便查看产生该输出的命令是否成功完成,即使习惯用法和便利性会指导您使用 shell 管道。
解决方案 6:
set -e set -e选项指示 Bash 如果任何命令1 具有非零退出状态则立即退出。您不会想为命令行 shell 设置此项,但在脚本中它非常有用。
在所有广泛使用的通用编程语言中,未处理的运行时错误 - 无论是Java中抛出的异常,还是C中的段错误,还是Python中的语法错误- 都会立即停止程序的执行;后续行将不再执行。
默认情况下,Bash 不会这样做。如果您在命令行上使用 Bash,此默认行为正是您想要的
您不希望因为输入错误而导致退出!但在脚本中,您真正想要的是相反的结果。
如果脚本中有一行失败,但最后一行成功,则整个脚本的退出代码为成功。这使得很容易忽略错误。
再次强调,使用 Bash 作为命令行 shell 和在脚本中使用它时,您的需求是不同的。在脚本中,对错误的容忍度要高得多,这就是
set -e
您所得到的。
复制自:Bash 严格模式
解决方案 7:
我相信其目的是让所讨论的脚本快速失败。
要亲自测试,只需set -e
在 Bash 提示符下键入。现在,尝试运行ls
。您将获得一个目录列表。现在,键入lsd
。该命令无法识别并将返回错误代码,因此您的 Bash 提示符将关闭(由于set -e
)。
现在,要在“脚本”上下文中理解这一点,请使用这个简单的脚本:
#!/bin/bash
# set -e
lsd
ls
如果按原样运行,您将ls
在最后一行获得来自 的目录列表。如果取消注释set -e
并再次运行,您将看不到目录列表,因为一旦遇到来自 的错误,bash 就会停止处理lsd
。
解决方案 8:
脚本1:无需设置-e
#!/bin/bash
decho "hi"
echo "hello"
这将在该行引发错误decho
,并且程序继续执行下一行。
脚本 2:使用设置-e
#!/bin/bash
set -e
decho "hi"
echo "hello"
直到decho "hi"
,shell 才会处理,然后程序退出。它不会再继续执行。
解决方案 9:
如果命令失败,它会停止脚本的执行。
一个值得注意的例外是if
声明。例如:
set -e
false
echo never executed
set -e
if false; then
echo never executed
fi
echo executed
false
echo never executed
解决方案 10:
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 选项。
解决方案 11:
set -e
意味着脚本的读者在脱离上下文查看代码片段时无法知道脚本将做什么。 (这有点讽刺,但却切中要害。)errexit
多年来我一直反对使用,最近在将 shell 片段积累为模板的环境中做了大量工作。 errexit
在此环境中默认启用,但这在很大程度上无关紧要,因为任何先前的代码片段都可能禁用 errexit,并且收集模板的顺序并不为人所知,并且可能是不确定的。但请考虑一个简单的情况,您在没有上下文的情况下查看 shell 脚本中的几行:
$ sed -n 3,6p sample-script
echo this will be printed
set -e # enable errexit again just to be sure it's on
false
echo but errexit is enabled so this will not be printed
由于 errexit 已启用,因此读者可以合理地预期“但 errexit 已启用,因此不会打印”这一行实际上不会出现。但是,请考虑脚本的实际执行情况:
$ ./sample-script
this will be printed
but errexit is enabled so this will not be printed
没有任何神奇的魔力,也没有任何非常奇怪的事情导致这种行为。这只是 shell 的正常行为。另一方面,如果我们稍微更改脚本并明确所需的行为,意外行为就会消失。考虑一下:
$ cat sample-script-2
#!/bin/bash -e
if
echo this will be printed
false || exit
echo but this is not printed because the author was explicit about exiting
then : ; fi
$ ./sample-script-2
this will be printed
errexit 是为了让脚本编写者和维护者的工作更轻松的一种尝试,但它失败了。而且它还在继续失败。借用 python 的话:“显式优于隐式”。如果您希望脚本在命令返回非零时终止,请写入cmd || exit
。如果您希望脚本在命令返回非零时有时失败,并且您希望确保脚本的维护者真的不知道实际发生了什么,请写入set -e; cmd
解决方案 12:
我迟迟才发布有关问题中特定代码的单独答案。它有多个问题。
# Automatically added by dh_installinit
这有点误导,因为脚本的作者显然修改了实际上自动添加的代码。
if [ "$1" = install ]; then
该preinst
脚本将在多种情况下运行,并接收一个参数,该参数指示哪种情况导致它被调用。如果操作是install
(而不是,例如,upgrade
或abort-upgrade
,这是另外两个可能的值;请参阅https://www.debian.org/doc/debian-policy/ch-maintainerscripts.html),则此部分将运行
if [ -d /usr/share/MyApplicationName ]; then
echo "MyApplicationName is just installed"
return 1
fi
这似乎很可疑。该echo
命令可能不会打印到任何有用的地方,并return 1
导致整个安装失败。
更切题的是,MyApplicationName
有异常的大写字母;典型的值更像my_application
是或my-application
rm -Rf $HOME/.config/nautilus-actions/nautilus-actions.conf
rm -Rf $HOME/.local/share/file-manager/actions/*
这些也看起来非常可疑。包管理系统不应该触碰任何用户的任何非系统文件。此外,当此脚本运行时,$HOME
可能会发生这种情况/root
,因此作者希望在这里实现的任何目标可能实际上都不会发生。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理必备:盘点2024年13款好用的项目管理软件