为什么 set -e 在带有括号 () 后跟 OR 列表 || 的子 shell 中不起作用?

为什么 set -e 在带有括号 () 后跟 OR 列表 || 的子 shell 中不起作用?

我最近遇到过一些这样的脚本:

( set -e ; do-stuff; do-more-stuff; ) || echo failed

这对我来说看起来很好,但它不起作用!set -e当您添加 时, 不适用||。没有它,它工作得很好:

$ ( set -e; false; echo passed; ); echo $?
1

但是,如果我添加||,则将set -e被忽略:

$ ( set -e; false; echo passed; ) || echo failed
passed

使用真实的、独立的 shell 可以按预期工作:

$ sh -c 'set -e; false; echo passed;' || echo failed
failed

我已经在多个不同的 shell(bash、dash、ksh93)中尝试过这一点,并且所有的行为方式都相同,所以这不是一个错误。有人可以解释一下吗?

答案1

根据这个线程set -e,这是 POSIX 指定在子 shell 中使用“”的行为。

(我也很惊讶。)

首先,行为:

-e当执行 while、until、if 或 elif 保留字后面的复合列表时,应忽略该设置,以 ! 开头的管道。保留字,或 AND-OR 列表中除最后一个以外的任何命令。

第二篇文章指出,

总之,(子 shell 代码)中的 set -e 不应该独立于周围的上下文进行操作吗?

不会。POSIX 描述很清楚,周围的上下文会影响子 shell 中是否忽略 set -e。

第四篇文章中还有更多内容,也是由 Eric Blake 撰写的,

第 3 点是不是要求子 shell 覆盖被忽略的上下文set -e。也就是说,一旦你处于一个-e被忽略的上下文中,就会有没有什么你可以做一些事情来-e再次得到服从,即使是一个 subshel​​l。

$ 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 标准的措辞并没有特别明确这一点。如果您在命令失败的上下文中阅读它,则该规则不适用:它仅适用于周围的上下文,但是,它完全适用于它。

答案2

事实上,如果您在子 shell后set -e使用运算符,则在子 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

亚伦·D·马拉斯科在他的回答中很好地解释了它为什么会这样。

这里有一个小技巧可以用来解决这个问题:在后台运行内部命令,然后立即等待它。内置函数wait将返回内部命令的退出代码,现在您使用的是||after 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

这是建立在这个想法之上的通用函数。如果删除关键字,它应该在所有 POSIX 兼容的 shell 中工作local,即将所有关键字替换local x=yx=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
}

使用示例:

#!/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 "$@"

运行示例:

$ ./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

使用此方法时唯一需要注意的是,从传递给的命令中完成的 Shell 变量的所有修改run都不会传播到调用函数,因为该命令在子 shell 中运行。

答案3

使用顶层时的解决方法set -e

我提出这个问题是因为我使用的set -e是错误检测方法:

/usr/bin/env bash
set -e
do_stuff
( take_best_sub_action_1; take_best_sub_action_2 ) || do_worse_fallback
do_more_stuff

如果没有||,脚本将停止运行并且永远不会到达do_more_stuff

由于似乎没有干净的解决方案,我想我只会set +e对我的脚本做一个简单的处理:

/usr/bin/env bash
set -e
do_stuff
set +e
( take_best_sub_action_1; take_best_sub_action_2 )
exit_status=$?
set -e
if [ "$exit_status" -ne 0 ]; then
  do_worse_fallback
fi
do_more_stuff

答案4

在后台运行命令的方法由@skozin 建议不会在 中工作bashbash仍然禁用set -e后台命令。不过dash工作ash得很好。

相关内容