Mad*_*ist 42 shell shell-script
我最近遇到了一些这样的脚本:
( set -e ; do-stuff; do-more-stuff; ) || echo failed
Run Code Online (Sandbox Code Playgroud)
这对我来说看起来不错,但它不起作用!将set -e不适用,当你添加的||。没有它,它工作正常:
$ ( set -e; false; echo passed; ); echo $?
1
Run Code Online (Sandbox Code Playgroud)
但是,如果我添加||,set -e则忽略:
$ ( set -e; false; echo passed; ) || echo failed
passed
Run Code Online (Sandbox Code Playgroud)
使用真正的、单独的 shell 可以按预期工作:
$ sh -c 'set -e; false; echo passed;' || echo failed
failed
Run Code Online (Sandbox Code Playgroud)
我已经在多个不同的 shell(bash、dash、ksh93)中尝试过这个,并且都以相同的方式运行,所以这不是一个错误。有人可以解释一下吗?
Aar*_*sco 43
根据此线程,这是 POSIX 指定的set -e在子 shell 中使用 " "的行为。
(我也很惊讶。)
一、行为:
-e执行while、until、if 或elif 保留字后的复合列表时,应忽略该设置,以 保留字,或除最后一个以外的 AND-OR 列表中的任何命令。
第二个帖子说明,
总而言之,不应该在(子外壳代码)中设置 -e 独立于周围的上下文运行吗?
不。POSIX 描述很清楚,周围的上下文会影响 set -e 在子 shell 中是否被忽略。
埃里克·布莱克 (Eric Blake) 的第四篇文章还有更多内容,
第 3 点不要求子 shell 覆盖
set -e被忽略的上下文。也就是说,一旦你在一个方面-e被忽略,没有什么可以做的就是-e再次服从,甚至没有一个子shell。Run Code Online (Sandbox Code Playgroud)$ bash -c 'set -e; if (set -e; false; echo hi); then :; fi; echo $?' hi 0即使我们调用了
set -e两次(在父shell和子shell中),事实上子shell存在于一个-e被忽略的上下文中(if语句的条件),我们在子shell中没有什么可以重新启用-e.
这种行为绝对令人惊讶。这是违反直觉的:人们会期望重新启用set -e会产生效果,并且周围的环境不会成为先例;此外,POSIX 标准的措辞并没有特别清楚地说明这一点。如果您在命令失败的上下文中阅读它,则该规则不适用:它仅适用于周围的上下文,但是,它完全适用于它。
实际上,set -e如果在子shell||之后使用运算符,则它在子shell 中无效;例如,这行不通:
#!/bin/sh
# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer
set -e
outer() {
echo '--> outer'
(inner) || {
exit_code=$?
echo '--> cleanup'
return $exit_code
}
echo '<-- outer'
}
inner() {
set -e
echo '--> inner'
some_failed_command
echo '<-- inner'
}
outer
Run Code Online (Sandbox Code Playgroud)
Aaron D. Marasco在他的回答中很好地解释了它为什么会这样。
这里有一个小技巧可以用来解决这个问题:在后台运行内部命令,然后立即等待它。该wait内建将返回内部命令的退出代码,现在你正在使用||后wait,而不是内部功能,set -e工作正常后内:
#!/bin/sh
# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup
set -e
outer() {
echo '--> outer'
inner &
wait $! || {
exit_code=$?
echo '--> cleanup'
return $exit_code
}
echo '<-- outer'
}
inner() {
set -e
echo '--> inner'
some_failed_command
echo '<-- inner'
}
outer
Run Code Online (Sandbox Code Playgroud)
这是建立在这个想法之上的通用函数。如果删除local关键字,它应该在所有 POSIX 兼容的 shell 中工作,即local x=y用 just替换 all x=y:
# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
local cmd="$1"; shift
local exit_code=0
local e_was_set=1; if ! is_shell_attribute_set e; then
set -e
e_was_set=0
fi
"$cmd" "$@" &
wait $! || {
exit_code=$?
}
if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
set +e
fi
if [ -n "$CLEANUP" ]; then
RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
return $?
fi
return $exit_code
}
is_shell_attribute_set() { # attribute, like "x"
case "$-" in
*"$1"*) return 0 ;;
*) return 1 ;;
esac
}
Run Code Online (Sandbox Code Playgroud)
用法示例:
#!/bin/sh
set -e
# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh
main() {
echo "--> main: $@"
CLEANUP=cleanup run inner "$@"
echo "<-- main"
}
inner() {
echo "--> inner: $@"
sleep 0.5; if [ "$1" = 'fail' ]; then
oh_my_god_look_at_this
fi
echo "<-- inner"
}
cleanup() {
echo "--> cleanup: $@"
echo " RUN_CMD = '$RUN_CMD'"
echo " RUN_EXIT_CODE = $RUN_EXIT_CODE"
sleep 0.3
echo '<-- cleanup'
return $RUN_EXIT_CODE
}
main "$@"
Run Code Online (Sandbox Code Playgroud)
运行示例:
$ ./so_3 fail; echo "exit code: $?"
--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
RUN_CMD = 'inner'
RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127
$ ./so_3 pass; echo "exit code: $?"
--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
RUN_CMD = 'inner'
RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0
Run Code Online (Sandbox Code Playgroud)
使用此方法时,您唯一需要注意的是,从您传递给的命令执行的所有 Shell 变量修改run都不会传播到调用函数,因为该命令在子 shell 中运行。