괄호 () 뒤에 OR리스트가있는 서브 쉘에서 set -e가 작동하지 않는 이유는 무엇입니까?


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

나는 여러 다른 쉘 (bash, dash, ksh93)에서 이것을 시도했고 모두 같은 방식으로 작동하므로 버그가 아닙니다. 누군가 이것을 설명 할 수 있습니까?


`(....)``구문은 내용을 실행하기 위해 별도의 쉘을 시작합니다. 설정은 외부에 적용되지 않습니다.
vonbrand

@vonbrand, 당신은 요점을 놓쳤다. 그는 서브 쉘 내부에 적용하기를 원하지만 ||서브 쉘 외부는 서브 쉘 내부의 동작에 영향을줍니다.
CJM

1
비교 (set -e; echo 1; false; echo 2)(set -e; echo 1; false; echo 2) || echo 3
요한

답변:


32

이 스레드 에 따르면 POSIX set -e가 서브 쉘에서 " " 를 사용하도록 지정하는 동작 입니다.

(나도 놀랐다.)

먼저, 행동 :

-e그동안 다음 화합물 목록을 실행할 때 경우, 또는 예약어 ELIF, 파이프 라인의 시작으로까지 설정은 무시해야한다! 예약어 또는 마지막이 아닌 AND-OR 목록의 명령

두 번째 포스트 노트

요약하면 set-e in (서브 쉘 코드)은 주변 컨텍스트와 독립적으로 작동하지 않아야합니까?

POSIX 설명은 주변 컨텍스트가 서브 쉘에서 set -e가 무시되는지 여부에 영향을 준다는 것이 분명합니다.

네 번째 게시물에는 Eric Blake가 조금 더 있습니다.

포인트 3은 무시되는 컨텍스트를 무시하기 위해 서브 쉘을 요구 하지 않습니다set -e . 즉, 문맥에 한 번입니다 -e무시가없는 것도 당신이 얻을 할 수 -e조차 서브 쉘, 다시 순종은.

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

set -e부모와 서브 쉘에서 두 번 호출했지만 서브 쉘이 -e무시 되는 컨텍스트 (if 문의 조건)에 존재한다는 사실 은 서브 쉘에서 다시 활성화하기 위해 수행 할 수있는 작업이 없습니다. -e.

이 동작은 확실히 놀랍습니다. 직관적이지 않습니다. 다시 활성화 set -e하면 효과가 있을 것으로 예상 하고 주변 상황이 우선하지 않을 것입니다. 또한 POSIX 표준의 표현은이를 명확하게 나타내지 않습니다. 명령이 실패한 컨텍스트에서 읽으면 규칙이 적용되지 않습니다. 규칙은 주변 컨텍스트에만 적용되지만 완전히 적용됩니다.


그 링크에 감사드립니다, 그들은 매우 흥미있었습니다. 그러나 나의 예는 (IMO)는 실질적으로 다릅니다. 대부분의 논의는 상위 쉘에서 set-e가 서브 쉘에 의해 상속되는지 여부입니다 set -e; (false; echo passed;) || echo failed. 사실, 표준의 말로 -e가 무시되는 것은 놀랍지 않습니다. 필자의 경우, subshell에서 -e 명시 적으로 설정 하고 실패시 서브 쉘 이 종료 될 것으로 예상 합니다 . 서브 쉘에는 AND-OR 목록이 없습니다.
MadScientist

동의하지 않습니다. 두 번째 게시물 (앵커를 작동시킬 수 없음)은 " POSIX 설명은 주변 컨텍스트가 서브 쉘에서 set -e가 무시되는지 여부에 영향을 준다는 것을 명확하게 보여줍니다. "-서브 쉘은 AND-OR 목록에 있습니다.
Aaron D. Marasco

네 번째 게시물 (Erik Blake도)은 " 우리가 set -e를 두 번 (부모와 서브 쉘 모두에서) 호출했지만, 서브 쉘이 -e가 무시되는 상황 (if의 조건)에 존재한다는 사실 문), -e를 활성화 다시 우리의 서브 쉘에서 할 수있는 일은 없다. "
아론 D. Marasco

네가 옳아; 내가 어떻게 잘못 읽었는지 잘 모르겠습니다. 감사.
MadScientist

1
나는 머리카락을 찢어 버리는이 행동이 POSIX 사양에 있음을 알게되어 기쁩니다. 그래서 해결 방법은 무엇입니까?! if||&&전염성이? 이것은 터무니 없다
Steven Lu

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

그의 대답에서 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

이 아이디어를 기반으로하는 일반적인 기능은 다음과 같습니다. 당신이 제거하면 그것은 모든 POSIX 호환 쉘에서 작동합니다 local키워드를 즉, 모든 교체, local x=y단지와 함께 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

이 방법을 사용할 때 알아야 run할 것은 명령이 서브 쉘에서 실행되기 때문에 사용자가 전달한 명령에서 수행 된 쉘 변수의 모든 수정 사항이 호출 함수로 전파되지 않는다는 것입니다.


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)에서 인용 할 수 있습니다.

실패한 명령이 && 또는 ||에서 실행 된 명령의 [...] 부분 인 경우 쉘이 종료되지 않습니다. 마지막 && 또는 || 뒤에 오는 명령을 제외한리스트 [...]

아마도 여러 명령에 대한 평가는 || 문맥.


글쎄, 모든 쉘이 그런 식으로 행동한다면 그것은 버그가 아니라 정의에 의한 것입니다. 그것은 표준 행동입니다 :-). 우리는 비 직관적 인 것으로 행동을 애도 할 수도 있지만 ... eval을 사용한 트릭은 매우 흥미 롭습니다.
MadScientist 2012

어떤 껍질을 사용하십니까? eval트릭은 나를 위해 작동하지 않습니다. 나는 bash, posix 모드에서 bash 및 대시를 시도했다.
Dunatotatos

@Dunatotatos는 Hauke가 말했듯이 bash4.2였습니다. bash4.3에서 "고정"되었습니다. pdksh 기반 쉘은 동일한 "문제"를 갖습니다. 그리고 여러 버전의 여러 셸에는와 함께 다양한 종류의 "문제"가 set -e있습니다. set -e의도적으로 고장났습니다. 제어 구조, 서브 셸 또는 명령 대체가없는 가장 간단한 쉘 스크립트 이외의 용도로는 사용하지 않을 것입니다.
Stéphane Chazelas

1

최상위 수준을 사용할 때 해결 방법 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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.