我有一个简单的shell脚本,其序言如下:
#!/usr/bin/env bash
set -eu
set -o pipefail
我还有以下功能:
foo() {
printf "Foo working... "
echo "Failed!"
false # point of interest #1
true # point of interest #2
}
将foo()
作为常规命令执行可以按预期工作:脚本在#1
处退出,因为false
的返回码是非零的,我们使用set -e
。
我的目标是将函数foo()
的输出捕获到一个变量中,并仅在执行foo()
期间发生错误时才打印它。这是我的想法
printf "Doing something that could fail... "
if a="$(foo 2>&1)"; then
echo "Success!"
else
code=$?
echo "Error:"
printf "${a}"
exit $code
fi
脚本不会在#1
处退出,并执行if
语句的"Success!"
路径。注解掉#2
处的true
会导致执行if
语句的"Error:"
路径。
看起来bash只是忽略了替换中的set -e
,而if
语句只是检查foo()
中最后一个命令的返回代码。
Q:是什么导致了这种奇怪的行为?
答:这就是bash的工作方式,这是正常的行为
问:有没有办法让bash在命令替换中尊重set -e
,并使其正确工作?
答:您不应该将set -e
用于此目的
问:如果没有set -e
(即set -e
),您将如何实现它?只有在执行某个函数时出错时才打印该函数的输出)。
答:见接受的答案和我的“最后的想法”部分。
我正在使用:
GNU bash,版本5.0.11(1)-release(x86_64-apple-darwin 18.6.0)
最后的想法/外卖(可能对其他人有用):
请注意,使用if ...; then
,甚至&& ... || ...
将禁用大多数“传统”bash错误处理方法(包括set -e
和trap ... ERR
+ set -o errtrace
)。如果你想像我一样做一些事情,你可能应该手动检查函数内部的返回代码,并手动返回一个非空的退出代码(dangerous_command || return 1
),以避免在错误时继续执行(无论你是否使用set -e
,你都可以这样做)。
正如所回答的,set -e
不会在命令替换内传播by design。如果希望实现错误处理逻辑,可以将trap ... ERR
与set -o errtrace
结合使用,这将与在命令替换中运行的函数一起工作(除非将它们放在if
语句中,这也将禁用trap ... ERR
,因此在这种情况下,如果您希望在出现错误时停止函数,则手动返回代码检查是您唯一的选择)。
如果你仔细想想,这整个行为是有道理的:你不会期望你的脚本在一个由if
语句“保护”的命令上终止,因为你的if
语句的全部目的是检查命令是否成功。
就我个人而言,我仍然不会完全避免set -e
和trap ... ERR
,因为它们真的很有用,但是理解它们在不同情况下的行为是很重要的,因为它们也不是银。
3条答案
按热度按时间v6ylcynt1#
问:如果没有
set -e
(即set -e
),您将如何实现它?打印函数的输出,只有在执行过程中出错时才打印)?你可以通过检查函数的返回值来使用这种方法:
现在运行如下:
如果
$RANDOM
是偶数,此伪函数代码返回失败,如果$RANDOM
是奇数,则返回成功。原问题原答案
您还需要在命令替换中启用
set -e
:然后将其用作:
但是请注意,在shell脚本中使用
set -e
并不理想,在许多情况下它可能无法退出脚本。Do check this important post on
set -e
niknxzdl2#
如果没有set -e(即只有在执行某个函数时出错时才打印该函数的输出)。
从函数中返回一个非零的返回状态来指示错误/失败。
pkwftd7m3#
正如其他人所说,
errexit
不是处理程序错误的可靠方法。它的一个大问题是,在几种常见情况下,包括在命令替换中,它会被静默禁用。如果你仍然想使用
errexit
,有几种方法可以获得你想要的效果。一种方法是在主代码中暂时禁用
errexit
,在命令替换中显式启用errexit
(如@anubhava的答案所示),从$?
获取命令替换的退出代码,并在主代码中重新启用errexit
。另一种可能的方法(在问题中的前导码和
foo
定义代码之后)是:shopt -s lastpipe
导致管道的最后一个命令在顶级shell中运行。这意味着在管道末尾的命令中设置的变量(如本例中的a
)可以在程序中稍后使用。lastpipe
是在Bash 4.2中引入的,所以这段代码不能在旧版本的Bash上运行。set +o pipefail
(临时)禁用pipefail
,以防止在管道开始时发生故障的foo
导致整个管道失败。read -r -d '' a
将其所有输入(假设不包含NUL字符),包括内部换行符,读入变量a
。read
周围的{ ... || true; }
隐藏了read
在其输入上遇到EOF时返回的非零状态,从而防止管道失败。code=${PIPESTATUS[0]}
捕获管道中第一个命令的状态(foo
)。set -o pipefail
重新启用pipefail
,以便在程序的其余部分启用它。