如果 errexit 打开,我如何运行可能失败的命令并获取其退出代码?
Posted
技术标签:
【中文标题】如果 errexit 打开,我如何运行可能失败的命令并获取其退出代码?【英文标题】:If errexit is on, how do I run a command that might fail and get its exit code? 【发布时间】:2017-10-20 05:29:35 【问题描述】:我的所有脚本都打开了 errexit;也就是说,我运行set -o errexit
。但是,有时我想运行grep
之类的命令,但即使命令失败也想继续执行我的脚本。
我该怎么做?也就是说,如何在不杀死整个脚本的情况下将命令的退出代码放入变量中?
我可以关闭 errexit,但我不想这样做。
【问题讨论】:
保持set -e
关闭is really the best thing。自动错误处理只有在其行为一致、可预测和可靠时才比成本更有利,而 set -e
不是这些事情:它在 shell 之间、同一 shell 的 版本 之间变化,并且具有许多隐藏的惊喜:例如,如果您正在编写一个函数,errexit
是否对其中的任何命令的错误生效取决于 函数本身的退出状态是否正在测试。
@CharlesDuffy 唯一的替代方案是 shell 在出错后脱轨,破坏我的服务器,或者在每个命令后添加 || exit $?
。您描述的-e
缺陷导致的代码维护开销比这小得多。
@ivan_pozdeev,首先,它只是|| exit
(当不在需要|| return
的函数上下文中时); $?
是默认退出状态。其次,foo || exit
在每条命令之后显然不是正确的——如果是这样,set -e
的行为就不会有这么大(并且在 shell 之间变化)集其行为中的异常,使其行为对上下文敏感且难以预测。例如,(( ++count )) || exit
显然是错误的,因为它在将 count
从 0
增加到 1
时退出,但 set -e; (( ++count ))
正是这样做的。
...或者,更确切地说,在某些版本的 bash 中而不是在其他版本中,这进一步强化了我的观点:如果你不能依赖set -e
为保持一致,您最好进行明确的 || die "error message here"
调用(die
是在退出前打印错误的函数的常规名称),这样您可以确保一致性。
【参考方案1】:
您的errexit
只会在失败的命令是“未经测试”的情况下导致脚本终止。根据 FreeBSD 上的 man sh
:
Exit immediately if any untested command fails in non-interactive
mode. The exit status of a command is considered to be explic-
itly tested if the command is part of the list used to control an
if, elif, while, or until; if the command is the left hand oper-
and of an ``&&'' or ``||'' operator; or if the command is a pipe-
line preceded by the ! keyword.
所以..如果您正在考虑使用这样的构造:
grep -q something /path/to/somefile
retval=$?
if [ $retval -eq 0 ]; then
do_something # found
else
do_something_else # not found
fi
你应该改用这样的结构:
if grep -q something /path/to/somefile; then
do_something # found
else
do_something_else # not found
fi
if
关键字的存在使得 grep 命令经过测试,因此不受errexit
的影响。而且这种方式需要更少的打字。
当然,如果你真的需要变量中的退出值,那么没有什么能阻止你使用$?
:
if grep -q something /path/to/somefile; then
do_something # found
else
unnecessary=$?
do_something $unnecessary # not found
fi
【讨论】:
很好的答案。请注意在后一个示例上扩展的以下“陷阱”:if ! false; then e=$?; echo $e; else echo ok; fi
打印 0
,因为 !
运算符引入了另一个评估,而 if false; then echo ok; else e=$?; echo $e; fi
prints 1
符合预期。【参考方案2】:
这里有一种方法可以实现这一点:您可以“关闭”set -o errexit
的某些代码行,然后在您决定时再次打开它:
set +e #disables set -o errexit
grep -i something file.txt
rc="$?" #capturing the return code for last command
set -e #reenables set -o errexit
另一个选项如下:
grep -i something file.txt || rc="$?"
这将允许您捕获变量rc
的返回码,而不会中断您的脚本。您甚至可以扩展最后一个选项以在同一行捕获和处理返回代码,而不会触发退出:
grep -i something file.txt || rc="$?" && echo rc="$?" > somefile.txt && command || :
最后一位 ||:
将保证上面的行总是返回一个返回码 = 0 (true)。
【讨论】:
事实上,是使用了 or 条 (||
),而不是变量回显到文本文件。如果命令的退出状态位于||
的左侧,则认为它已被显式测试。有关手册页的全文,请参阅 my answer。
是的,我没有另外说明(使用“或”||
是使其“工作”的原因)。但是,我上面的评论是正确的,因为如果最后一个命令失败(在我的示例中为 echo "$rc" > file.txt
),那仍然会触发退出。
我认为你的评论的唯一问题是它没有明确分号后出现的命令实际上不是包含||
的同一命令行的一部分,它完全是不同的命令,无论其在相关行的右侧或下方的“位置”如何。如果写入file.txt
会产生权限错误,则与问题中的问题无关。
@ghoti 好的,我现在明白你的意思了。为了清楚起见,我更改了答案,并添加了一些改进。【参考方案3】:
我能想到的最紧凑、重复最少的形式:
<command> && rc=$? || rc=$?
没有变量名重复的表单——rc=$(<command> && echo $? || echo $?)
——有一个表达式右值,但也会捕获<command>
1的标准输出。所以,只有当你“知道”<command>
没有正常输出时,它才是安全的。
使用a && b || c
构造is safe here,因为rc=$?
和$(echo $?)
永远不会失败。
1当然,您可以通过摆弄文件描述符来解决这个问题,但这会使构造冗长且不方便作为标准
【讨论】:
以上是关于如果 errexit 打开,我如何运行可能失败的命令并获取其退出代码?的主要内容,如果未能解决你的问题,请参考以下文章