【问题标题】:Bash subshell errexit semanticsBash subshel​​l errexit 语义
【发布时间】:2015-04-09 07:55:41
【问题描述】:

我为我的 shell 脚本启用了 errexit(和 pipefail),因为这是我通常想要的行为。但是,有时我想捕获错误并以特定方式处理它们。

我知道对于包含布尔运算符或将用作条件(if、while 等)的命令禁用 errexit

例如

git push && true
echo "Pushed: $?"

成功时会回显“Pushed: 0”,失败时会回显“Pushed: something else”。

但是,如果我希望子 shell 启用 errexit,但又希望捕获此子 shell 的退出代码,该怎么办?

例如:

#!/usr/bin/env bash
set -o errexit

(
    git push
    echo "Hai"
) && true

echo "Did it work: $?"

问题是,bash 看到 && 布尔运算符并禁用子 shell 的 errexit。这意味着“海”总是被回响。这是不可取的。

如何在这个子 shell 中启用 errexit,并捕获子 shell 的状态代码,而不让该退出代码终止外壳 而不是不断地启用和禁用整个地方的 errexit

更新

我有一种强烈的感觉,解决方案是使用陷阱并捕获退出信号。在我自行回答之前,请随时提供答案。

【问题讨论】:

  • 我看不出pipefail 与这些有什么关系。您的脚本中没有管道。
  • 也许你说的是errexit
  • @Barmar 抱歉,我实际上是在谈论两者的结合。我的真实脚本确实包含子外壳内的管道。我会更新问题。
  • @Barmar 我已将问题更新为关于 errexit 而不是 pipefail(因为 pipefail 在这里无关紧要)。
  • @Barmar 您错误地编辑了我的问题。 set -e 等价于 set -o errexit。您有效地说 set errexit 两次。

标签: bash


【解决方案1】:

似乎我偶然发现了许多 shell 爱好者的争论点:

http://austingroupbugs.net/view.php?id=537#bugnotes

基本上,标准说了些什么,解释器忽略了它,因为标准似乎不合逻辑,但现在像 Bash 这样的解释器确实存在语义混乱,没有人愿意修复它。

不幸的是,trap <blah> EXIT 不能用来做我想做的事情,因为trap 基本上只是信号的中断处理程序,没有办法在预定点继续执行脚本(因为你会在其他语言中使用try..finally 块)。

一切都很糟糕

因此,据我所知,从本质上讲,绝对没有明智的方法来执行错误处理。您的选择是:

#!/usr/bin/env bash
set -e

# Some other code

set +e
(
    git push || exit $?
    echo "Hai"
)
echo "Did it work: $?"
set -e

或:

#!/usr/bin/env bash
set -e

(
    git push &&
    echo "Hai" ||
    exit $?
) && true

echo "Did it work: $?"

有点让你想知道为什么你首先要打扰set -e

【讨论】:

  • 您的第一个选项接近我想要的,但可以更符合预期:在子shell中您可以再次执行set -e,这样您就不必保护您的其他带有|| exit $? 的语句了。
【解决方案2】:

您可以对输出解析进行一些修改。命令替换不继承errexit(除了在带有inherit_errexit 的Bash 4.4 上),但它确实继承了带有errtraceERR 陷阱。所以你可以使用陷阱在出错时退出子shell,并使用local或其他方式避免退出父shell。

handle_error() {
    local exit_code=$1 && shift
    echo -e "\nHANDLE_ERROR\t$exit_code"
    exit $exit_code
}

return_code() {
    # need to modify if not GNU head/tail
    local output="$(echo "$1" | head -n -1)"
    local result="$(echo "$1" | tail -1)"
    if [[ $result =~ HANDLE_ERROR\  [0-9]+ ]]; then
        echo "$output"
        return $(echo "$result" | cut -f2)
    else
        echo "$1"
        return 0
    fi
}

set -o errtrace
trap 'handle_error $?' ERR

main() {
    local output="$(echo "output before"; echo "running command"; false; echo "Hai")"
    return_code "$output" && true
    echo "Did it work: $?"
}

main

不幸的是,在我使用带有命令替换的&& true 的测试中,陷阱无法工作(即使使用命令分组),所以你不能将它折叠成一行。如果你想这样做,那么你可以让handle_error 设置一个全局变量而不是返回退出状态。然后你得到:

    return_code "$(echo "output before"; echo "running command"; false; echo "Hai")"
    echo "Did it work: $global_last_error"

还要注意command substitution swallows trailing newlines,所以目前如果子shell 的输出中最初没有换行符,这段代码会在输出中添加一个换行符。

这可能不是 100% 可靠的,但可以减轻您反复切换 errexit 标志的负担。也许有一种方法可以在不解析的情况下利用相同的模式?

【讨论】:

  • inherit_errexit,正是我想要的。感谢您提及!
【解决方案3】:

使用 Bash set 命令更改当前 shell 的选项。对 shell 选项的更改不会被子 shell 继承。原因是脚本作者可能想更改 subshel​​l 的环境选项!

这些示例退出到父 shell 而不打印 'hai'

( set -e; git push; echo 'hai' )

相同:

( set -e; git push; printf '\nhai' )

相同:

( set -e
git push
printf '\nhai'
)

使用运算符“&& 或 ||”创建复合命令跟随子shell 保持子shell 打开,直到所有命令都解决。

使用这两个命令查找引用的 bash 手册部分:

man bash
/errexit

如果管道(可能由单个简单命令组成)、列表或复合命令(参见上面的 SHELL GRAMMAR)以非零状态退出,则立即退出。如果失败的命令是紧跟在 while 或 until 关键字之后的命令列表的一部分、位于 if 或 elif 保留字之后的测试的一部分、在 && 或 || 中执行的任何命令的一部分,则 shell 不会退出列出除了最后一个 && 或 || 之后的命令,管道中除最后一个之外的任何命令,或者命令的返回值是否用 ! 反转。如果子shell 以外的复合命令由于在忽略-e 时命令失败而返回非零状态,则shell 不会退出。如果设置了 ERR 陷阱,则会在 shell 退出之前执行。该选项分别适用于 shell 环境和每个子 shell 环境(参见上面的命令执行环境),并且可能导致子 shell 在执行子 shell 中的所有命令之前退出。

这部分bash手册再次提到:

如果失败的命令是命令的一部分,则不会执行 ERR 陷阱 紧跟在 while 或 until 关键字之后的列表,if 语句中的部分测试,在 && 或 || 中执行的命令的一部分列出除了最后一个 && 或 || 之后的命令,管道中除最后一个之外的任何命令,或者命令的返回值是否正在使用 ! 反转。这些是 errexit (-e) 选项遵循相同的条件。

最后,Bash 手册的这一部分指出set -e 被启用了 POSIX 的 Bash 的子shell 继承:

为执行命令替换而生成的子 shell 从父 shell 继承 -e 选项的值。当不在 posix 模式下时,bash 会清除此类子 shell 中的 -e 选项。

【讨论】:

  • 这个答案不起作用。具体来说,它没有考虑由子外壳的周围上下文引起的问题,如其他答案中所述。
【解决方案4】:

可能是一个错误: https://groups.google.com/forum/?fromgroups=#!topic/gnu.bash.bug/NCK_0GmIv2M 这就是说,set -e in (subshel​​ls) 应该独立于周围的上下文。 手册页说“[set -e] 适用于 shell 环境和 每个子shell环境单独”,但实际上 set -e 如果在周围上下文中被禁用,则无法在(子shell)中工作。

【讨论】:

    【解决方案5】:

    我用这个代码

    function runWithOwnErrorHandling {
    
      functionName="$1"
      shift
    
      set +o errexit
      OLDTRAP="$(trap -p ERR)"
      trap - ERR
      (
        set -o errexit
    
        $functionName "$@"
      )
      FUNCEXIT=$?
      set -o errexit
      $OLDTRAP
    }
    

    调用它

    function someFunc {
      echo "c1"; false
      echo "c2";
    }
    
    runWithOwnErrorHandling someFunc
    [[ ${FUNCEXIT} -ne 0 ]] && echo someFunc failed
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-26
      • 2011-06-05
      • 1970-01-01
      • 2011-08-20
      • 1970-01-01
      • 2013-06-06
      相关资源
      最近更新 更多