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


30

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

( 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

使用真实的独立外壳可以按预期工作:

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

我已经在多个不同的shell(bash,dash,ksh93)中进行了尝试,并且它们的行为方式都相同,所以这不是一个bug。有人可以解释吗?


`(....)``构造启动一个单独的shell来运行其内容,其中的任何设置都不适用于外部。
vonbrand

@vonbrand,您错过了重点。他希望将其应用于子外壳内部,但是||子外壳外部会影响子外壳内部的行为。
cjm

1
比较(set -e; echo 1; false; echo 2)(set -e; echo 1; false; echo 2) || echo 3
约翰

Answers:


32

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

(我也很惊讶。)

一,行为:

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

第二篇笔记,

总之,(子外壳程序代码)中的-e设置不应该独立于周围环境运行吗?

否。POSIX描述清楚地表明,周围的上下文会影响在子外壳程序中是否忽略set -e。

第四篇文章中还有更多内容,同样是埃里克·布莱克(Eric Blake),

第3点是要求子shell覆盖,其中上下文set -e被忽略。也就是说,一旦您处于-e被忽略的环境中,就无法再服从任何东西-e,甚至没有子外壳。

$ bash -c 'set -e; if (set -e; false; echo hi); then :; fi; echo $?' 
hi 
0 

即使我们调用了set -e两次(在父级和子Shell中都进行了调用),但事实是子Shell存在于-e被忽略的上下文中(if语句的条件),但我们无法在子Shell中重新启用任何操作-e

这种行为绝对令人惊讶。这是违反直觉的:人们期望重新启用set -e会产生效果,并且周围的环境不会成为先例。此外,POSIX标准的措辞并没有特别明确。如果您在命令失败的上下文中阅读该规则,则该规则将不适用:它仅适用于周围的上下文,但是完全适用于它。


感谢这些链接,它们非常有趣。但是,我的示例与(IMO)完全不同。该讨论的大部分内容是父shell中的set -e是否由subshel​​l继承set -e; (false; echo passed;) || echo failed。实际上,考虑到标准的措辞,在这种情况下-e被忽略并不令我感到惊讶。但是,就我而言,我在subshel​​l中显式设置了-e ,并期望子shell在失败时退出。子
外壳中

我不同意。第二篇文章(我无法让锚起作用)说:“ POSIX描述很清楚,周围的上下文会影响在子外壳中是否忽略set -e。 ”-子外壳在AND-OR列表中。
亚伦·马拉斯科

第四篇文章(同样是Erik Blake)也说:“ 即使我们两次调用set -e(在父级和子shell中),但实际上,子shell存在于-e被忽略的上下文中(if的条件声明),我们无法在子外壳中重新启用-e。
Aaron D. Marasco 2013年

你是对的; 我不确定如何误读这些内容。谢谢。
MadScientist 2013年

1
我很高兴得知我正在撕头发的这种行为原来是POSIX规范。那么解决方法是什么?if||&&有传染性?这很荒谬
史蒂芬·卢

7

的确,set -e如果您在子外壳||之后使用运算符,则在子外壳内部不会产生任何影响。例如,这行不通:

#!/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·马拉斯科 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

这是基于此思想的通用功能。如果删除local关键字,它应该在所有POSIX兼容的shell中都可以工作,即local x=y用just 替换所有关键字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
}

用法示例:

#!/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中运行。


2

我不会仅仅因为几个外壳的行为而排除它是一个错误。;-)

我提供了更多乐趣:

start cmd:> ( eval 'set -e'; false; echo passed; ) || echo failed
passed

start cmd:> ( eval 'set -e; false'; echo passed; ) || echo failed
failed

start cmd:> ( eval 'set -e; false; echo passed;' ) || echo failed
failed

我可以引用man bash(4.2.24)的话:

如果失败的命令是在&&或||中执行的任何命令的一部分,则shell不会退出 列表,最后一个&&或||之后的命令除外 [...]

也许对几个命令进行评估会导致忽略||。上下文。


好吧,如果所有的shell都以这种方式运行,那么按照定义,这不是错误,而是标准行为:-)。我们可能会把行为归咎于非直觉,但是……eval的把戏很有趣,这是肯定的。
MadScientist

您使用什么外壳?这eval招对我不起作用。我尝试了bash,posix模式下的bash和破折号。
Dunatotatos

正如Hauke所说,@ Dunatotatos是bash4.2。它在bash4.3中被“修复”。基于pdksh的外壳将具有相同的“问题”。几个shell的多个版本都有各种不同的“问题” set -eset -e被设计破坏了。除了最简单的没有控制结构,子shell或命令替换的shell脚本之外,我什么都不会使用它。
斯特凡Chazelas

1

usint顶层时的解决方法 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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.