简体   繁体   English

如何在杀死shell的函数中避免扩展错误?

[英]How can one avoid expansion errors in functions killing the shell?

I ran into a surprising behavior in one of my shell scripts today. 我今天在我的一个shell脚本中遇到了令人惊讶的行为。 It is demonstrated by the following example: 以下示例演示了这一点:

test.sh 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"

The idea in the original script was that I wanted to allow glob expansion errors to occur for a particular command so as to avoid executing that one command when there were no arguments, yet detect that error and take alternative action. 原始脚本中的想法是,我希望允许特定命令发生glob扩展错误,以避免在没有参数时执行该命令,但检测到该错误并采取替代操作。 My expectation was that when killme.* did not match anything, executing the above script via 我的期望是,当killme.*与任何东西都不匹配时,通过执行上述脚本

./test.sh || echo "script failed"

would emit 会发光

glob error
life goes on ...

or maybe 或者可能

function failed

. It didn't (with Bash 4.2.46). 它没有(与Bash 4.2.46)。 Instead it printed 相反它打印

script failed

. While troubleshooting the issue, I discovered something even more curious: if I simplify the script further by eliminating the function, the behavior changes. 在解决问题时,我发现了一些更奇怪的东西:如果通过消除函数进一步简化脚本,行为会发生变化。 That is, consider this alternative script: 也就是说,考虑这个替代脚本:

test2.sh test2.sh

#!/bin/bash

shopt -s failglob

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

echo "life goes on ..."

If I run that via 如果我经过那个

./test2.sh || echo "script failed"

, it prints ,它打印

life goes on ...

There seem to be some other weird variations when a function like the one in the first script is called in a loop, but I haven't fully characterized that. 当在循环中调用第一个脚本中的函数时,似乎还有一些其他奇怪的变体,但我没有完全表征它。

Questions : 问题

  1. Is this documented behavior? 这是记录在案的行为? My examination of the Bash manual has been unavailing. 我对Bash手册的检查一直没有。 It specifies that an "expansion error" occurs, and it seems natural that that's a shell error, not an error in the command, but if there's anything by which I should be able to predict the details of the observed results then I've missed it. 它指定发生“扩展错误”,这似乎是一个shell错误,而不是命令中的错误,但是如果有什么我应该能够预测观察结果的详细信息那么我就错过了它。

  2. I can solve the issue by running the expansion in a subshell, but is there any lighter weight workaround? 我可以通过在子shell中运行扩展来解决问题,但是有没有更轻量级的解决方法? I guess I could perform the expansion in advance, with failglob unset, and test the result, but that's messy and contains a race condition. 我想我可以提前执行扩展, failglob设置,并测试结果,但这很麻烦并且包含竞争条件。

As the context for my above comment ... you can use a variable with shell globbing to check if a file with the pattern exists, and remove them all if they do, else print out the error message. 作为我上面评论的上下文...您可以使用带有shell globbing的变量来检查是否存在具有该模式的文件,如果它们存在则将其全部删除,否则打印出错误消息。 This leaves you without depending on an error state to trigger the "this didn't work" message. 这使您无需依赖错误状态来触发“此操作无效”消息。

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

... I can't (at present) offer any insight into why the function is failing, other than the rumination that perhaps function calls are executed as subshells. ...我不能(目前)提供任何有关函数失败原因的见解,除了可能函数调用作为子shell执行的反思。

Edit : I found this gem in bash 's subst.c ... it looks like we jump up to the top shell context, discarding all current context, and set the failure code: 编辑 :我在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);
    }

... in this case, I suspect that bash is parsing doit || echo "function failed" ...在这种情况下,我怀疑bash正在解析doit || echo "function failed" doit || echo "function failed" as a single command, causing the whole bit to fail. doit || echo "function failed"作为单个命令,导致整个位失败。 Since bash returns the exit code of the last command in a script, that explains why you're seeing your script 'fail' (ie ./my_script.sh || echo "script failed" is printing "script failed"). 由于bash返回脚本中最后一个命令的退出代码,这解释了为什么你看到你的脚本“失败”(即./my_script.sh || echo "script failed"正在打印“脚本失败”)。

You can see that if you add echo "exit_code: $?" 你可以看到,如果你添加echo "exit_code: $?" as the last line of your script it will print a non-zero code (ie failure), but your script will return a success 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 ~]$ 

I guess this all comes down to how bash parses, and what it considers to be a 'command' (something that I'm going to look further into, for my own edification). 我想这一切都归结为bash如何解析,以及它认为是什么“命令”(我将进一步研究的内容,对于我自己的启发)。 In the meantime, I would not depend on shopt -s failglob from within a function. 与此同时,我不会依赖于函数中的shopt -s failglob Viable alternatives include using shopt -s failglob from the root level of the script, or sticking to the standard test operators (which are implemented as bash builtins) when working inside functions. 可行的替代方案包括在脚本的根级使用shopt -s failglob ,或者在内部函数中使用时坚持使用标准测试运算符(实现为bash内置函数)。

  1. Is this documented behavior? 这是记录在案的行为? My examination of the Bash manual has been unavailing. 我对Bash手册的检查一直没有。 It specifies that an "expansion error" occurs, and it seems natural that that's a shell error, not an error in the command, but if there's anything by which I should be able to predict the details of the observed results then I've missed it. 它指定发生“扩展错误”,这似乎是一个shell错误,而不是命令中的错误,但是如果有什么我应该能够预测观察结果的详细信息那么我就错过了它。

Neither the current version of the Bash manual nor the manual for the version on which I discovered the problem seems to document the behavior that should be expected when pathname expansion results in an expansion error. 当前版本的Bash手册和我发现问题的版本的手册似乎都没有记录路径名扩展导致扩展错误时应该预期的行为。 Some versions of the manual do document behavior when an error occurs during parameter expansion, and apparently that varies between some Bash versions and depending on whether Bash is running in POSIX mode. 某些版本的手册会在参数扩展期间发生错误时记录行为,并且显然在某些Bash版本之间会有所不同,具体取决于Bash是否在POSIX模式下运行。

POSIX itself, however, does not distinguish between different varieties of expansion error. 但是,POSIX本身并不区分不同种类的扩展错误。 It specifies that (all) expansion errors in non-interactive shells cause the shell to terminate with a diagnostic. 指定非交互式shell中的(所有)扩展错误导致shell以诊断终止。 This is the behavior exhibited by my test.sh , but it conflicts with the behavior exhibited by my test2.sh . 这是我的test.sh所展示的行为,但它与我的test2.sh所展示的行为相冲突。

Inasmuch as Bash does not claim to be fully POSIX-conformant, especially when not running in POSIX-compatibility mode, the discrepancy with POSIX cannot be considered a bug, but the surprising and undocumented inconsistency between the behavior of the two scripts certainly seems buggy to me, so I have filed an issue about it. 因为Bash没有声称完全符合POSIX,特别是在没有在POSIX兼容模式下运行时,与POSIX的差异不能被认为是一个错误,但两个脚本的行为之间的令人惊讶和未记录的不一致似乎肯定是错误的我,所以我提出了一个问题。

  1. I can solve the issue by running the expansion in a subshell, but is there any lighter weight workaround? 我可以通过在子shell中运行扩展来解决问题,但是有没有更轻量级的解决方法? I guess I could perform the expansion in advance, with failglob unset, and test the result, but that's messy and contains a race condition. 我想我可以提前执行扩展,failglob未设置,并测试结果,但这很麻烦并且包含竞争条件。

Inasmuch as the POSIX shell is expected to terminate on expansion errors, and Bash does this in at least some contexts, the only safe way to enable recovery from expansion errors seems to be to cause the error to occur in a subshell. 由于POSIX shell预计会在扩展错误时终止,而Bash至少在某些情况下会这样做,因此从扩展错误中恢复的唯一安全方法似乎是在子shell中发生错误。

In my particular case, I want to ensure that the command does not run with zero arguments or with an unexpanded glob as an argument, as it may do the wrong thing in these cases. 在我的特定情况下,我想确保命令不以零参数或未扩展的glob作为参数运行,因为它可能在这些情况下做错了。 It is OK, though, if the command runs with arguments designating files that disappear after expansion but before the command tries to operate on the arguments. 但是,如果命令运行时参数指定文件在扩展后但在命令尝试操作参数之前消失,则可以。 The simplest and most reliable way to accomplish seems to be to just run the command in a subshell in which the failglob option is set. 最简单和最可靠的方法似乎是在子shell中运行命令,其中设置了failglob选项。

It would also be possible to pre-expand the glob with failglob unset, test whether it is empty, and then use the pre-expanded result instead of expanding the glob again. 也可以使用failglob unset预扩展glob,测试它是否为空,然后使用预扩展结果而不是再次展开glob。 That seems like overkill for my purposes, so I have opted to go with the subshell. 对于我的目的来说,这似乎有些过分,所以我选择使用子shell。

Update 更新

As @eurythmia pointed out in his answer, Bash's behavior is even stranger than I thought. 正如@eurythmia在他的回答中指出的那样,Bash的行为甚至比我想象的还要奇怪。 In test1.sh , the expansion error does not cause the whole script to terminate; test1.sh中 ,扩展错误不会导致整个脚本终止; rather, it causes the pipeline containing the do_it call to fail immediately, altogether bypassing any consideration of the success or failure of the function call command itself, and for that reason not executing the echo command. 相反,它会导致包含do_it调用的管道立即失败,完全绕过对函数调用命令本身成功或失败的任何考虑,并因此不执行echo命令。 For all intents and purposes, the do_it call itself neither succeeds nor fails, which is highly peculiar at best. 对于所有意图和目的, do_it调用本身既不成功也不失败,这在很多情况下是非常特殊的。

That does not, however, change the conclusions presented in this answer: the behavior is definitely not documented for Bash, and it is inconsistent with POSIX. 但是,这并没有改变这个答案中提出的结论:Bash的行为绝对没有记录,而且它与POSIX不一致。 The simplest safe alternative is to isolate any use of the failglob option inside a subshell, but if that's impractical for some reason then there are workarounds. 最简单的安全替代方法是在子shell中隔离failglob选项的任何使用,但如果由于某种原因这是不切实际的,那么就有解决方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM