【问题标题】:How can one avoid expansion errors in functions killing the shell?如何避免在杀死 shell 的函数中出现扩展错误?
【发布时间】:2018-10-15 19:40:15
【问题描述】:

我今天在我的一个 shell 脚本中遇到了令人惊讶的行为。如下例所示:

test.sh

#!/bin/bash

do_it() {
  shopt -s failglob
  {
    rm killme.*
    echo "and then ..."
  } 2>/dev/null || echo "glob error"

  echo "life goes on ..."
}

do_it || echo "function failed"

原始脚本中的想法是,我希望允许特定命令发生全局扩展错误,以避免在没有参数时执行该命令,但 检测该错误并采取替代行动。我的期望是当killme.* 不匹配任何东西时,通过

执行上述脚本
./test.sh || echo "script failed"

会发射

glob error
life goes on ...

或许

function failed

。它没有(使用 Bash 4.2.46)。相反,它打印了

script failed

。在对问题进行故障排除时,我发现了一些更奇怪的事情:如果我通过消除函数来进一步简化脚本,行为就会发生变化。也就是说,考虑这个替代脚本:

test2.sh

#!/bin/bash

shopt -s failglob

{
  rm killme.*
  echo "and then ..."
} 2>/dev/null || echo "glob error"

echo "life goes on ..."

如果我通过

运行那个
./test2.sh || echo "script failed"

,它会打印出来

life goes on ...

当循环调用第一个脚本中的函数时,似乎还有其他一些奇怪的变化,但我还没有完全描述这一点。

问题

  1. 这是记录在案的行为吗?我对 Bash 手册的检查一直没有用。它指定发生“扩展错误”,这似乎很自然,这是一个 shell 错误,而不是命令中的错误,但如果有什么我应该能够预测的细节观察到的结果然后我错过了。

  2. 我可以通过在子 shell 中运行扩展来解决这个问题,但是有没有更轻量级的解决方法?我想我可以在未设置 failglob 的情况下提前执行扩展,然后测试结果,但这很混乱并且包含竞争条件。

【问题讨论】:

  • 总是存在竞争条件。如果有人在 shell 扩展通配符和命令实际运行之间删除了所有匹配项,则会出错。
  • 是的,@Barmar,但是在这两种情况下他们会得到不同的错误。特别是,shell 错误还是命令错误会有所不同。
  • 使用 'shopt -s nullglob' 可能会有更好的运气......我会在答案中加上上下文,因为 cmets 是有限的 :-)
  • 文档说在 POSIX 模式下,“如果发生参数扩展错误,非交互式 shell 将退出”;但我看不出你的两个脚本之间有什么区别会导致 test.sh 使用 POSIX 模式,而 test2.sh 不会。 (并且在本地测试,我发现将set -o posix 添加到test2.sh 或set +o posix 到test.sh,不会影响行为。)
  • 谢谢,@ruakh。我的不是 parameter 扩展错误,所以也许这可以解释为什么 bash 对它的处理与是否启用 POSIX 模式无关。然而,与 bash 不同,POSIX 似乎不区分不同类型的扩展错误。 bash 对我的test.sh 的行为似乎是POSIX-conformant。它与test2.sh 的行为,不是。我可能会针对我的特定问题只使用一个子shell,因为这似乎是最安全的。

标签: bash error-handling


【解决方案1】:

作为我上述评论的上下文...您可以使用带有 shell globbing 的变量来检查具有该模式的文件是否存在,如果存在则将它们全部删除,否则打印出错误消息。这使您无需依赖错误状态来触发“这不起作用”消息。

shopt -s nullglob
a=(killme.*)
if [[ -n $a ]]; then
 rm killme.* > /dev/null 2>&1
 echo "life goes on ...
else
  echo "glob error"
fi 

...我不能(目前)提供任何关于函数失败原因的见解,除了反省函数调用可能作为子shell 执行。

编辑: 我在bashsubst.c 中找到了这个gem ...看起来我们跳到了顶层shell上下文,丢弃了所有当前上下文,并设置了失败代码:

  else if (fail_glob_expansion != 0)
    {
      last_command_exit_value = EXECUTION_FAILURE;
      report_error (_("no match: %s"), tlist->word->word);
      exp_jump_to_top_level (DISCARD);
    }

...在这种情况下,我怀疑 bash 将 doit || echo "function failed" 解析为 single 命令,导致整个位失败。由于bash 返回脚本中最后一个命令的退出代码,这就解释了为什么您看到脚本“失败”(即./my_script.sh || echo "script failed" 正在打印“脚本失败”)。

您可以看到,如果您将echo "exit_code: $?" 添加为脚本的最后一行,它将打印一个非零代码(即失败),但您的脚本将返回一个成功代码:

[eurythmia@localhost ~]$ cat ./test_script.sh 
#!/bin/bash

do_it() {
  shopt -s failglob
  {
    rm killme.*
    echo "and then ..."
  } >/dev/null 2>&1 || echo "glob error"

  echo "life goes on ..."
} 

do_it || echo "function failed"
echo "exit code: $?"
[eurythmia@localhost ~]$ ./test_script.sh || echo "I failed"
exit code: 1
[eurythmia@localhost ~]$ echo $?
0
[eurythmia@localhost ~]$ 

我想这一切都归结为 bash 的解析方式,以及它认为什么是“命令”(为了我自己的启迪,我将进一步研究)。同时,我不会在函数中依赖shopt -s failglob。可行的替代方案包括在脚本的根级别使用shopt -s failglob,或者在函数内部工作时坚持使用标准测试运算符(作为 bash 内置函数实现)。

【讨论】:

  • 不,函数调用不会作为子shell 隐式执行(至少在 OP 的情况下;有很多方法可以做到这一点,但它们涉及管道、命令替换或其他操作自己创建一个子shell)。
  • 我不认为函数调用会隐式发生在子shell中,但我没有时间检查是否是这种情况。谢谢@CharlesDuffy :-)
  • 谢谢,但正如我在问题中提到的,我不想进行预测试。事实上,关键是我想确保 如果 glob 不匹配任何内容,该命令不会运行。如果在 glob 展开后匹配的文件消失,则命令失败是可以的,但如果 glob 展开为空,该命令将执行错误的操作。在这种情况下,让 glob 无法扩展的风险较小(尽管并非没有风险)。
  • @JohnBollinger 我回去重新阅读了这个问题,显然我误解了那一点。我可以确认我在 bash 4.4.23(1) 中看到的行为与您相同。删除到/dev/null 的重定向表明它无法与您期望的那样匹配全局,但失败的结果是......好吧,令人费解。我建议使用 bash 提出一个错误(您可以使用bashbug 命令行工具来执行此操作),因为这看起来不应该发生。明天我可能会更深入地研究这个问题,因为这是一个有趣的问题:-)
【解决方案2】:
  1. 这是记录在案的行为吗?我对 Bash 手册的检查一直没有用。它指定发生“扩展错误”,并且它 似乎很自然,这是一个 shell 错误,而不是命令中的错误, 但如果有什么我应该能够预测 观察结果的详细信息然后我错过了。

Bash 手册的当前版本和我发现问题所在版本的手册似乎都没有记录路径名扩展导致扩展错误时应该预期的行为。当参数扩展过程中发生错误时,某些版本的手册会记录行为,并且显然在某些 Bash 版本之间会有所不同,具体取决于 Bash 是否在 POSIX 模式下运行。

然而,POSIX 本身并不区分不同种类的扩展错误。 specifies 非交互式 shell 中的(所有)扩展错误会导致 shell 以诊断方式终止。这是我的 test.sh 表现出的行为,但它与我的 test2.sh 表现出的行为冲突。

由于 Bash 并未声称完全符合 POSIX,尤其是在不以 POSIX 兼容模式运行时,与 POSIX 的差异不能被视为错误,但两个脚本的行为之间令人惊讶且未记录的不一致肯定是对我来说似乎有问题,所以我已经提交了一个关于它的问题。

  1. 我可以通过在子shell 中运行扩展来解决这个问题,但是有没有更轻量级的解决方法?我想我可以执行 提前扩展,未设置 failglob,并测试结果,但是 这很混乱,并且包含竞争条件。

由于 POSIX shell 预计会因扩展错误而终止,而 Bash 至少在某些情况下会这样做,因此从扩展错误中恢复的唯一安全方法似乎是导致错误发生在子 shell 中。

在我的特定情况下,我想确保命令不会以零参数或未扩展的 glob 作为参数运行,因为在这些情况下它可能会做错事。但是,如果该命令使用指定文件的参数运行,这些文件在扩展后但在命令尝试对参数进行操作之前消失,则可以。最简单、最可靠的完成方式似乎是在设置了failglob 选项的子shell 中运行命令。

也可以在未设置failglob 的情况下预先扩展glob,测试它是否为空,然后使用预先扩展的结果而不是再次扩展glob。就我的目的而言,这似乎有点矫枉过正,所以我选择使用 subshel​​l。

更新

正如@eurythmia 在他的回答中指出的那样,Bash 的行为比我想象的还要奇怪。在test1.sh中,展开错误不会导致整个脚本终止;相反,它会导致包含do_it 调用的管道 立即失败,完全绕过对函数调用命令本身成功或失败的任何考虑,因此不执行echo 命令.出于所有意图和目的,do_it 调用本身既不会成功也不会失败,这充其量是非常奇特的。

然而,这并没有改变这个答案中提出的结论:这种行为绝对没有记录在 Bash 中,而且它与 POSIX 不一致。最简单安全的替代方法是在子shell 中隔离failglob 选项的任何使用,但如果由于某种原因这不切实际,那么有一些变通方法。

【讨论】:

  • 您在 test.sh 中看到的行为不是 shell 终止。正如下面的答案中所解释的,bash 正在跳转到顶级 shell 上下文并且使 command 失败。您使用的命令是复合命令,因此 shell 脚本会返回错误。之后简单地添加另一个命令将显示 shell 没有终止,它只是使命令失败并继续执行下一个命令。
  • 这个答案正在 Meta 上讨论:meta.stackoverflow.com/questions/375620/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多