Почему 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, конвейер, начинающийся с! зарезервированное слово или любая другая команда из списка И-ИЛИ, кроме последней.

Второй пост отмечает,

Итак, не должен ли set -e in (код подоболочки) работать независимо от окружающего контекста?

Нет. Описание POSIX ясно, что окружающий контекст влияет на то, игнорируется ли set -e в подоболочке.

Еще немного в четвертом посте, также от Эрика Блейка,

Точка 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 не делает это особенно ясным. Если вы читаете его в контексте, где команда не выполняется, правило не применяется: оно применяется только в окружающем контексте, однако оно применяется к нему полностью.

Аарон Д. Мараско
источник
Спасибо за эти ссылки, они были очень интересными. Тем не менее, мой пример (ИМО) существенно отличается. Большая часть этой дискуссии , является ли набор -e в родительской оболочке наследуются подоболочкой: set -e; (false; echo passed;) || echo failed. На самом деле меня не удивляет, что в данном случае -e игнорируется, учитывая формулировку стандарта. В моем случае, однако, я явно устанавливаю -e в подоболочке и ожидаю, что подоболочка завершится неудачно. В подоболочке нет списка «И-ИЛИ» ...
MadScientist
Я не согласен. Во втором посте (я не могу заставить работать якоря) говорится: « В описании POSIX ясно, что окружающий контекст влияет на то, игнорируется ли set -e в подоболочке. » - подоболочка находится в списке AND-OR.
Аарон Д. Мараско
В четвертом посте (также Эрик Блейк) также говорится: « Несмотря на то, что мы дважды вызывали set -e (как в родительском, так и в подоболочке), тот факт, что подоболочка существует в контексте, где -e игнорируется (условие if ), мы ничего не можем сделать в подоболочке, чтобы снова включить -e. "
Аарон Д. Мараско
Вы правы; Я не уверен, как я их неправильно понял. Спасибо.
MadScientist
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

Аарон Д. Мараско в своем ответе отлично объясняет, почему он так себя ведет.

Вот небольшой трюк, который можно использовать для исправления: запустите внутреннюю команду в фоновом режиме, а затем сразу дождитесь ее. 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, не будут распространяться на вызывающую функцию, потому что команда выполняется в подоболочке.

skozin
источник
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
Какую оболочку вы используете? evalТрюк не работает для меня. Я пробовал bash, bash в режиме posix и dash.
Дунатотатос
@Dunatotatos, как сказал Хауке, был bash4.2. Это было «исправлено» в bash4.3. Оболочки на основе pdksh будут иметь ту же «проблему». И несколько версий нескольких оболочек имеют всевозможные «проблемы» с set -e. set -eсломан по дизайну. Я бы не использовал его ни для чего, кроме простейших сценариев оболочки без структур управления, подоболочек или подстановок команд.
Стефан
1

Обходной путь, когда usint toplevel 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
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
источник