如何在 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 -eif 语句只是检查了 [=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 -etrap ... ERR + set -o errtrace) 设计。 如果你想像我一样做一些事情,你可能应该手动检查函数中的 return 代码并手动检查 return 非空退出代码 (dangerous_command || return 1) 以避免继续执行出错时(无论是否使用 set -e 都可以这样做)。

如回答所述,set -e 不会在命令替换内部传播 设计。 如果您希望实现错误处理逻辑,您可以将 trap ... ERRset -o errtrace 结合使用,这将在命令替换中与函数 运行 一起工作(除非您将它们放在if 语句,这也会禁用 trap ... ERR,因此在这种情况下,手动 return 代码检查是您​​唯一的选择,如果您希望在错误时停止您的功能)。

如果你仔细想想,这整个行为是有道理的:你不会期望你的脚本在 if 语句的命令 "guarded" 上终止,因为整个点您的 if 语句正在检查命令是否成功。

就我个人而言,我仍然不会完全避免 set -etrap ... 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 并不理想,并且在许多情况下可能无法退出脚本。

Do check this important post on 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 警告。