如何在 bash 命令替换中使用 `set -e`?
How to use `set -e` inside a bash command substitution?
我有一个简单的 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
的 return 代码不为零,我们使用 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
语句只是检查了 [=14= 中最后一个命令的 return 代码].
问:是什么导致了这种奇怪的行为?
答:这就是 bash 的工作原理,这是正常行为
问:有没有什么方法可以让 bash 在命令替换中尊重 set -e
并使其正常工作?
答:您不应该为此目的使用 set -e
问:在没有 set -e
的情况下,您将如何实现此功能(即仅在执行函数时出错时才打印函数的输出)?
A:查看已接受的答案和我的 "final thoghts" 部分。
我正在使用:
GNU bash, version 5.0.11(1)-release (x86_64-apple-darwin18.6.0)
最后的想法/要点(可能对其他人有用):
请注意,使用 if ...; then
,甚至 && ... || ...
将禁用大多数类型的 "traditional" bash 错误处理方法(这包括 set -e
和 trap ... ERR
+ set -o errtrace
) 设计。
如果你想像我一样做一些事情,你可能应该手动检查函数中的 return 代码并手动检查 return 非空退出代码 (dangerous_command || return 1
) 以避免继续执行出错时(无论是否使用 set -e
都可以这样做)。
如回答所述,set -e
不会在命令替换内部传播 设计。
如果您希望实现错误处理逻辑,您可以将 trap ... ERR
与 set -o errtrace
结合使用,这将在命令替换中与函数 运行 一起工作(除非您将它们放在if
语句,这也会禁用 trap ... ERR
,因此在这种情况下,手动 return 代码检查是您唯一的选择,如果您希望在错误时停止您的功能)。
如果你仔细想想,这整个行为是有道理的:你不会期望你的脚本在 if
语句的命令 "guarded" 上终止,因为整个点您的 if
语句正在检查命令是否成功。
就我个人而言,我仍然不会完全避免 set -e
和 trap ... ERR
,因为它们确实很有用,但了解 它们的行为方式在不同的情况下很重要,因为它们也不是灵丹妙药。
Q: How would you go about implementing this without set -e
(i.e. print the output of a function only if something went wrong while executing it)?
您可以通过检查函数的 return 值来使用这种方式:
#!/usr/bin/env bash
foo() {
local n=$RANDOM
echo "Foo working with random=$n ..."
(($n % 2))
}
echo "Doing something that could fail..."
a="$(foo 2>&1)"
code=$?
if (($code == 0)); then
echo "Success!"
else
printf '{"ErrorCode": %d, "ErrorMessage": "%s"}\n' $code "$a"
exit $code
fi
现在 运行 为:
$> ./errScript.sh
Doing something that could fail...
Success!
$> ./errScript.sh
Doing something that could fail...
{"ErrorCode": 1, "ErrorMessage": "Foo working with random=27662 ..."}
$> ./errScript.sh
Doing something that could fail...
Success!
$> ./errScript.sh
Doing something that could fail...
{"ErrorCode": 1, "ErrorMessage": "Foo working with random=31864 ..."}
如果 $RANDOM
是偶数,此虚拟函数代码 return 失败,如果 $RANDOM
是奇数,则成功。
原始问题的原始答案
您还需要在命令替换中启用 set -e
:
#!/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
}
printf "Doing something that could fail... "
a="$(set -e; foo)"
code=$?
if (($code == 0)); then
echo "Success!"
else
echo "Error:"
printf "${a}"
exit $code
fi
然后将其用作:
./errScript.sh; echo $?
Doing something that could fail... 1
但是请注意,在 shell 脚本中使用 set -e
并不理想,并且在许多情况下可能无法退出脚本。
How would you go about implementing this without set -e (i.e. print the output of a function only if something went wrong while executing it)?
Return 来自函数的非零 return 状态,表示 error/failure.
foo() {
printf "Foo working... "
echo "Failed!"
return 1 # point of interest #1
return 0 # point of interest #2
}
if a="$(foo 2>&1)"; then
echo "Success!"
else
code=$?
echo "Error:"
printf "${a}"
exit $code
fi
正如其他人所说,errexit
不是处理程序错误的可靠方法。它的一大问题是它在几种常见情况下被静默禁用,包括在命令替换中。
如果您仍想使用 errexit
,有几种方法可以获得您想要的效果。
一种方法是在主代码中暂时禁用 errexit
,在命令替换中显式启用 errexit
(如@anubhava 的回答所示),获取退出代码来自 $?
的命令替换,并在主代码中重新启用 errexit
。
另一种可能的方法(在问题中的序言和 foo
定义代码之后)是:
shopt -s lastpipe
printf "Doing something that could fail... "
set +o pipefail
foo 2>&1 | { read -r -d '' a || true; }
code=${PIPESTATUS[0]}
set -o pipefail
if (( code == 0 )); then
echo "Success!"
else
echo "Error:"
printf '%s\n' "$a"
exit "$code"
fi
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
因此程序的其余部分都启用它。
- 已对问题中的代码进行了一些调整以停止 Shellcheck 警告。
我有一个简单的 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
的 return 代码不为零,我们使用 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
语句只是检查了 [=14= 中最后一个命令的 return 代码].
问:是什么导致了这种奇怪的行为?
答:这就是 bash 的工作原理,这是正常行为
问:有没有什么方法可以让 bash 在命令替换中尊重 set -e
并使其正常工作?
答:您不应该为此目的使用 set -e
问:在没有 set -e
的情况下,您将如何实现此功能(即仅在执行函数时出错时才打印函数的输出)?
A:查看已接受的答案和我的 "final thoghts" 部分。
我正在使用:
GNU bash, version 5.0.11(1)-release (x86_64-apple-darwin18.6.0)
最后的想法/要点(可能对其他人有用):
请注意,使用 if ...; then
,甚至 && ... || ...
将禁用大多数类型的 "traditional" bash 错误处理方法(这包括 set -e
和 trap ... ERR
+ set -o errtrace
) 设计。
如果你想像我一样做一些事情,你可能应该手动检查函数中的 return 代码并手动检查 return 非空退出代码 (dangerous_command || return 1
) 以避免继续执行出错时(无论是否使用 set -e
都可以这样做)。
如回答所述,set -e
不会在命令替换内部传播 设计。
如果您希望实现错误处理逻辑,您可以将 trap ... ERR
与 set -o errtrace
结合使用,这将在命令替换中与函数 运行 一起工作(除非您将它们放在if
语句,这也会禁用 trap ... ERR
,因此在这种情况下,手动 return 代码检查是您唯一的选择,如果您希望在错误时停止您的功能)。
如果你仔细想想,这整个行为是有道理的:你不会期望你的脚本在 if
语句的命令 "guarded" 上终止,因为整个点您的 if
语句正在检查命令是否成功。
就我个人而言,我仍然不会完全避免 set -e
和 trap ... ERR
,因为它们确实很有用,但了解 它们的行为方式在不同的情况下很重要,因为它们也不是灵丹妙药。
Q: How would you go about implementing this without
set -e
(i.e. print the output of a function only if something went wrong while executing it)?
您可以通过检查函数的 return 值来使用这种方式:
#!/usr/bin/env bash
foo() {
local n=$RANDOM
echo "Foo working with random=$n ..."
(($n % 2))
}
echo "Doing something that could fail..."
a="$(foo 2>&1)"
code=$?
if (($code == 0)); then
echo "Success!"
else
printf '{"ErrorCode": %d, "ErrorMessage": "%s"}\n' $code "$a"
exit $code
fi
现在 运行 为:
$> ./errScript.sh
Doing something that could fail...
Success!
$> ./errScript.sh
Doing something that could fail...
{"ErrorCode": 1, "ErrorMessage": "Foo working with random=27662 ..."}
$> ./errScript.sh
Doing something that could fail...
Success!
$> ./errScript.sh
Doing something that could fail...
{"ErrorCode": 1, "ErrorMessage": "Foo working with random=31864 ..."}
如果 $RANDOM
是偶数,此虚拟函数代码 return 失败,如果 $RANDOM
是奇数,则成功。
原始问题的原始答案
您还需要在命令替换中启用 set -e
:
#!/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
}
printf "Doing something that could fail... "
a="$(set -e; foo)"
code=$?
if (($code == 0)); then
echo "Success!"
else
echo "Error:"
printf "${a}"
exit $code
fi
然后将其用作:
./errScript.sh; echo $?
Doing something that could fail... 1
但是请注意,在 shell 脚本中使用 set -e
并不理想,并且在许多情况下可能无法退出脚本。
How would you go about implementing this without set -e (i.e. print the output of a function only if something went wrong while executing it)?
Return 来自函数的非零 return 状态,表示 error/failure.
foo() {
printf "Foo working... "
echo "Failed!"
return 1 # point of interest #1
return 0 # point of interest #2
}
if a="$(foo 2>&1)"; then
echo "Success!"
else
code=$?
echo "Error:"
printf "${a}"
exit $code
fi
正如其他人所说,errexit
不是处理程序错误的可靠方法。它的一大问题是它在几种常见情况下被静默禁用,包括在命令替换中。
如果您仍想使用 errexit
,有几种方法可以获得您想要的效果。
一种方法是在主代码中暂时禁用 errexit
,在命令替换中显式启用 errexit
(如@anubhava 的回答所示),获取退出代码来自 $?
的命令替换,并在主代码中重新启用 errexit
。
另一种可能的方法(在问题中的序言和 foo
定义代码之后)是:
shopt -s lastpipe
printf "Doing something that could fail... "
set +o pipefail
foo 2>&1 | { read -r -d '' a || true; }
code=${PIPESTATUS[0]}
set -o pipefail
if (( code == 0 )); then
echo "Success!"
else
echo "Error:"
printf '%s\n' "$a"
exit "$code"
fi
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
因此程序的其余部分都启用它。- 已对问题中的代码进行了一些调整以停止 Shellcheck 警告。