Bash subshel​​l errexit语义

时间:2015-04-09 07:55:42

标签: bash

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

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

e.g。

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

将回应"推动:0"成功,或者"推动:别的东西"失败。

但是,如果我想让一个子shell启用errexit,那么我希望捕获这个子shell的退出代码呢?

例如:

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

(
    git push
    echo "Hai"
) && true

echo "Did it work: $?"

问题是,bash看到&& boolean运算符并禁用子shell的errexit。这意味着" Hai"永远是回声。这是不可取的。

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

更新

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

5 个答案:

答案 0 :(得分:9)

看来我偶然发现许多贝壳爱好者争论不休:

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

基本上,标准说了些什么,解释者忽略了它,因为标准似乎不合逻辑,但现在像Bash这样的解释器确实令人困惑的语义,没有人想要解决它。

不幸的是,trap <blah> EXIT不能用来做我想要的,因为trap基本上只是信号的中断处理程序,没有办法在预定的时候继续执行脚本point(就像在其他语言中使用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

答案 1 :(得分:1)

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

这些示例退出到父shell而不打印&#39; hai&#39;

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

相同的:

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

相同的:

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

与运营商创建Compond命令&amp;&amp;&amp;或||&#39;在子shell之后保持子shell打开,直到所有命令都解析。

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

man bash

/errexit

Exit immediately if a pipeline (which may consist of a single simple command), a list, or a compound command (see SHELL GRAMMAR above), exits with a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test following the if or elif reserved words, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return value is being inverted with !. If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit. A trap on ERR, if set, is executed before the shell exits. This option applies to the shell environment and each subshell environment separately (see COMMAND EXECUTION ENVIRONMENT above), and may cause subshells to exit before executing all the commands in the subshell.

bash手册的这一部分再次提及:

The ERR trap is not executed if the failed command is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of a command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command's return value is being inverted using !. These are the same conditions obeyed by the errexit (-e) option.

最后,Bash手册的这一部分表明set -e由POSIX启用的Bash的子shell继承:

Subshells spawned to execute command substitutions inherit the value of the -e option from the parent shell. When not in posix mode, bash clears the -e option in such subshells.

答案 2 :(得分:1)

你可以通过输出解析做一些黑客攻击。命令替换不会继承errexit(除了使用inherit_errexit在Bash 4.4上),但它确实会继承ERR陷阱errtrace。因此,您可以使用陷阱在出错时退出子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标志的负担。也许有一种方法可以在没有解析的情况下利用相同的模式?

答案 3 :(得分:0)

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

答案 4 :(得分: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