Как получить эффект и полезность set -e внутри функции оболочки?

set -e (или скрипт, начинающийся с #!/bin/sh -e) чрезвычайно полезен для автоматического сброса в случае возникновения проблемы. Это избавляет меня от необходимости проверять на ошибки каждую команду, которая может завершиться ошибкой.

Как мне получить эквивалент этого внутри функции?

Например, у меня есть следующий скрипт, который немедленно завершает работу при ошибке со статусом завершения ошибки:

#!/bin/sh -e

echo "the following command could fail:"
false
echo "this is after the command that fails"

Результат ожидаемый:

the following command could fail:

Теперь я хотел бы обернуть это в функцию:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Ожидаемый результат:

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function

Фактический результат:

the following output could fail:
this is after the command that fails
run this all the time regardless of the success of my_function

(т.е. функция игнорирует set -e)

Это предположительно ожидаемое поведение. Мой вопрос: как мне получить эффект и полезность set -e внутри функции оболочки? Я хотел бы иметь возможность настроить что-то, чтобы мне не приходилось индивидуально проверять ошибки при каждом вызове, но сценарий останавливался при обнаружении ошибки. Он должен раскручивать стек настолько, насколько это необходимо, пока я не проверю результат или не выйду из самого скрипта, если я его не проверил. Это то, что уже делает set -e, за исключением того, что он не вложен.

Я нашел тот же вопрос спросил вне Stack Overflow, но не нашел подходящего ответа.


person Robie Basak    schedule 01.11.2010    source источник
comment
Этот вопрос не является дубликатом. Я задал этот вопрос за три года до другого вопроса. Этот вопрос конкретно касается поиска способа добиться того же поведения, что и set -e, но внутри функции. Другой вопрос просто спрашивает о подобном удивительном поведении. У этого вопроса гораздо больше голосов, звезд, просмотров и полезных ответов, охватывающих различные альтернативы, и я думаю, что имеет гораздо больше смысла оставлять его открытым, чем другой. Если что, пометьте дубликат наоборот.   -  person Robie Basak    schedule 23.03.2018
comment
Справедливости ради, однако, ответ на другой вопрос очень точен, а также отвечает на этот вопрос. Я недостаточно разбираюсь в bash, чтобы решить, что куда и как следует использовать, но если владелец золотого значка так думает, я склонен последовать голосованию за обман.   -  person Félix Adriyel Gagnon-Grenier    schedule 26.03.2018
comment
Ничего не имею против другого вопроса. Этот вопрос, однако, здесь для разработчиков, которые хотят решить конкретный вопрос, который я задал. Понятно, что хорошего ответа нет, но все ответы здесь дают что-то полезное. Я не думаю, что уместно держать этот вопрос закрытым для новых ответов, которые могут быть столь же полезными. Предоставление ответа в другом вопросе вместо этого скроет его, поскольку этот вопрос задает немного другой вопрос (почему это происходит? а не как я могу этого добиться?).   -  person Robie Basak    schedule 28.03.2018
comment
Я отредактировал заголовок (теперь взятый из исходного тела вопроса), чтобы попытаться сделать это более понятным.   -  person Robie Basak    schedule 28.03.2018
comment
Что не так с установкой ловушки ошибок в соответствии с дубликатом?   -  person jthill    schedule 14.11.2018
comment
Ловушка ошибок не приведет к раскручиванию стека.   -  person Robie Basak    schedule 14.11.2018
comment
comment
Вы можете имитировать set -e с ловушкой DEBUG. См. мой ответ ниже: stackoverflow.com/a/62707941/2668666   -  person jmrah    schedule 03.07.2020


Ответы (10)


Из документации set -e:

Если этот параметр включен, если простая команда не выполняется по любой из причин, перечисленных в Последствия ошибок оболочки, или возвращает значение состояния выхода > 0 и не является частью составного списка, следующего за ключевым словом while, until или if, и не является частью списка AND или OR и не является конвейером, которому предшествует зарезервированное слово !, то оболочка должна немедленно завершить работу.

В вашем случае false является частью конвейера, которому предшествует ! и часть if. Таким образом, решение состоит в том, чтобы переписать ваш код так, чтобы это было не так.

Другими словами, здесь нет ничего особенного в функциях. Пытаться:

set -e
! { false; echo hi; }
person Roman Cheplyaka    schedule 01.11.2010
comment
Там нет трубопровода. Это составная команда, а не простая команда. - person Dennis Williamson; 02.11.2010
comment
Есть конвейер, хотя и вырожденный. Если вы сверитесь со стандартом (в частности, 2.10.2 Правила грамматики оболочки), вы увидите, что конвейер состоит как минимум из одной команды (которая может быть простой, состоящей из func.def.), которой предшествует необязательный знак взрыва. Нет другого способа ввести отрицание, кроме конвейера. - person Roman Cheplyaka; 02.11.2010
comment
Теперь, быть частью действительно неоднозначно. Можно было бы ожидать (и я так и сделал), что это относится к непосредственным частям. В этой интерпретации false будет частью группы фигурных скобок, а группа фигурных скобок будет частью конвейера, но false не будет частью конвейера. Но, по-видимому, все реализации, которые у меня есть, разделяют другое мнение - где часть оператора может быть встроена в этот оператор как бы глубоко. - person Roman Cheplyaka; 02.11.2010
comment
s/соединение func.def./compound или func.def./ - person Roman Cheplyaka; 02.11.2010
comment
Любая ссылка на документацию set -e? - person Dmitri Zaitsev; 27.04.2015
comment
@DmitriZaitsev стандарт POSIX - person Roman Cheplyaka; 28.04.2015
comment
Это объясняет поведение - спасибо - но на самом деле это не решает мою проблему. Как получить поведение set -e, но локализованное для функции? Я хочу написать свою функцию, не заботясь о том, успешны или нет отдельные простые команды, и чтобы функция немедленно возвращала ненулевое значение (предположительно $?), как только какая-либо из этих простых команд терпит неудачу. Как указывает Гинтаутас в другом ответе, объединение с && сделает это, но я хочу что-то, что означает, что для удобства мне не нужно менять тело функции. - person Robie Basak; 16.02.2016
comment
@RobieBasak Я понимаю, почему вы спрашиваете об этом, но ваш запрос на самом деле не имеет смысла. set -e применяется к содержимому функций, если только функция не вызывается одним из способов, исключенных из set -e, как описано в документации, на которую ссылается Роман. set -e применяется только к определенным контекстам вызова, поэтому, если вы хотите, чтобы ваша функция возвращалась, как только что-то пойдет не так, независимо от того добавление || return к каждой команде (или объединение всего с помощью &&) является стандартным решением. - person dimo414; 17.03.2017
comment
Иными словами, set -e — это неудобный зверь, и часто это не совсем то, что вы хотите, чтобы ваш скрипт делал, даже если он кажется действительно полезным молотком. - person dimo414; 17.03.2017
comment
Длинное имя набора -e для поиска в Google: errexit - person MarcH; 14.11.2018

Вы можете напрямую использовать подоболочку в качестве определения функции и настроить ее на немедленный выход с помощью set -e. Это ограничит область действия set -e только функциональной подоболочкой и впоследствии позволит избежать переключения между set +e и set -e.

Кроме того, вы можете использовать присваивание переменной в тесте if, а затем повторить результат в дополнительном операторе else.

# use subshell for function definition
f() (
   set -exo pipefail
   echo a
   false
   echo Should NOT get HERE
   exit 0
)

# next line also works for non-subshell function given by agsamek above
#if ret="$( set -e && f )" ; then 
if ret="$( f )" ; then
   true
else
   echo "$ret"
fi

# prints
# ++ echo a
# ++ false
# a
person philz    schedule 08.03.2013
comment
Интересный. if ret=$( set -e; f );then рано выходит из f с dash -e, но не с bash -e. Я определил f() { echo a; false; echo "Should NOT get HERE"; }, так как хотел протестировать его с помощью нормально определенных функций, а не функций подоболочки. - person Peter Cordes; 14.11.2015
comment
Я знаю, что это старый ответ, но он не работает для меня в Bash 4.4 и Bash 5. Я получаю вывод «Не следует получать ЗДЕСЬ». Мой вывод: ++ echo a ++ false ++ echo Should NOT get HERE ++ exit 0 Это все еще работает для вас? - person jmrah; 02.07.2020

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

Отключите set -e, затем запустите вызов функции внутри подоболочки с включенным set -e. Сохраните статус выхода подоболочки в переменной, снова включите set -e, затем проверьте переменную.

f() { echo "a"; false;  echo "Should NOT get HERE"; }

# Don't pipe the subshell into anything or we won't be able to see its exit status
set +e ; ( set -e; f ) ; err_status=$?
set -e

## cleaner syntax which POSIX sh doesn't support.  Use bash/zsh/ksh/other fancy shells
if ((err_status)) ; then
    echo "f returned false: $err_status"
fi

## POSIX-sh features only (e.g. dash, /bin/sh)
if test "$err_status" -ne 0 ; then
    echo "f returned false: $err_status"
fi

echo "always print this"

Вы не можете запускать f как часть конвейера или как часть списка команд && из || (кроме как последней команды в конвейере или списке), или как условие в if или while, или в других контекстах, которые игнорируют set -e. Этот код также не может находиться ни в одном из этих контекстов, поэтому, если вы используете это в функции, вызывающие должны использовать ту же хитрость подоболочки / сохранения-выхода-статуса. Такое использование set -e для семантики, аналогичной генерированию/перехвату исключений, на самом деле не подходит для общего использования, учитывая ограничения и трудный для чтения синтаксис.

trap err_handler_function ERR имеет те же ограничения, что и set -e, в том смысле, что он не срабатывает при ошибках в контекстах, где set -e не завершает работу при неудачных командах.

Вы можете подумать, что следующее сработает, но это не так:

if ! ( set -e; f );then    ##### doesn't work, f runs ignoring -e
    echo "f returned false: $?"
fi

set -e не действует внутри подоболочки, потому что она запоминает, что находится внутри условия if. Я думал, что наличие подоболочки изменит это, но сработает только нахождение в отдельном файле и запуск на нем целой отдельной оболочки.

person antak    schedule 19.06.2012
comment
Примечание. Это не будет работать, если это часть функции, которая снова используется с if, ||, && и т. д. :( - person jomo; 25.08.2015
comment
более красивый способ написать тест: if ((EXIT)); then echo "f failed"; fi. Арифметические контексты очень хороши для выполнения логических операций. - person Peter Cordes; 14.11.2015

Это немного глупо, но вы можете сделать:

export -f f
if sh -ec f; then 
...

Это будет работать, если ваша оболочка поддерживает экспорт -f (bash поддерживает).

Обратите внимание, что это не завершит выполнение скрипта. Эхо после false в f не будет выполнено, равно как и тело if, но будут выполнены операторы после if.

Если вы используете оболочку, не поддерживающую export -f, вы можете получить нужную семантику, запустив sh в функции:

f() { sh -ec '
  echo This will execute
  false
  echo This will not
  '
}
person William Pursell    schedule 22.04.2011
comment
Просто чтобы уточнить, так как -f f выглядело запутанным. Если бы у меня была функция whatsamacallit, стал бы я использовать sh -ec whatsamacallit, чтобы функция возвращалась при первой ошибке? - person Ehtesh Choudhury; 05.04.2013
comment
@Shurane Да, замените f на имя функции. - person William Pursell; 05.04.2013

Соедините все команды в вашей функции с оператором &&. Это не слишком сложно и даст желаемый результат.

person Gintautas Miliauskas    schedule 01.11.2010
comment
Да, я понимаю, что могу это сделать, но мне это не нравится. Это запутанно, слишком легко пропустить одну из них при изменении функции позже, и усложняется при работе с другими конструкциями оболочки и потоком управления. Весь смысл set -e в том, чтобы избежать этого. Я приму ответ, что такого механизма нет! - person Robie Basak; 02.11.2010
comment
set -e не имеет смысла, потому что это кладж. Я использую его все время, потому что он много раз экономил мне много времени, но у меня очень низкие ожидания, как и у всех. - person MarcH; 14.11.2018

Это предусмотрено дизайном и спецификацией POSIX. Мы можем прочитать в man bash:

Если составная команда или функция оболочки выполняется в контексте, где -e игнорируется, ни одна из команд, выполняемых в составе составной команды или тела функции, не будет затронута параметром -e, даже если установлен -e и команда возвращает статус ошибки. Если составная команда или функция оболочки устанавливает -e во время выполнения в контексте, где -e игнорируется, этот параметр не будет иметь никакого эффекта, пока составная команда или команда, содержащая вызов функции, не завершится.

поэтому вам следует избегать полагаться на set -e внутри функций.

В следующем примереОстин Групп:

set -e
start() {
   some_server
   echo some_server started successfully
}
start || echo >&2 some_server failed

set -e внутри функции игнорируется, потому что функция является командой в списке И-ИЛИ, отличном от последнего.

Вышеупомянутое поведение указано и требуется POSIX (см.: Желаемое действие):

Настройка -e должна игнорироваться при выполнении составного списка, следующего за зарезервированным словом while, until, if или elif, конвейера, начинающегося с зарезервированного слова !, или любой команды списка И-ИЛИ, отличной от последней.

person kenorb    schedule 16.01.2016

Примечание/редактирование. Как отметил комментатор, в этом ответе используется bash, а не sh, как в OP, использованном в его вопросе. Я пропустил эту деталь, когда изначально опубликовал ответ. Я все равно оставлю этот ответ, так как он может заинтересовать кого-нибудь из прохожих.

Вы готовы к этому?

Вот способ сделать это с помощью ловушки DEBUG, которая запускается перед каждой командой и создает ошибки, подобные всем идиомам исключения/попытки/перехвата из других языков. Посмотри. Я сделал ваш пример еще одним «звонком» вглубь.

#!/bin/bash

# Get rid of that disgusting set -e.  We don't need it anymore!
# functrace allows RETURN and DEBUG traps to be inherited by each
# subshell and function.  Plus, it doesn't suffer from that horrible
# erasure problem that -e and -E suffer from when the command 
# is used in a conditional expression.
set -o functrace

# A trap to bubble up the error unless our magic command is encountered 
# ('catch=$?' in this case) at which point it stops.  Also don't try to
# bubble the error if were not in a function.
trap '{ 
    code=$?
    if [[ $code != 0 ]] && [[ $BASH_COMMAND != '\''catch=$?'\'' ]]; then
        # If were in a function, return, else exit.
        [[ $FUNCNAME ]] && return $code || exit $code
    fi
}' DEBUG

my_function() {
    my_function2
}

my_function2() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

# the || isn't necessary, but the 'catch=$?' is.
my_function || catch=$?
echo "Dealing with the problem with errcode=$catch (⌐■_■)"

echo "run this all the time regardless of the success of my_function"

и вывод:

the following command could fail:
Dealing with the problem with errcode=1 (⌐■_■)
run this all the time regardless of the success of my_function

Я не проверял это в дикой природе, но на мой взгляд, есть куча плюсов:

  1. На самом деле не так уж и медленно. Я запускал скрипт в тесном цикле с параметром functrace и без него, и время очень близко друг к другу до 10 000 итераций.

  2. Вы можете расширить эту ловушку DEBUG, чтобы распечатать трассировку стека без выполнения всего этого цикла по $FUNCNAME и $BASH_LINENO ерунды. Вы как бы получаете это бесплатно (кроме фактического выполнения эхо-линии).

  3. Не нужно беспокоиться об этом shopt -s inherit_errexit.

person jmrah    schedule 03.07.2020
comment
Интересный. Но я должен отметить, что OP использовала оболочку bourne, а не bash. - person marcelm; 19.11.2020
comment
Вы совершенно правы. Я пропустил эту деталь :). Спасибо, что указали на это. Я отредактировал ответ, чтобы отразить это важное различие. - person jmrah; 20.11.2020
comment
нп; это все еще интересное решение. Есть +1! - person marcelm; 20.11.2020

Я знаю, что это не то, о чем вы спрашивали, но вы можете знать или не знать, что поведение, которое вы ищете, встроено в «сделать». Любая часть процесса «создания», которая завершается с ошибкой, прерывает выполнение. Однако это совершенно другой способ «программирования», чем сценарии оболочки.

person jcomeau_ictx    schedule 01.11.2010

Для этого вам нужно будет вызвать вашу функцию во вспомогательной оболочке (внутри квадратных скобок ()).

Я думаю, вы хотите написать свой скрипт следующим образом:

#!/bin/sh -e

my_function() {
    echo "the following command could fail:"
    false
    echo "this is after the command that fails"
}

(my_function)

if [ $? -ne 0 ] ; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

Тогда вывод (по желанию):

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function
person lothar    schedule 01.11.2010

Если подоболочка не подходит (скажем, вам нужно сделать что-то сумасшедшее, например, установить переменную), вы можете просто проверить каждую команду, которая может дать сбой, и исправить это, добавив || return $?. Это заставляет функцию возвращать код ошибки в случае сбоя.

Это некрасиво, но это работает

#!/bin/sh
set -e

my_function() {
    echo "the following command could fail:"
    false || return $?
    echo "this is after the command that fails"
}

if ! my_function; then
    echo "dealing with the problem"
fi

echo "run this all the time regardless of the success of my_function"

дает

the following command could fail:
dealing with the problem
run this all the time regardless of the success of my_function
person cobbal    schedule 06.11.2019