如何在bash命令替换中使用`set -e`? 原始问题的原始答案

我有一个简单的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处退出,并且执行"Success!"语句的if路径。 在true处注释掉#2会导致执行"Error:"语句的if路径。

bash似乎只是忽略了替换内的set -e,而if语句只是检查foo()中最后一条命令的返回代码。

问:是什么原因导致这种奇怪的行为?

A:这就是bash的工作方式,这是正常行为

问:有什么方法可以使bash在命令替换中遵守set -e并使其正常工作?

A:您不应为此目的使用set -e

问:如果不使用set -e,如何实现此功能(即,仅当执行过程中出现问题时才打印函数的输出)?

A:请参阅已接受的答案和我的“最终想法”部分。

我正在使用:

  

GNU bash,版本5.0.11(1)-发行版(x86_64-apple-darwin18.6.0)

最终想法/总结(可能对其他人有用)

请注意,使用if ...; then甚至是&& ... || ...将禁用大多数“传统” bash错误处理方法(包括set -etrap ... ERR + {{1} })设计。 如果您想做像我一样的事情,您可能应该手动检查函数内部的返回代码,并手动返回非空退出代码(set -o errtrace),以避免继续执行错误(无论是否执行此操作,您是否使用dangerous_command || return 1

根据回答,set -e不会在设计的命令替换内传播。 如果您希望实现这样的错误处理逻辑,则可以将set -etrap ... ERR结合使用,这将与在命令替换内部运行的函数一起使用(也就是说,除非将它们放在{{1}中) }语句,它也会禁用set -o errtrace,因此,在这种情况下,如果您希望在出错时停止函数,则手动返回代码检查是唯一的选择。

如果您考虑一下,那么整个行为都是有意义的:您不会期望脚本以if语句“保护”的命令终止,因为{{1 }}语句正在检查命令是否成功。

我个人仍然不会完全避免使用trap ... ERRif,因为它们确实有用,但是了解 在不同情况下的表现很重要,因为它们也不是银弹。

jinsong0002 回答:如何在bash命令替换中使用`set -e`? 原始问题的原始答案

  

问:在没有set -e的情况下如何实现这一点(即,仅在执行时出现问题的情况下打印函数的输出)?

您可以通过检查函数的返回值来使用这种方式:

#!/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是偶数,则该虚拟功能代码将返回失败,而$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

但是请注意,在外壳程序脚本中使用set -e并不理想,并且在许多情况下可能无法退出脚本。

Do check this important post on set -e

,
  

在不使用set -e的情况下如何实现(即仅在执行过程中出现问题时才输出函数的输出)?

从函数返回非零返回状态以指示错误/失败。

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
  • { ... || true; }周围的read隐藏了read在其输入上遇到EOF时返回的非零状态,从而防止了管道失败。
  • code=${PIPESTATUS[0]}捕获管道中第一条命令(foo)的状态。
  • set -o pipefail重新启用pipefail,以便在程序的其余部分启用该功能。
  • 已对问题中的代码进行了一些调整,以停止Shellcheck警告。
本文链接:https://www.f2er.com/3101606.html

大家都在问