使用“set -eu”时 EXIT 和 ERR 陷阱的正确行为

使用“set -eu”时 EXIT 和 ERR 陷阱的正确行为

set -e当使用( errexit)、set -u( nounset) 以及 ERR 和 EXIT 陷阱时,我观察到一些奇怪的行为。它们看起来是相关的,因此将它们放在一个问题中似乎是合理的。

1)set -u不触发ERR陷阱

  • 代码:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
    
  • 预期:ERR 陷阱被调用,RC != 0
  • 实际:ERR 陷阱是不是被叫,RC == 1
  • 注意:set -e不改变结果

2)set -eu在 EXIT 陷阱中使用退出代码是 0 而不是 1

  • 代码:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
    
  • 预期:EXIT 陷阱被调用,RC == 1
  • 实际:调用 EXIT 陷阱,RC == 0
  • 注意:使用 时set +e,RC == 1。当任何其他命令引发错误时,EXIT 陷阱将返回正确的 RC。
  • 编辑:有一篇关于这个主题的帖子有趣的评论表明这可能与正在使用的 Bash 版本有关。使用 Bash 4.3.11 测试此代码片段会得到 RC=1,因此效果更好。不幸的是,目前不可能在所有主机上升级 Bash(从 3.2.51 开始),因此我们必须想出其他解决方案。

任何人都可以解释这些行为吗?

搜索这些主题并不是很成功,考虑到有关 Bash 设置和陷阱的帖子数量,这相当令人惊讶。有一个论坛主题虽如此,但结论却并不令人满意。

答案1

man bash

  • set -u
    • 执行参数扩展时,将未设置的变量和特殊参数以外的参数"@"视为"*"错误。如果尝试对未设置的变量或参数进行扩展,shell 会打印一条错误消息,并且如果不是-i交互式的,则会以非零状态退出。

POSIX 规定,如果发生膨胀误差,非交互式 shell应退出当扩展与 shell 特殊内置相关联时(无论如何,这是一个经常被忽略的区别bash,所以也许是无关紧要的)或除此之外的任何其他实用程序。

  • Shell 错误的后果:
    • 一个膨胀误差是当 shell 扩展定义在单词扩展进行(例如,"${x!y}", 因为!不是有效的运算符);一个实现可能如果能够在标记化过程中而不是在扩展过程中检测到它们,则将它们视为语法错误。
    • [A]n 交互式 shell 应将诊断消息写入标准错误而不退出。

也来自man bash

  • trap ... ERR
    • 如果 sigspec 是, 命令精氨酸每当管道时执行(可能由一个简单的命令组成)、列表或复合命令返回非零退出状态,但满足以下条件:
      • 如果失败的命令是紧跟在whileoruntil关键字之后的命令列表的一部分,则不会执行 trap...
      • ...声明中测试的一部分if...
      • &&...在或列表中执行的命令的一部分,除了最后的或||之后的命令...&&||
      • ...管道中除最后一个命令之外的任何命令...
      • ...或者如果使用 来反转命令的返回值!
    • 这些条件与错误退出 -e选项。

请注意上面的陷阱就是对某些人的评价其他命令的返回。但是当一个膨胀误差发生时,没有运行命令来返回任何内容。在你的例子中,echo 永远不会发生- 因为当 shell 计算并扩展其参数时,它会遇到一个-unset 变量,该变量已由显式 shell 选项指定,以导致立即退出当前的脚本化 shell。

所以出口trap(如果有)将被执行,并且 shell 会退出并显示一条诊断消息,并且退出状态不是 0 - 正如它应该做的那样。

至于控制中心:0事情,我希望这是某种版本特定的错误 - 可能与两个触发器有关出口同时发生并且一方获得另一方的退出代码(这不应该发生)。无论如何,使用bash安装的最新二进制文件pacman

bash <<\IN
    printf "shell options:\t$-\n"
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
IN

我添加了第一行,这样您就可以看到 shell 的条件是脚本化 shell 的条件 - 它是不是交互的。输出是:

shell options:  hB
bash: line 4: UNSET_VAR: unbound variable
EXIT (rc: 1)

以下是一些相关注释最近的变更日志:

  • 修复了导致异步命令设置不正确的错误$?
  • 修复了导致生成错误消息的错误扩展错误命令中的 for行号错误。
  • 修复了导致信号情报信号退出不能trap用于异步子 shell 命令。
  • 修复了导致第二个及后续事件的中断处理问题信号情报被交互式 shell 忽略。
  • shell 在运行这些信号的处理程序时不再阻止信号的接收trap,并允许最多 trap递归运行处理程序 (在处理程序执行trap时运行处理程序)trap

我认为最相关的是最后一个或第一个,或者可能是两者的组合。 Atrap处理程序就其本质而言异步因为它的全部工作就是等待和处理异步信号。您可以使用 和 同时触发-eu两个$UNSET_VAR

所以也许你应该更新,但如果你喜欢自己,你会完全使用不同的 shell 来完成它。

答案2

(我使用的是 bash 4.2.53)。对于第 1 部分,bash 手册页只是说“错误消息将写入标准错误,并且非交互式 shell 将退出”。它并没有说将调用 ERR 陷阱,尽管我同意如果这样做的话它会很有用。

务实地说,如果您真正想要的是更干净地处理未定义的变量,一个可能的解决方案是将大部分代码放在一个函数中,然后在子 shell 中执行该函数并恢复返回代码和 stderr 输出。这是一个示例,其中“cmd()”是函数:

#!/bin/bash
trap 'rc=$?; echo "ERR at line ${LINENO} (rc: $rc)"; exit $rc' ERR
trap 'rc=$?; echo "EXIT (rc: $rc)"; exit $rc' EXIT
set -u
set -E # export trap to functions

cmd(){
 echo "args=$*"
 echo ${UNSET_VAR}
 echo hello
}
oops(){
 rc=$?
 echo "$@"
 return $rc # provoke ERR trap
}

exec 3>&1 # copy stdin to use in $()
if output=$(cmd "$@" 2>&1 >&3) # collect stderr, not stdout 
then    echo ok
else    oops "fail: $output"
fi

在我的狂欢上我得到

./script my stuff; echo "exit was $?"
args=my stuff
fail: ./script: line 9: UNSET_VAR: unbound variable
ERR at line 15 (rc: 1)
EXIT (rc: 1)
exit was 1

答案3

对于那些在寻找如何使用nounsetwith时发现这一点的人trap ERR

以下不会触发错误陷阱:

set -o nounset

trap 'trapped error on line $LINENO' ERR

echo $foo

预期产出

trapped error on line 5

实际产量

./script: line 5: foo: unbound variable

如果您想保持该nounset标志处于启用状态,一种解决方法是检查退出陷阱上的返回代码。

set -o nounset

trap '[[ $? != 0 ]] && echo caught error' EXIT

echo $foo

输出

./script: line 5: foo: unbound variable
caught error

相关内容