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

dvd*_*sng 31 scripting bash parameter shell-script trap

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

1)set -u不触发 ERR 陷阱

  • 代码:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
    
    Run Code Online (Sandbox Code Playgroud)
  • 预期: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}
    
    Run Code Online (Sandbox Code Playgroud)
  • 预期:退出陷阱被调用,RC == 1
  • 实际:调用退出陷阱,RC == 0
  • 注意:当使用 时set +e,RC == 1。当任何其他命令抛出错误时,EXIT 陷阱返回正确的 RC。
  • 编辑:有一个关于这个主题的 SO post 有一个有趣的评论,表明这可能与正在使用的 Bash 版本有关。使用 Bash 4.3.11 测试此代码段的结果是 RC=1,因此更好。不幸的是,目前无法在所有主机上升级 Bash(从 3.2.51 开始),因此我们必须想出一些其他解决方案。

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

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

mik*_*erv 18

来自man bash

  • set -u
    • 治疗尚未设定的变量和除特殊参数的其他参数"@""*"执行参数扩展时为错误。如果在未设置的变量或参数上尝试扩展,shell 会打印一条错误消息,如果不是-i交互式的,则以非零状态退出。

POSIX 指出,在发生扩展错误的情况下,当扩展与 shell 特殊内置(这是一个经常忽略的区别,因此可能无关紧要)或除此之外的任何其他实用程序关联时,非交互式 shell应退出.bash

  • 外壳错误的后果
    • 一个膨胀误差是一个当所定义的外壳膨胀时出现字展开被执行(例如,"${x!y}"因为!不是有效的操作者) ; 如果能够在标记化期间而不是在扩展期间检测到它们,则实现可以将它们视为语法错误。
    • [A]n 交互式 shell 应在不退出的情况下将诊断消息写入标准错误。

也来自man bash

  • trap ... ERR
    • 如果 sigspec 是ERR,则只要管道(可能由单个简单命令组成)、列表或复合命令返回非零退出状态,就会执行命令arg,但须符合以下条件:
      • ERR如果失败的命令是紧跟在命令列表的一部分,不执行陷阱一个whileuntil关键字...
      • ...if声明中测试的一部分...
      • ...在执行的命令的一部分&&||在最终除命令列表&&||...
      • ...管道中的任何命令,但最后一个...
      • ...或者如果命令的返回值正在使用!.
    • 这些是errexit -e选项所遵循的相同条件。

注意上面的ERR陷阱是关于评估其他命令的返回。但是当发生扩展错误时,没有命令 run 返回任何内容。在您的示例中,echo 永远不会发生- 因为当 shell 评估和扩展其参数时,它遇到了一个-unset 变量,该变量已由显式 shell 选项指定,以导致立即退出当前的脚本 shell。

因此EXIT陷阱(如果有)被执行,并且 shell 以诊断消息和非 0 的退出状态退出——正如它应该做的那样。

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

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

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

shell options:  hB
bash: line 4: UNSET_VAR: unbound variable
EXIT (rc: 1)
Run Code Online (Sandbox Code Playgroud)

以下是最近更新日志中的一些相关说明:

  • 修复了导致异步命令设置不$?正确的错误。
  • 修正了由产生引起错误消息扩展的错误for的命令有错误的行号。
  • 修复了导致SIGINTSIGQUITtrap在异步子 shell 命令中不可用的错误。
  • 修复了导致交互式 shell 忽略第二个和后续SIGINT 的中断处理问题。
  • trap为这些信号运行处理程序时,外壳不再阻止接收信号,并允许大多数 trap处理程序递归 运行(在trap处理trap程序执行时运行处理程序)

我认为最相关的要么是最后一个,要么是第一个——或者可能是两者的结合。一个trap处理程序是其本质异步的,因为它的整个工作就是等待和处理异步信号。你用-eu和同时触发两个$UNSET_VAR

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


meu*_*euh 12

(我使用的是 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
Run Code Online (Sandbox Code Playgroud)

在我的 bash 我得到

./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
Run Code Online (Sandbox Code Playgroud)