使用“-o errtrace”捕获命令替换中的错误(即 set -E)

使用“-o errtrace”捕获命令替换中的错误(即 set -E)

根据这个参考手册:

-E(也称为 -o errtrace)

如果设置,则 ERR 上的任何陷阱都会由 shell 函数、命令替换和在子 shell 环境中执行的命令继承。在这种情况下,通常不会继承 ERR 陷阱。

但是,我一定是错误地解释了它,因为以下内容不起作用:

#!/usr/bin/env bash
# -*- bash -*-

set -e -o pipefail -o errtrace -o functrace

function boom {
  echo "err status: $?"
  exit $?
}
trap boom ERR


echo $( made up name )
echo "  ! should not be reached ! "

我已经知道简单的赋值,my_var=$(made_up_name)将退出脚本set -e(即errexit)。

-E/-o errtrace应该像上面的代码一样工作吗 ?或者,很可能是我读错了?

答案1

注意:zsh如果您不将其配置为接受此处大多数示例的“内联注释”,并且不像我在sh <<-\CMD.

好吧,正如我在上面的评论中所说,我具体不知道巴什的set -E,但我知道 POSIX 兼容 shell 提供了一种测试值的简单方法(如果您需要的话):

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
        echo "echo still works" 
    }
    _test && echo "_test doesnt fail"
    # END
    CMD
sh: line 1: empty: error string
+ echo

+ echo 'echo still works'
echo still works
+ echo '_test doesnt fail'
_test doesnt fail

上面你会看到虽然我用了parameter expansion去测试${empty?} _test()仍然 returns一次通过——正如最后所表明的那样echo发生这种情况是因为失败的值杀死了$( command substitution )包含它的子 shell,但它的父 shell -_test此时 - 继续前进。并且echo不在乎——很高兴只为一个\newline; echo不是一个测试。

但请考虑一下:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    _test ||\
            echo "this doesnt even print"
    # END
    CMD
_test+ sh: line 1: empty: function doesnt run

因为我喂过_test()'s输入中预先评估的参数INIT here-document现在_test()函数甚至根本不尝试运行。更何况是sh外壳显然完全放弃了幽灵echo "this doesnt even print" 甚至不打印。

大概就是这样不是你想要什么。

发生这种情况是因为${var?}样式参数扩展是旨在退出shell如果缺少参数,则像这样工作:

${parameter:?[word]}

如果出现以下情况,则指示错误Null或者Unset.如果参数未设置或为空,则expansion of word(或者如果省略单词,则指示未设置的消息)应为written to standard errorshell exits with a non-zero exit status否则,值parameter shall be substituted交互式 shell 不需要退出。

我不会复制/粘贴整个文档,但如果您希望失败set but null您使用以下形式的值:

${var :? error message }

随着:colon如上。如果你想要一个null要成功,只需省略冒号即可。您也可以否定它并仅在设定值时失败,正如我稍后将展示的那样。

另一轮运行_test():

    sh <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    echo "this runs" |\
        ( _test ; echo "this doesnt" ) ||\
            echo "now it prints"
    # END
    CMD
this runs
sh: line 1: empty: function doesnt run
now it prints

这适用于各种快速测试,但在上面您会看到_test(),从中间跑pipeline失败了,事实上它包含command listsubshel​​l 完全失败,因为函数内的任何命令都没有运行,以下命令也没有运行echo完全可以运行,尽管也表明它可以很容易地进行测试,因为echo "now it prints" 现在打印。

我想,细节决定成败。在上面的例子中,退出的shell是不是脚本的_main | logic | pipeline但是( subshell in which we ${test?} ) ||所以需要一点沙箱。

这可能并不明显,但如果你只想通过相反的情况,或者只set=值,它也相当简单:

    sh <<-\CMD
    N= #N is NULL
    _test=$N #_test is also NULL and
    v="something you would rather do without"    
    ( #this subshell dies
        echo "v is ${v+set}: and its value is ${v:+not NULL}"
        echo "So this ${_test:-"\$_test:="} will equal ${_test:="$v"}"
        ${_test:+${N:?so you test for it with a little nesting}}
        echo "sure wish we could do some other things"
    )
    ( #this subshell does some other things 
        unset v #to ensure it is definitely unset
        echo "But here v is ${v-unset}: ${v:+you certainly wont see this}"
        echo "So this ${_test:-"\$_test:="} will equal NULL ${_test:="$v"}"
        ${_test:+${N:?is never substituted}}
        echo "so now we can do some other things" 
    )
    #and even though we set _test and unset v in the subshell
    echo "_test is still ${_test:-"NULL"} and ${v:+"v is still $v"}"
    # END
    CMD
v is set: and its value is not NULL
So this $_test:= will equal something you would rather do without
sh: line 7: N: so you test for it with a little nesting
But here v is unset:
So this $_test:= will equal NULL
so now we can do some other things
_test is still NULL and v is still something you would rather do without

上述示例利用了 POSIX 参数替换的所有 4 种形式及其各种:colon null或者not null测试。上面的链接中有更多信息,并且又来了

我想我们应该展示我们的_test函数也能工作,对吗?我们只是声明empty=something作为我们函数的参数(或之前的任何时间):

    sh <<-\CMD
    _test() { echo $( echo ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?tested as a pass before function runs}
    INIT
    echo "this runs" >&2 |\
        ( empty=not_empty _test ; echo "yay! I print now!" ) ||\
            echo "suspiciously quiet"
    # END
    CMD
this runs
not_empty
echo still works
yay! I print now!

应该指出的是,这个评估是独立的——不需要额外的测试就会失败。再举几个例子:

    sh <<-\CMD
    empty= 
    ${empty?null, no colon, no failure}
    unset empty
    echo "${empty?this is stderr} this is not"
    # END
    CMD
sh: line 3: empty: this is stderr

    sh <<-\CMD
    _input_fn() { set -- "$@" #redundant
            echo ${*?WHERES MY DATA?}
            #echo is not necessary though
            shift #sure hope we have more than $1 parameter
            : ${*?WHERES MY DATA?} #: do nothing, gracefully
    }
    _input_fn heres some stuff
    _input_fn one #here
    # shell dies - third try doesnt run
    _input_fn you there?
    # END
    CMD
heres some stuff
one
sh: line :5 *: WHERES MY DATA?

最后我们回到最初的问题:如何处理$(command substitution)子shell中的错误?事实是 - 有两种方法,但都不是直接的。问题的核心是shell的求值过程——shell展开(包括$(command substitution))在 shell 的评估过程中比当前 shell 命令执行更早发生 - 这是您的错误可以被捕获和捕获的时候。

操作遇到的问题是,当当前 shell 评估错误时,$(command substitution)subshel​​l 已被替换掉 - 不存在任何错误。

那么有哪两种方式呢?要么你在$(command substitution)与没有子 shell 一样进行测试,或者将其结果吸收到当前 shell 变量中并测试其值。

方法一:

    echo "$(madeup && echo \: || echo '${fail:?die}')" |\
          . /dev/stdin

sh: command not found: madeup
/dev/stdin:1: fail: die

    echo $?

126

方法二:

    var="$(madeup)" ; echo "${var:?die} still not stderr"

sh: command not found: madeup
sh: var: die

    echo $?

1

无论每行声明的变量数量有多少,这都会失败:

   v1="$(madeup)" v2="$(ls)" ; echo "${v1:?}" "${v2:?}"

sh: command not found: madeup
sh: v1: parameter not set

我们的返回值保持不变:

    echo $?
1

现在是陷阱:

    trap 'printf %s\\n trap resurrects shell!' ERR
    v1="$(madeup)" v2="$(printf %s\\n shown after trap)"
    echo "${v1:?#1 - still stderr}" "${v2:?invisible}"

sh: command not found: madeup
sh: v1: #1 - still stderr
trap
resurrects
shell!
shown
after
trap

    echo $?
0

答案2

这是由于 bash 中的错误造成的。在命令替换步骤中

echo $( made up name )

made在子 shell 中运行(或无法找到),但子 shell 已“优化”,不会使用父 shell 中的某些陷阱。这是固定的版本4.4.5:

在某些情况下,一个简单的命令被优化以消除分叉,导致 EXIT 陷阱不被执行。

使用 bash 4.4.5 或更高版本,您应该看到以下输出:

error.sh: line 13: made: command not found
err status: 127
  ! should not be reached !

陷阱处理程序已按预期调用,然后子 shell 退出。 (set -e只会导致子 shell 退出,而不是父 shell 退出,因此实际上应该到达“不应到达”消息。)

旧版本的解决方法是强制创建完整的、非优化的子 shell:

echo $( ( made up name ) )

额外的空格是必需的与算术展开式区分开来。

答案3

如果设置,则 ERR 上的任何陷阱都会由 shell 函数、命令替换和在子 shell 环境中执行的命令继承

在你的脚本中它是一个命令执行(echo $( made up name ))。在 bash 命令中,用以下任一分隔;或与新队。在命令中

echo $( made up name )

$( made up name )被视为命令的一部分。即使这部分失败并返回错误,整个命令也会成功执行,因为echo不知道这一点。由于命令返回 0,因此没有触发陷阱。

你需要把它放在两个命令中,赋值和echo

var=$(made_up_name)
echo $var

相关内容