【问题标题】:Why is bash errexit not behaving as expected in function calls?为什么 bash errexit 在函数调用中的行为不符合预期?
【发布时间】:2013-11-05 12:31:47
【问题描述】:

在 bash 手册页中,它指出:

如果管道(可能由单个简单命令组成),则立即退出,
括在括号中的子shell命令,或执行为的命令之一 用大括号括起来的命令列表的一部分...

所以我假设一个函数应该被认为是一个用大括号括起来的命令列表。但是,如果对函数调用应用条件,则 errexit 不再保留在函数体内,它会在返回之前执行整个命令列表。即使您在函数内显式创建子shell 并为该子shell 启用了errexit,命令列表中的所有命令也会被执行。这是一个演示该问题的简单示例:

function a() { b ; c ; d ; e ; }
function ap() { { b ; c ; d ; e ; } ; }
function as() { ( set -e ; b ; c ; d ; e ) ; }
function b() { false ; }
function c() { false ; }
function d() { false ; }
function e() { false ; }

( set -Eex ; a )
+ a
+ b
+ false

( set -Eex ; ap )
+ ap
+ b
+ false

( set -Eex ; as )
+ as
+ set -e
+ b
+ false

现在,如果我对他们每个人都应用一个条件......

( set -Eex ; a || false )
+ a
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false

( set -Eex ; ap || false )
+ ap
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false

( set -Eex ; as )
+ as
+ set -e
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false

【问题讨论】:

标签: bash


【解决方案1】:

你开始引用manual,但随后你删掉了解释这种行为的部分,即下一句

-e 如果管道(可能包含单个简单命令、括在括号中的子shell 命令或作为括号括起来的命令列表的一部分执行的命令之一)返回非零状态,则立即退出。 shell 不退出如果失败的命令是紧跟在whileuntil 关键字之后的命令列表的一部分,是if 语句中测试的一部分,部分在&&|| 列表中执行的任何命令,除了最后一个&&|| 之后的命令,管道中除最后一个之外的任何命令,或者如果命令的返回状态正在反转!

【讨论】:

  • 好的,那么有没有办法在子shell 中显式地使某些事情失败,而不会在命令列表中每个命令的末尾添加||
  • 在你的引用中没有任何关于函数的内容。
  • @Olleg,这也让我感到困惑。我认为这是因为该函数作为带有|| 的语句的一部分被调用,因此它禁用了set -e
  • 它确实使具有可能失败的命令的函数完全一文不值。您必须改为执行一堆$? 检查函数以确保并在正确的位置退出。否则它将无法在其他管道或 if 条件中工作,或者在失败时需要它评估为 false 的任何东西。如果你问我,这很烦人。
  • 仅供参考,这种忽略 shell 选项的令人沮丧的行为也适用于 set -E 和 ERR 陷阱。
【解决方案2】:

bug-bash mailing list Eric Blake 对函数进行了更明确的解释:

简短回答:历史兼容性。

...

确实,POSIX 规定的正确行为(即,'set -e' 是 在整个 f() 的持续时间内完全被忽略,因为 f 在忽略“set -e”的上下文中调用)不直观。但 它是标准化的,所以我们必须忍受它。

还有一些关于set -e 是否可以被利用来实现想要的行为的说法:

因为一旦您处于忽略“set -e”的上下文中,历史 行为是没有其他方法可以重新打开它,因为 忽略上下文中的整个代码体。就这样完成了 30 几年前,在真正考虑 shell 函数之前,我们 坚持那个糟糕的设计决定。

【讨论】:

  • 多么令人失望的答案。也许bash 应该提供一个“非标准”选项来覆盖这个标准但令人惊讶的错误行为。
  • 但它是标准化的,所以我们必须忍受它。我会猜测并说这是 POSIX 标准规定的。不能轻易改变,否则为什么要遵守标准?
  • 你不必忍受它。只是不要遵循标准。
【解决方案3】:

不是原始问题的答案,而是潜在问题的解决方法:设置错误陷阱

function on_error() {
    echo "error happened!"
}
trap on_error ERR

echo "OK so far"
false
echo "this line should not execute"

行为本身的原因在其他答案中得到了正确解释(基本上它是根据手册和 POSIX 预期的 bash 行为):https://stackoverflow.com/a/19789651/1091436

【讨论】:

  • 在我的测试中,一旦命令是条件语句的一部分,ERR 陷阱就会从调用堆栈中的所有函数中完全剥离(就像 OP 的 -e 一样),所以我我不确定你到底是什么意思。你能澄清一下你的意思吗?
  • @jrahhali 我不确定你的问题是什么,但是如果我执行上面的脚本,我会得到预期的顺序:写“到目前为止还好”,然后写“发生错误!” ,然后退出。
  • 奇数。当我将其作为脚本运行时,会为我打印“此行不应执行”。运行脚本时,"$-" 的输出是什么?
  • @jrahhali 当然,您还应该将 bash 设置为退出错误。否则false 不会在脚本中做任何事情。 set -e 在文件的开头
  • 是的,使用set -e 将使*您的脚本工作。但我不确定 ERR 陷阱如何解决 OP 的问题。
【解决方案4】:

不是答案,但您可以通过定义此辅助函数来解决这种反直觉的行为:

fixerrexit() { ( eval "expr '$-' : '.*e' >/dev/null && set -e; $*"; ); }

然后通过fixerrexit调用你的函数。

示例:

f1()
{
  mv unimportant-0.txt important.txt
  rm unimportant-*.txt
}

set -e

if fixerrexit f1
then
  echo here is the important file: important.txt
  echo unimportant files are deleted
fi

如果外部上下文打开了errexit,那么fixerrexitf1() 内部也会打开errexit,因此您不必担心发生故障后执行的命令。

唯一的缺点是您无法设置变量,因为它在子外壳中运行 f1

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多