Чому множина -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, ти пропустив пункт. Він хоче, щоб воно застосовувалося всередині нижньої частини, але ||поза зовнішньою оболонкою впливає на поведінку всередині нижньої частини.
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, окрім останнього.

Друге повідомлення зазначає,

Підсумовуючи це, чи не повинен встановити -e в (код підключення) діяти незалежно від оточуючого контексту?

Ні. В описі 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 зрозуміло, що оточуючий контекст впливає на те, чи встановлено -e ігнорується в нижній частині корпусу ".
Аарон Д. Мараско

Четвертий пост (також Ерік Блейк) також говорить: " Навіть якщо ми два рази називали 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

Єдине, про що потрібно пам’ятати, використовуючи цей метод, - це те, що всі модифікації змінних Shell, виконані з команди, яку ви передаєте, 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

Яку оболонку ви використовуєте? evalТрюк не працює для мене. Я спробував bash, bash в posix режимі та dash.
Дунатотатос

@Dunatotatos, як сказав Хоуке, це було bash4.2. Це було "зафіксовано" у bash4.3. оболонки на основі pdksh матимуть ту саму "проблему". І кілька версій декількох оболонок мають всілякі різні "проблеми" set -e. set -eпорушується конструкцією. Я б не використовував його ні для чого, крім найпростіших скриптів оболонки без керуючих структур, підпакетів або підстановок команд.
Стефан Шазелас

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.