errexit ( set -e
) 通常被建议作为一种使简单脚本更加健壮的方法。然而,任何见过它在更复杂的脚本(尤其是函数)中的行为的人通常都会认为这是一个可怕的陷阱。子 shell 也存在非常相似的问题。举个例子,改编自https://stackoverflow.com/questions/29926013/exit-subshell-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)。
问题
是否有任何技术原因导致您无法编写具有与 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
内置)将以某种方式重新定义,以避免隐式地将其数值强制为真/假退出状态。有没有一个 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"
subshell 和 之间添加会导致一些可怕的额外差异|| echo "FAILED"
。
我认为狡猾的外壳不能回答我的问题。您可以对函数使用子 shell(使用(
代替{
在子 shell 中运行函数),并以 . 启动每个子 shell set -e
。但是,使用子 shell 会带来无法修改全局变量的限制。至少,我还没有看到有人捍卫它作为通用编码风格。
答案1
这POSIX标准(参见-e)比 Bash 手册中有关errexit
shell 选项的内容更清楚。
启用此选项后,当任何命令失败时(由于 Shell 错误的后果中列出的任何原因或返回大于零的退出状态),shell 应立即退出,就像执行 exit 特殊内置实用程序一样不带任何参数,但以下情况除外:
多命令管道中任何单个命令的失败都不应导致 shell 退出。仅考虑管道本身的故障。
当执行 while、until、if 或 elif 保留字(以 ! 开头的管道)后面的复合列表时,应忽略 -e 设置。保留字,或 AND-OR 列表中除最后一个以外的任何命令。
如果子 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
的退出状态列表 为零,因为:
shell
errexit
选项被忽略。返回状态是指定的最后一个命令的退出状态列表, 这里
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 块 }