Bash:在被引用的脚本中出错时停止

19
在要执行的 shell 脚本中,我可以使用 set -e 在出现错误时中止执行。
然而,在一个已被引用的脚本中使用 set -e 会导致如果后面的命令退出时出现错误状态,原始 shell 将被关闭。
source set_e.sh
./exit_1.sh
# shell dies

一个微不足道的解决方案是在脚本末尾使用set +e,但如果被使用(如果将来有人包装我的脚本),这会破坏父级的set -e。如何在被源代码引用的脚本中实现出错中止功能?


@BlueMoon OP的帖子解释了为什么那样做行不通。 - that other guy
你不能在调用环境中设置一个退出陷阱并在那里处理它吗? - Mr. Llama
6个回答

9

这是不可能的。但是如果您想使用子Shell,可以选择这样做:

(
    set -e
    source another.sh
)

被调用脚本无法更改调用脚本的环境。

注意:最好使用换行符而不是分号来分隔两个命令。


我认为这是一个好主意,但显然子shell的stdout和stderr没有传播到外部:/ - xeruf
@xeruf 请尝试使用mkfifo - glibg10b

5

嗯,关于源脚本中的错误拦截后原作者想要什么并不是很清楚,但以下作为解决方案入口:

您可以在 ERR 上设置陷阱,并在其中处理源脚本中的错误。以下有两种情况:一个是源脚本使用了“set -e”,另一个是源脚本没有使用“set -e”。

主要脚本使用定义了“set -e”的次要脚本,并捕获了错误:

[galaxy => ~]$ cat primary.sh
#!/bin/sh

set -e
echo 'Primary script'
trap 'echo "Got an error from the secondary script"' ERR
source secondary.sh
trap - ERR
echo 'Primary script exiting'
[galaxy => ~]$ cat secondary.sh
#!/bin/sh

echo 'Secondary script'
set -e
echo 'Secondary script generating an error'
false
echo 'Secondary script - should not be reached'
[galaxy => ~]$ ./primary.sh
Primary script
Secondary script
Secondary script generating an error
Got an error from the secondary script
[galaxy => ~]$

主要脚本在不带“set -e”参数的情况下调用次要脚本,并捕获错误:

[galaxy => ~]$ cat primary.sh
#!/bin/sh

set -e
echo 'Primary script'
trap 'echo "Got an error from the secondary script"' ERR
source secondary.sh
trap - ERR
echo 'Primary script exiting'
[galaxy => ~]$ cat secondary.sh
#!/bin/sh

echo 'Secondary script'
echo 'Secondary script generating an error'
false
echo 'Secondary script - should not be reached if sourced by primary.sh'
[galaxy => ~]$ ./primary.sh
Primary script
Secondary script
Secondary script generating an error
Got an error from the secondary script
[galaxy => ~]$

作为一个额外的奖励:拦截源脚本中的错误并继续执行:
[galaxy => ~]$ cat primary.sh
#!/bin/sh

echo 'Primary script'
i=0
while [ $i = 0 ]; do
    i=1
    trap 'echo "Got an error from the secondary script"; break' ERR
    source secondary.sh
done
trap - ERR
echo 'Primary script exiting'
[galaxy => ~]$ cat secondary.sh
#!/bin/sh

echo 'Secondary script'
echo 'Secondary script generating an error'
false
echo 'Secondary script - should not be reached if sourced by primary.sh'
[galaxy => ~]$ ./primary.sh
Primary script
Secondary script
Secondary script generating an error
Got an error from the secondary script
Primary script exiting
[galaxy => ~]$

我认为只有当次要脚本中没有 while、for 或 until 时,使用 break 的陷阱才有效。这是真的吗? - kdubs
是的,@kdubs,这是正确的。但重点是要演示这个概念。 - galaxy
没问题,我只是想确保我理解了。 - kdubs

4
set -e替换为return 0 (或选择你喜欢的整数代替0)。你可以将源文件视为一个函数来处理它。例如:
$ cat myreturn.sh
#!/bin/bash

let i=0

while test "$i" -lt 10; do

    echo "i $i"
    if test "$i" -gt 5 ; then
        return 5
    fi
    sleep 1
    ((i+=1))

done

return 4

$ ( . myreturn.sh )
i 0
i 1
i 2
i 3
i 4
i 5
i 6

$ echo $?
5

  1. 你不应该随意选择整数作为退出代码 - 0 表示成功,其他值则不然 - 最好它们有一些含义。
  2. 这并不能回答问题,因为“返回0”无法帮助您捕获您无法控制的脚本中的错误。
- xeruf

2
您可以检查是否已启用set -e,并在需要时有条件地设置它:
[[ $- == *e* ]] && state=-e || state=+e
set -e
yourcode
set "$state"

请注意,set -e 存在缺陷,脚本不应该依赖它的正确性。例如,如果有人将你的脚本引入到一个 if 语句中,set -e 可能无法正常工作:

echo '
 set -e
 ls file.that.doesnt.exist
 echo "Success"
' > yourscript

set -e
if ! source yourscript
then 
  echo "Initialization failed"
fi
echo "Done"

在bash 4.3.30中,set -e 将不再在yourscript中因失败而中止:

ls: cannot access file.that.doesnt.exist: No such file or directory
Success
Done

在Bash 2、3以及4.2版本中,使用这个命令将会终止整个脚本:

ls: cannot access file.that.doesnt.exist: No such file or directory

1
第二部分有误导性 - 如果yourscript设置了errexit,那么包装代码确实会在其中的错误中终止(使用bash 4.2.x进行测试)。与shell if-then-else的常规用法相比,这可能看起来不符合直觉,但如果您正在源代码不可靠的代码,则将其包装在子shell中的期望似乎并不不合理。 - Josip Rodin
1
@JosipRodin 我看到 bash 2 和 3 中会中止整个脚本,但在 4.3.30(1)-release 中,它未能像描述的那样中止。有趣的是了解行为如此之大的变化。 - that other guy
很好的发现。我认为当前的行为与主要建议一致 - 使用子shell。例如,如果您执行:set +e; ( . yourscript ); if [ $? -ne 0 ]; then echo "Initialization failed"; fi - 结果是相当合理的。 - Josip Rodin
当然,使用source的目的是为了避免创建子shell,否则你可以直接正常运行它。 - that other guy
我认为问题可能在于您期望它以预处理器#include的方式在父脚本的相同上下文中运行,同时又在该脚本的if语句中运行它。后者已知会以一种使errexit无效的方式修改上下文。因此,在那里修改source的行为是理所当然的。 - Josip Rodin
我认为这并不能回答问题 - 无论您在脚本之后的哪个位置取消设置 "e",只要被源脚本存在错误,父shell 就会被终止,只要set -e设置。 - xeruf

2

我发现对于我来说,设置、捕获和取消 set -e 很容易出错。因此,我更喜欢使用错误陷阱。

这是我经常使用的一行代码,包括尝试静默的 return (如果被引用则可行),否则使用 exit。同时,取消错误陷阱,否则它会留在被引用的脚本中。

trap 'echo 在第 $LINENO 行出现错误 $?; trap - ERR; return 2>/dev/null || exit' ERR


0

您可以通过set -o检测源脚本开始时是否设置了errexit选项,并在源脚本结束时恢复其原始值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接