Правильное поведение ловушек 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 trap, RC == 1
  • Фактически: ловушка EXIT называется, RC == 0
  • Примечание. При использовании set +eRC == 1. Ловушка EXIT возвращает правильный RC, когда любая другая команда выдает ошибку.
  • Редактировать: есть SO сообщение на эту тему с интересным комментарием, предполагающим, что это может быть связано с используемой версией Bash. Тестирование этого фрагмента с помощью Bash 4.3.11 дает RC = 1, так что это лучше. К сожалению, в настоящее время обновление Bash (с версии 3.2.51) на всех хостах невозможно, поэтому нам нужно найти какое-то другое решение.

Кто-нибудь может объяснить любое из этих поведений?

Поиск по этим темам был не очень успешным, что довольно удивительно, учитывая количество постов в настройках и ловушках Bash. Хотя есть одна ветка форума , но вывод довольно неудовлетворительный.

dvdgsng
источник
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
    • Treat неустановленные переменные и другие , чем специальные параметры параметров "@"и "*"как ошибка при выполнении расширения параметра. Если попытка раскрытия выполняется для неустановленной переменной или параметра, оболочка выводит сообщение об ошибке и, если не является -iинтерактивной, завершает работу с ненулевым состоянием.

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

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

Также из man bash:

  • trap ... ERR
    • Если sigspec - ERR , команда arg выполняется всякий раз, когда конвейер (который может состоять из одной простой команды) , списка или составной команды возвращает ненулевой статус выхода при соблюдении следующих условий:
      • 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.

И поэтому, может быть, вам следует просто обновить, но если вы любите себя, вы сделаете это с другой оболочкой.

mikeserv
источник
Спасибо за объяснение того, как расширение параметров обрабатывается по-разному. Это многое прояснило для меня.
dvdgsng
Я даю вам награду, потому что ваше объяснение было очень полезным.
dvdgsng
@dvdgsng - Грасиас. Из любопытства вы когда-нибудь подходили к вашему решению?
mikeserv
9

(Я использую Bash 4.2.53). В первой части справочной страницы bash просто написано «Сообщение об ошибке будет записано со стандартной ошибкой, и неинтерактивная оболочка завершится». В нем не говорится, что будет вызвана 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
meuh
источник
хорошо, практичное решение, которое действительно добавляет ценность!
Флориан Хейгл