考虑以下代码
外部范围.sh
#!/bin/bash
set -e
source inner-scope.sh
echo $(inner)
echo "I thought I would've died :("
内部范围.sh
#!/bin/bash
function inner() { echo "winner"; return 1; }
我试图outer-scope.sh
在调用inner()
失败时退出。由于$()
调用了子 shell,所以不会发生这种情况。
我还怎样才能获得函数的输出,同时保留函数可能以非零退出代码退出的事实?
答案1
$()
保留退出状态;您只需在没有自己状态的语句中使用它,例如赋值。
输出=$(内部)
在此之后,$?
将包含的退出状态inner
,并且您可以使用各种检查它:
output=$(inner) || exit $?
echo $output
或者:
if ! output=$(inner); then
exit $?
fi
echo $output
或者:
if output=$(inner); then
echo $output
else
exit $?
fi
(注意:exit
没有参数的裸命令相当于exit $?
- 也就是说,它以最后一个命令的退出状态退出。我使用第二种形式只是为了清楚起见。)
另外,需要注意的是:source
在这种情况下完全不相关。您只需inner()
在outer-scope.sh
文件中定义,即可获得相同的结果。
答案2
根据内置命令 set 的手册,shell 确实不是如果出现以下情况则退出:
- 失败的命令是紧跟
while
或until
关键字的命令列表的一部分,- 测试的一部分在
if
声明中,&&
或列表中执行的任何命令的一部分,除了最后一个或||
之后的命令,&&
||
- 管道中的任何命令,但最后一个命令除外,
- 或者如果命令的返回状态被反转
!
。
以上任何一种方法都可以,除了第一种方法,因为你没有运行循环。不过为了便于阅读,最好使用以下if
语句:
set -e
if x=$(echo a; false); then
echo "subshell worked"
else
echo "subshell failed"
fi
echo "x: $x"
或者你可以反转结果:
bash -c 'set -e; ! x=$(echo a; false); echo "[$?]x:$x"' # [0]x:a
bash -c 'set -e; ! y=$(echo b; true ); echo "[$?]y:$y"' # [1]y:b
但要注意结果现在被反转了。幸运的是,即使成功的子 shell 被反转,bash 也不会退出。
有趣的是,下面的方法似乎也有效,通过将子 shell 调用分组到列表中,然后反转整个列表,而不仅仅是子 shell 调用:
bash -c 'set -e; ! { x=$(echo a; false); echo "[$?]x:$x"; }' # [1]x:a
这在某种程度上与上面的第 3 种情况相匹配,它也是一个列表,只是它没有使用&&
或||
。
内联初始化的特殊情况
注意函数局部变量的一个棘手情况,比较下面两个几乎相同的函数:
f() { local v=$(echo data; false); echo output:$v, status:$?; }
g() { local v; v=$(echo data; false); echo output:$v, status:$?; }
在这两个函数中,子 shell 都以false
导致其失败的命令结束,然而,在执行时,我们会得到:
$ f # fooled by 'local' with inline initialization
output:data, status:0
$ g # good one, with separated declaration and initialization
output:data, status:1
为什么?
在 Bash 中,local
实际上是一个内置命令当子 shell 的输出用于初始化local
变量时,退出状态不再是子 shell 的退出状态,而是local
命令,只要0
局部变量被声明即可。
答案3
#!/bin/bash
set -e
source inner-scope.sh
foo=$(inner)
echo $foo
echo "I thought I would've died :("
通过添加echo
,子 shell 不会独立存在(不会单独检查)并且不会中止。赋值可以避免这个问题。
您也可以这样做,并将输出重定向到文件,以便稍后处理。
tmpfile=$( mktemp )
inner > $tmpfile
cat $tmpfile
rm $tmpfile