有没有人尝试编写一个修复 errexit 的 shell(`set -e`)?

有没有人尝试编写一个修复 errexit 的 shell(`set -e`)?

errexit ( set -e) 通常被建议作为一种使简单脚本更加健壮的方法。然而,任何见过它在更复杂的脚本(尤其是函数)中的行为的人通常都会认为这是一个可怕的陷阱。子 shell 也存在非常相似的问题。举个例子,改编自https://stackoverflow.com/questions/29926013/exit-subshel​​l-on-error

(
set -o errexit
false
true
) && echo "OK" || echo "FAILED";

这里的陷阱是 shell 显示“OK”,而不是“FAILED”。[*]

虽然在特定情况下的行为set -e在历史上以不幸的方式有所不同,但现代发行版上可用的 shell 声称遵循 POSIX shell 标准,并且测试显示以下所有项都有相同的行为(“OK”):

  • bash-4.4.19-2.fc28.x86_64(软呢帽)
  • busybox-1.26.2-3.fc27.x86_64(软呢帽)
  • dash 0.5.8-2.4(Debian 9)
  • zsh 5.3.1-4+b2(Debian 9)
  • posh 0.12.6+b1(Debian 9)
  • ksh 93u+20120801-3.1(Debian 9)。

问题

  1. 是否有任何技术原因导致您无法编写具有与 POSIX 类似功能的 shell sh,除了它打印FAILED上述代码之外?

    我希望它也能改变,以便返回 false 的 set -e顶级表达式被认为是致命的。&&https://serverfault.com/a/847016/133475 希望它能有一些更好的习惯用法与 grep 一起使用。

    set -e
    
    # print matching lines, but it's not an error if there are no matches in the file
    (
    set +e
    grep pattern file.txt
    RET=$?
    [[ $RET == 0 || $RET == 1 ]] || exit $RET
    )
    

    我想我还假设算术语句(let内置)将以某种方式重新定义,以避免隐式地将其数值强制为真/假退出状态。

  2. 有没有一个 shell 的例子可以做到这一点?

    我不介意查看与 Bourne 语法不同的内容。在编写简短的“粘合”脚本时,我对紧凑的东西感兴趣,但它也有一个紧凑的策略来检测错误。我不喜欢用作&&语句分隔符,如果这意味着意外省略语句分隔符将导致错误被默默忽略。


[*] 编辑。这也许不是一个完美的例子。讨论表明更好的例子是将set -o errexit子 shell 移到上面。

(false; echo foo) || echo bar; echo \ $?AFAICT 这与表中的测试用例非常相似这里,它表示它将在原始 Bourne shell 及其大多数后代上显示“foo”(然后是“0”)。例外情况是“hist.ash”和 bash 1.14.7。

当您set -e在子 shell 中添加 - (set -e; false; echo foo) || echo bar; echo \ $?- 时,有两个额外的例外,“SVR4 sh sun5.10”和 dash-0.3.4。

就我的问题的精神而言,我认为进入set -e子外壳会分散注意力。我主要感兴趣的是在set -e脚本顶部使用的习惯用法,它也适用于子 shell(这已经是 POSIX 行为)。

Jörg Schilling 表示,对于我在这个问题中使用的示例,原始 Unix 中的 Bourne shell 会打印“FAILED”,他已将此 shell 移植到 POSIX,如下所示希利工具中的“osh”,并且该结果已于 2018 年 6 月 11 日发布时得到验证。也许这是基于 1)set -e位于子 shell 内部和 2) “SVR4 sh sun5.10”。 “osh”“基于 OpenSolaris 源,因此基于 SVR4 和 SVID3”。或者也许在&& echo "OK"subshel​​l 和 之间添加会导致一些可怕的额外差异|| echo "FAILED"

我认为狡猾的外壳不能回答我的问题。您可以对函数使用子 shell(使用(代替{在子 shell 中运行函数),并以 . 启动每个子 shell set -e。但是,使用子 shell 会带来无法修改全局变量的限制。至少,我还没有看到有人捍卫它作为通用编码风格。

答案1

POSIX标准(参见-e)比 Bash 手册中有关errexitshell 选项的内容更清楚。

启用此选项后,当任何命令失败时(由于 Shell 错误的后果中列出的任何原因或返回大于零的退出状态),shell 应立即退出,就像执行 exit 特殊内置实用程序一样不带任何参数,但以下情况除外:

  1. 多命令管道中任何单个命令的失败都不应导致 shell 退出。仅考虑管道本身的故障。

  2. 当执行 while、until、if 或 elif 保留字(以 ! 开头的管道)后面的复合列表时,应忽略 -e 设置。保留字,或 AND-OR 列表中除最后一个以外的任何命令。

  3. 如果子 shell 命令以外的复合命令的退出状态是在 -e 被忽略时失败的结果,则 -e 不适用于该命令。

此要求分别适用于 shell 环境和每个子 shell 环境。例如,在:

set -e; (false; echo one) | cat; echo two

false 命令导致子 shell 退出而不执行 echo one;然而,执行 echo 2 是因为管道的退出状态 (false; echo one) |猫为零。

显然,示例代码等效于以下伪代码。

( LIST; ) && COMMAND || COMMAND

的退出状态列表 为零,因为:

  • shellerrexit选项被忽略。

  • 返回状态是指定的最后一个命令的退出状态列表, 这里true

因此,执行 AND 列表的第二部分:echo "OK"

答案2

不是“有人尝试过写……”这个问题的答案,而是关于如何完成它的一些想法。好奇是否有人可以提供反馈。

当前脚本解决方案(Python、JavaScript 和较旧的 Perl 等)中的常见方法是使用 try ... catch 块。预期行为是任何错误都会触发异常。此行为与 $? 中捕获错误的 *sh 行为冲突。显然,“-e”未能以大多数开发人员期望的方式正确工作。无需在这里详细说明,因为这已经被广泛记录。

两种可能的方法:

  • 创建更好的set -e:例如,set -ocatcherr具有更好的错误处理能力。
  • 添加新try { list ; }命令,遵循现代脚本解决方案

'-ofailerr` 可以定义如下:

  • 如果任何命令在控制流之外失败(if/while/until),则当前执行上下文应返回 1(如果这是顶级块,则退出)。此更改将有效地迫使开发人员对任何失败的命令提供错误处理,或者明确标记带有可以忽略的错误的命令。
  • 适用于简单的脚本,有效地在未处理的错误上中止。

方法try { list ; }类似,只是语法不同

例子:

function foo {
    cat $* > a.txt
    wc -l a.txt
}

set -oerrfail
foo /non/existing/file    # Will  fail, without executing the "wc -l"

if foo /non/existing/file ; then
   ...
else
   # This is the "catch" block
fi

使用 try 块:

try {
    foo /non/existing/file
    ...
} || { catch-block }

实际上,该 try 相当于“set -oerrofail”,在“if !”下运行命令列表。 { ... } ;然后 { catch 块 }

相关内容