我今天在一个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 ...
当像第一个脚本中的函数那样被循环调用时,似乎还有其他怪异的变化,但是我还没有完全描述它。
问题:
此记录的行为吗?我对Bash手册的检查一直无法进行。它指定发生“扩展错误”,这很自然是一个 shell 错误,不是命令中的错误,但是如果有什么我可以预测的详细信息,观察结果,然后我就错过了。
我可以通过在子外壳中运行扩展程序来解决此问题,但是是否有更轻巧的解决方法?我想我可以在未设置failglob
的情况下预先执行扩展并测试结果,但这很杂乱,并且包含竞争条件。
答案 0 :(得分:3)
作为我上面的评论的上下文,您可以使用带有外壳遍历的变量来检查是否存在带有模式的文件,如果存在则将其全部删除,否则打印出错误消息。这样一来,您就不必依靠错误状态来触发“此操作无效”消息。
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执行的),我(目前)无法提供有关函数失败原因的任何见解。
修改:
我在bash
的{{1}}中发现了这个gem ...看来我们跳到了顶级shell上下文,丢弃了所有当前上下文,并设置了失败代码:
subst.c
...在这种情况下,我怀疑bash将 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);
}
解析为 single 命令,从而导致整个操作失败。由于doit || echo "function failed"
返回脚本中最后一个命令的退出代码,因此可以解释为什么看到脚本“失败”(即bash
打印“脚本失败”)。
您会看到,如果将./my_script.sh || echo "script failed"
添加为脚本的最后一行,它将打印出非零代码(即失败),但是您的脚本将返回成功代码:
echo "exit_code: $?"
我想这全都归结为bash的解析方式以及它认为是“命令”的内容(出于我个人的兴趣,我将对此进行深入研究)。同时,我不会依赖函数内部的[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 ~]$
。可行的选择包括从脚本的根目录使用shopt -s failglob
或在函数内部工作时遵循标准的测试运算符(实现为bash内置函数)。
答案 1 :(得分:1)
- 这是记录的行为吗?我对Bash手册的检查一直无法进行。它指定发生“扩展错误”,并且 看起来很自然,那是shell错误,而不是命令错误, 但是如果有什么我应该能够预测的 观察结果的详细信息,然后我就错过了。
当前的Bash手册版本或我发现问题的版本的手册似乎都没有记录路径名扩展导致扩展错误时应预期的行为。在 parameter 扩展过程中发生错误时,该手册的某些版本会记录文档的行为,显然,某些Bash版本之间的错误会有所不同,具体取决于Bash是否以POSIX模式运行。
但是,POSIX本身无法区分不同类型的扩展错误。 specifies指出,非交互式shell中的(全部)扩展错误会导致shell终止并进行诊断。这是我的 test.sh 表现出的行为,但与我的 test2.sh 表现出的行为相冲突。
由于Bash并不声称完全符合POSIX,特别是当未在POSIX兼容模式下运行时,与POSIX的差异不能被认为是一个错误,但是这两个脚本的行为之间令人惊讶且未记录的不一致肯定对我来说似乎是越野车,所以我提出了一个问题。
- 我可以通过在子外壳中运行扩展程序来解决此问题,但是是否有更轻巧的解决方法?我想我可以表演 在未设置failglob的情况下进行预先扩展,并测试结果,但是 太杂乱了,里面有比赛条件。
由于预计POSIX外壳程序会因扩展错误而终止,并且Bash至少在某些情况下会这样做,所以从扩展错误中恢复的唯一安全方法似乎是导致该错误发生在子外壳程序中。 / p>
在我的特定情况下,我想确保该命令不以零参数或未扩展的glob作为参数运行,因为在这些情况下它可能做错事情。不过,如果命令运行时带有参数指定文件,这些文件在扩展后但在尝试对参数进行操作之前消失,则可以。最简单,最可靠的方法似乎是在设置了failglob
选项的子shell中运行命令。
还可以使用未设置failglob
的glob进行预扩展,测试其是否为空,然后使用预扩展的结果而不是再次扩展glob。就我的目的而言,这似乎有些过分,所以我选择了使用subshell。
更新
正如@eurythmia在回答中指出的那样,Bash的举止比我想象的还要奇怪。在 test1.sh 中,扩展错误不会导致整个 script 终止;相反,它会导致包含do_it
调用的 pipeline 立即失败,从而完全绕开了对函数调用命令本身成功或失败的任何考虑,因此不执行{{ 1}}命令。出于所有目的和目的,echo
调用本身不会成功也不会失败,这充其量是非常奇怪的。
但是,这不会改变此答案中给出的结论:绝对没有针对Bash记录该行为,并且与POSIX不一致。最简单的安全替代方法是在子外壳程序中隔离对do_it
选项的任何使用,但是如果由于某些原因不可行,则可以使用解决方法。