Правильна поведінка пасток EXIT та ERR при використанні `set -eu`


27

Я спостерігаю якусь дивну поведінку під час використання set -e( errexit), set -u( nounset) поряд із пастками ERR та EXIT. Вони здаються спорідненими, тому поставити їх в одне питання здається розумним.

1) set -uне викликає помилок ERR

  • Код:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
    
  • Очікується: ERR-пастка викликається, RC! = 0
  • Фактично: помилка ERR не викликається, RC == 1
  • Примітка: set -eне змінює результат

2) Використання set -euкоду виходу в пастці EXIT 0, а не 1

  • Код:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
    
  • Очікується: виклик лову EXIT, RC == 1
  • Фактично: викликається пастка EXIT, RC == 0
  • Примітка. При використанні set +eRC == 1. Пастка EXIT повертає належний RC, коли будь-яка інша команда видає помилку.
  • Редагувати: На цю тему є публікація SO з цікавим коментарем, який дозволяє припустити, що це може бути пов’язане з використовуваною версією Bash. Тестування цього фрагмента за допомогою Bash 4.3.11 призводить до RC = 1, тож це краще. На жаль, оновити Bash (з 3.2.51) на всіх хостах наразі неможливо, тому нам доведеться придумати якесь інше рішення.

Хтось може пояснити будь-яку з цих форм поведінки?

Пошук цих тем виявився не дуже вдалим, що досить дивно, враховуючи кількість публікацій у налаштуваннях та пастках Баша. Хоча є одна нитка форуму , але висновок досить незадовільний.


3
З 4 по тому я думаю, що bashзірвався зі стандартом і почав ставити пастки в підрозділи. Пастка, як передбачається, буде виконана в тому ж середовищі, звідки прийшло повернення, але bashцього не робило досить довго.
mikeserv

1
Зачекайте хвилинку - ви хочете рішення чи пояснення? А якщо ви хочете рішення, то рішення, що саме? Що ти хочеш статися? set -eі set -uвони розроблені спеціально для вбивства скриптованої оболонки. Використання їх в умовах, які можуть викликати їх застосування, знищать сценарій оболонки. Цього не обійти, окрім того, щоб не використовувати їх, а замість цього перевірити на ті умови, коли вони застосовуються в кодовій послідовності. Отже, в основному, ви можете написати хороший оболонку-код, або можете використовувати set -eu.
mikeserv

2
Насправді я шукаю і те, і інше, оскільки я не зміг знайти достатню інформацію про те, чому -uб не викликати пастку ERR (це помилка, тому не слід викликати пастку) або код помилки 0, а не 1. Останнє, здається, є помилкою, що вже було виправлено у більш пізній версії, тож це все. Але першу частину досить важко зрозуміти, якщо ви не зрозуміли, що помилки в оцінці оболонки (розширення параметра) та фактичні помилки в командах здаються двома різними речами. Щодо рішення, ну, як ви запропонували, я зараз намагаюся уникати -euі перевіряти вручну, коли це необхідно.
dvdgsng

1
@dvdsng - Добре. Ось так і слід - ви повинні розмістити свій сценарій, коли ви це зробите як відповідь і присвоїте собі винагороду. Мені дуже не подобаються ці варіанти - вони не дозволяють обробляти винятки жодним безпечним способом.
mikeserv

1
@dvdsng - де будь-який із цих варіантів може бути корисним, проте знаходиться в контексті, що не відповідає загальному. І тому можливо, що те, що ви раніше ними використовували, може бути локалізовано в контексті підрозділу типу: (set -u; : $UNSET_VAR)і подібного. Такі речі теж можуть бути хорошими - ви можете час від &&часу скидати багато :, (set -e; mkdir dir; cd dir; touch dirfile)якщо ви отримаєте мій дрейф. Просто це є контрольованими контекстами - коли ви встановлюєте їх як глобальні параметри, ви втрачаєте контроль і стаєте контрольованими. Однак, як правило, є більш ефективні рішення.
mikeserv

Відповіді:


15

Від man bash:

  • set -u
    • Розгляньте невстановлені змінні та параметри, відмінні від спеціальних параметрів, "@"і "*"як помилку при виконанні розширення параметра. Якщо спробу розширення буде застосовано до невстановленої змінної чи параметра, оболонка надрукує повідомлення про помилку і, якщо не -iнетерактивне, виходить із ненульовим статусом.

POSIX стверджує, що у випадку помилки розширення неінтерактивна оболонка вийде, коли розширення асоціюється або зі спеціальним вбудованим оболонкою (що розрізнення bashрегулярно ігнорується, так що, можливо, не має значення), або будь-якою іншою утилітою крім .

  • Наслідки помилок оболонки :
    • Помилка розширення є той , який виникає , коли розкладання оболонки , певне в Word , разложениях виконується (наприклад, "${x!y}", тому що !не є допустимим оператор) ; реалізація може трактувати їх як синтаксичні помилки, якщо вона здатна виявити їх під час токенізації, а не під час розширення.
    • [A] n інтерактивна оболонка повинна написати діагностичне повідомлення на стандартну помилку, не виходячи з неї.

Також від man bash:

  • trap ... ERR
    • Якщо sigspec є ERR , аргумент команди виконується кожного разу, коли конвеєр (який може складатися з однієї простої команди) , списку або складеної команди повертає ненульовий статус виходу за умови наступних умов:
      • ERR пастка не виконується , якщо відмовив команда є частиною списку команд відразу після whileабо untilключового слова ...
      • ... частина тесту в ifзаяві ...
      • ... частина команди , виконаної в &&або ||списку , крім команди , наступної за остаточний &&або ||...
      • ... будь-яка команда в конвеєрі, але остання ...
      • ... або якщо зворотне значення команди інвертується за допомогою !.
    • Це ті самі умови, яких дотримується -e варіант errexit .

Зверніть увагу вище, що пастка ERR - це все про оцінку повернення якоїсь іншої команди. Але коли виникає помилка розширення , не існує запуску команди, щоб повернути що-небудь. У вашому прикладі echo ніколи не буває - тому що, коли оболонка оцінює та розширює свої аргументи, вона стикається із -uзмінною nset, яка була визначена явним варіантом оболонки, щоб викликати негайний вихід із поточної скриптованої оболонки.

І так виконується пастка EXIT , якщо така є, і оболонка виходить із діагностичним повідомленням та статусом виходу, відмінним від 0, - саме так, як це слід робити.

Що стосується речі rc: 0 , я думаю, що це помилка, що стосується певної версії - можливо, це стосується двох тригерів для EXIT, що відбуваються одночасно, і того, хто отримує код виходу другого (який не повинен відбуватися) . І все одно, з bashоновленим бінарним файлом, встановленим pacman:

bash <<\IN
    printf "shell options:\t$-\n"
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
IN

Я додав перший рядок, щоб ви могли бачити, що умови оболонки є умовами сценарію оболонки - він не є інтерактивним. Вихід:

shell options:  hB
bash: line 4: UNSET_VAR: unbound variable
EXIT (rc: 1)

Ось кілька відповідних приміток із останніх журналів змін :

  • Виправлена ​​помилка, яка спричинила $?неправильне встановлення асинхронних команд .
  • Виправлена ​​помилка, яка спричинила повідомлення про помилки, породжені помилками розширення в forкомандах, неправильним номером рядка.
  • Виправлена ​​помилка, яка призвела до того, що SIGINT і SIGQUIT не були в trapпапці в асинхронних командах додаткової оболонки.
  • Виправлена ​​проблема з перериванням обробки, що спричинило ігнорування другого та наступного SIGINT інтерактивними оболонками.
  • Оболонка більше не блокує отримання сигналів під час запуску trapобробників для цих сигналів і дозволяє більшості trap обробників працювати рекурсивно (запущені trapобробники, коли trapобробник виконує) .

Я думаю, що це або останнє, або перше, що є найбільш актуальним - або, можливо, комбінація двох. trapОброблювач є за своєю природою асинхронним , так як вся його робота полягає в тому, щоб чекати і обробляти асинхронні сигнали . І ви запускаєте два одночасно з -euі $UNSET_VAR.

І тому, можливо, вам слід просто оновити, але якщо вам подобається, ви взагалі зробите це з іншою оболонкою.


Дякуємо за пояснення, як розширення параметрів обробляється по-різному. Це прояснило багато речей для мене.
dvdgsng

Я дарую вам нагороду, тому що ваше пояснення було найбільш корисним.
dvdgsng

@dvdgsng - Gracias. З цікавості ви коли-небудь придумали рішення / рішення?
mikeserv

9

(Я використовую bash 4.2.53). У частині 1 на сторінці bash man просто сказано: "На стандартну помилку буде записано повідомлення про помилку, і неінтерактивна оболонка вийде". Це не говорить про те, що буде викликано пастку ERR, хоча я згоден, було б корисно, якби це було.

Щоб бути прагматичним, якщо ви дійсно хочете, щоб більш чітко впоратися з невизначеними змінними, можливим рішенням є ввести більшу частину коду всередину функції, а потім виконати цю функцію в підколонці і відновити код повернення та висновок stderr. Ось приклад, де "cmd ()" є функцією:

#!/bin/bash
trap 'rc=$?; echo "ERR at line ${LINENO} (rc: $rc)"; exit $rc' ERR
trap 'rc=$?; echo "EXIT (rc: $rc)"; exit $rc' EXIT
set -u
set -E # export trap to functions

cmd(){
 echo "args=$*"
 echo ${UNSET_VAR}
 echo hello
}
oops(){
 rc=$?
 echo "$@"
 return $rc # provoke ERR trap
}

exec 3>&1 # copy stdin to use in $()
if output=$(cmd "$@" 2>&1 >&3) # collect stderr, not stdout 
then    echo ok
else    oops "fail: $output"
fi

На мій баш я потрапляю

./script my stuff; echo "exit was $?"
args=my stuff
fail: ./script: line 9: UNSET_VAR: unbound variable
ERR at line 15 (rc: 1)
EXIT (rc: 1)
exit was 1

приємне, практичне рішення, яке насправді додає цінності!
Флоріан Хейгл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.