대괄호() 뒤에 OR 목록 ||가 있는 하위 쉘에서 set -e가 작동하지 않는 이유는 무엇입니까?

대괄호() 뒤에 OR 목록 ||가 있는 하위 쉘에서 set -e가 작동하지 않는 이유는 무엇입니까?

최근에 다음과 같은 스크립트를 발견했습니다.

( 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)에서 시도했고 모두 같은 방식으로 작동하므로 버그가 아닙니다. 누군가 이것을 설명할 수 있나요?

답변1

~에 따르면이 스레드set -e, 이는 서브셸에서 ""를 사용하기 위한 POSIX 지정 동작입니다.

(나도 놀랐다.)

첫째, 행동은 다음과 같습니다.

-e!로 시작하는 파이프에 대해 while, Until, if 또는 elif 예약어 뒤에 오는 복합 목록을 실행할 때 이 설정은 무시되어야 합니다. 예약어 또는 AND-OR 목록의 마지막 항목을 제외한 모든 명령.

두 번째 기사에서는 다음과 같이 말합니다.

어쨌든 (서브쉘 코드)에서 -e를 설정하면 주변 컨텍스트와 독립적으로 작동하면 안 되나요?

습관. 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 

이를 두 번 호출하더라도 (상위 쉘과 하위 쉘에서) 하위 쉘이 무시된 컨텍스트 (if 문의 조건) set -e에 존재한다는 사실로 인해 하위 쉘에서 이를 다시 활성화하기 위해 아무것도 할 수 없습니다 .-e-e

이 행동은 정말 놀랍습니다. 이는 직관에 어긋납니다. 다시 활성화하면 효과가 있을 것으로 예상할 수 set -e있으며 주변 환경이 우선하지 않을 것이며 POSIX 표준의 표현에서는 이를 특별히 명확하게 나타내지 않습니다. 명령 실패 상황에서 읽는 경우 이 규칙은 적용되지 않습니다. 주변 컨텍스트에만 적용되지만 전체적으로 적용됩니다.

답변2

실제로, 서브쉘 뒤에 연산자를 사용하면 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.마라스코그의 대답에왜 이런 일을 하는지에 대한 좋은 설명입니다.

이 문제를 해결하기 위해 사용할 수 있는 작은 트릭은 다음과 같습니다. 백그라운드에서 내부 명령을 실행한 다음 즉시 기다리십시오. 내장 함수는 wait내부 명령의 종료 코드를 반환합니다. 이제 내부 함수가 아닌 ||after 를 사용하고 있으므로 후자 내부에서는 잘 작동합니다.waitset -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명령이 하위 쉘에서 실행되기 때문에 전달된 명령에서 수행된 쉘 변수에 대한 수정 사항이 호출 함수에 전파되지 않는다는 것입니다.

답변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이 제안함에서는 작동하지 않습니다 bash. 백그라운드 명령은 bash여전히 ​​비활성화되어 있습니다 . set -e그래도 잘 dash작동합니다 .ash

관련 정보