Bash子shell的errexit语义

15

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

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

例如:

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

成功时将回显“Pushed: 0”,失败时将回显“Pushed: 其他内容”。

但是,如果我想要一个启用了 errexit 的子 shell,但是我又希望捕获这个子 shell 的退出代码怎么办?

例如:

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

(
    git push
    echo "Hai"
) && true

echo "Did it work: $?"

问题是,bash看到&&布尔运算符并禁用子shell的errexit。这意味着"Hai"总是会被echo出来。这是不可取的。

如何在此子shell中启用errexit,并捕获子shell的状态代码,而不让该退出代码终止外部shell,而无需在各处不断启用和禁用errexit

更新

我强烈感觉解决方案是使用traps并捕获退出信号。在我进行自我回答之前,请随意提供答案。


也许你实际上是在谈论 errexit - Barmar
@Barmar 抱歉,我实际上是在谈论两者的结合。我的真正脚本确实包含了子shell中的管道。我会更新问题。 - Benjamin Dobell
@Barmar 我已经更新了问题,现在是关于 errexit 而不是 pipefail(因为 pipefail 在这里无关紧要)。 - Benjamin Dobell
@Barmar,您错误地编辑了我的问题。set -e等同于set -o errexit。您实际上让它重复了设置errexit - Benjamin Dobell
是的,我刚意识到这个问题,当你进行修复时,我正要回退。 - Barmar
显示剩余5条评论
5个回答

19

看起来我碰到了很多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 $? 保护其他语句了。 - marcolz
是的,多年前我决定set -e是一种"正确性表演"的做法。比盲目模仿稍微好一点(因为其基本原理实际上是合理的,并且对于最简单的情况下达到预期目标确实有效)。但在实践中,即使在同一个shell中,它的一致性也不足以满足非平凡代码的预期/意图,每次我真正需要强大的错误处理时,都必须手动编写而不使用set -eset -e实际上会使得完整/强大/通用解决方案变得更糟)。 - mtraceur

4
您可以通过输出解析进行一些黑客攻击。 命令替换不会继承 errexit (除了使用 inherit_errexit 的Bash 4.4),但它确实继承了 errtrace ERR 陷阱。 因此,您可以使用该陷阱在出现错误时退出子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"

请注意,命令替换会吞噬末尾的换行符,因此如果子shell中原本没有换行符,则该代码将向输出中添加一个换行符。
这种方法可能不是100%可靠,但可以减轻您反复切换 errexit 标志的负担。也许有一种方法可以利用相同的模式而不进行解析?

2
inherit_errexit,正是我在找的。谢谢你提到它! - bgfvdu3w

1
使用Bash的set命令可以更改当前shell的选项。对shell选项的更改不会被子shell继承。原因是脚本作者可能希望为子shell更改环境选项!
这些示例退出到父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语法)以非零状态退出,则立即退出。如果失败的命令是while或until关键字后紧随其后的命令列表的一部分、if或elif保留字后的测试的一部分、在&&或||列表中执行的任何命令,但不包括最后一个&&或||之后的命令、管道中的任何命令但不包括最后一个命令,或者命令的返回值被!反转,则shell不会退出。如果除子Shell外的复合命令因在忽略-e时失败而返回非零状态,则shell不会退出。如果设置了ERR陷阱,则在shell退出之前将执行该陷阱。此选项适用于shell环境和每个子shell环境(请参阅上面的COMMAND EXECUTION ENVIRONMENT),并且可能导致子shell在执行所有命令之前退出。
这部分bash手册再次提到了它:

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

最后,Bash手册的此部分指示set -e通过启用POSIX的Bash被子shell继承:

生成用于执行命令替换的子shell从父shell继承-e选项的值。当未处于posix模式时,bash会在这样的子shell中清除-e选项。


这个答案不起作用。具体来说,它没有考虑到子shell周围上下文引起的问题,正如其他答案中所讨论的那样。 - Matthew Booth

0

0

我使用这段代码

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接