shell 如何在bash命令替换中使用`set -e`?

gev0vcfq  于 2023-05-29  发布在  Shell
关注(0)|答案(3)|浏览(180)

我有一个简单的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 -etrap ... ERR + set -o errtrace)。如果你想像我一样做一些事情,你可能应该手动检查函数内部的返回代码,并手动返回一个非空的退出代码(dangerous_command || return 1),以避免在错误时继续执行(无论你是否使用set -e,你都可以这样做)。
正如所回答的,set -e不会在命令替换内传播by design。如果希望实现错误处理逻辑,可以将trap ... ERRset -o errtrace结合使用,这将与在命令替换中运行的函数一起工作(除非将它们放在if语句中,这也将禁用trap ... ERR,因此在这种情况下,如果您希望在出现错误时停止函数,则手动返回代码检查是您唯一的选择)。
如果你仔细想想,这整个行为是有道理的:你不会期望你的脚本在一个由if语句“保护”的命令上终止,因为你的if语句的全部目的是检查命令是否成功。
就我个人而言,我仍然不会完全避免set -etrap ... ERR,因为它们真的很有用,但是理解它们在不同情况下的行为是很重要的,因为它们也不是银。

v6ylcynt

v6ylcynt1#

问:如果没有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

但是请注意,在shell脚本中使用set -e并不理想,在许多情况下它可能无法退出脚本。
Do check this important post on set -e

niknxzdl

niknxzdl2#

如果没有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
pkwftd7m

pkwftd7m3#

正如其他人所说,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警告。

相关问题