Как использовать `set -e` внутри замены команды bash?

У меня есть простой сценарий оболочки со следующей преамбулой:

#!/usr/bin/env bash
set -eu
set -o pipefail

У меня также есть следующая функция:

foo() {
  printf "Foo working... "
  echo "Failed!"
  false  # point of interest #1
  true   # point of interest #2
}

Выполнение foo() как обычной команды работает так, как ожидалось: сценарий завершается в #1, потому что код возврата false не равен нулю, а мы используем set -e.

Моя цель — зафиксировать вывод функции foo() в переменной и распечатать его только в случае возникновения ошибки во время выполнения foo(). Вот что я придумал:

printf "Doing something that could fail... "
if a="$(foo 2>&1)"; then
  echo "Success!"
else
  code=$?
  echo "Error:"
  printf "${a}"
  exit $code
fi

Сценарий не завершается в #1, и выполняется путь "Success!" оператора if. Комментирование true в #2 приводит к выполнению пути "Error:" оператора if.

Кажется, что bash просто игнорирует set -e внутри подстановки, а оператор if просто проверяет код возврата последней команды в foo().

В: Что вызывает такое странное поведение?

A: Именно так работает bash, это нормальное поведение.

В: Есть ли способ заставить bash уважать set -e внутри подстановки команд и сделать так, чтобы это работало правильно?

A: Вы не должны использовать set -e для этой цели.

В: Как бы вы реализовали это без set -e (т. е. печатать вывод функции только в том случае, если что-то пошло не так во время ее выполнения)?

A: См. принятый ответ и мой раздел «последние мысли».

Я использую:

GNU bash, версия 5.0.11(1)-выпуск (x86_64-apple-darwin18.6.0)

Заключительные мысли/выводы (может быть полезным для кого-то еще):

Имейте в виду, что использование if ...; then или даже && ... || ... отключит большинство "традиционных" методов обработки ошибок bash (включая set -e и trap ... ERR + set -o errtrace) преднамеренно. Если вы хотите сделать что-то вроде того, что сделал я, вам, вероятно, следует вручную проверить коды возврата внутри вашей функции и вручную вернуть ненулевой код выхода (dangerous_command || return 1), чтобы избежать продолжения выполнения при ошибках (вы можете сделать это независимо от того, используете ли вы set -e или нет).

Как указано в ответе, set -e не распространяется внутри подстановок команд по дизайну. Если вы хотите реализовать логику обработки ошибок, вы можете использовать trap ... ERR в сочетании с set -o errtrace, которые будут работать с функциями, работающими внутри подстановок команд (то есть, если вы не поместите их в оператор if, который также отключит trap ... ERR, поэтому в в этом случае ручная проверка кода возврата — ваш единственный вариант, если вы хотите остановить свою функцию при ошибках).

Если подумать, все это поведение имеет смысл: вы не ожидаете, что ваш сценарий завершится по команде, «защищенной» оператором if, так как весь смысл вашего оператора if проверяет, успешно ли выполнена команда или нет. .

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


person krispet krispet    schedule 14.11.2019    source источник
comment
Это задокументировано, чтобы работать так. Прочтите о -e в разделе gnu.org/software/bash/ manual/bash.html#The-Set-Builtin   -  person glenn jackman    schedule 14.11.2019
comment
ИМО, вы не должны полагаться на set -e, а использовать условные команды для условного выполнения.   -  person glenn jackman    schedule 14.11.2019
comment
В: Как использовать set -e? А: просто не надо   -  person William Pursell    schedule 14.11.2019
comment
В этом случае bash не игнорирует set -e. Он ведет себя так, как задумано. К сожалению, то, как он спроектирован, не позволяет вам использовать его так, как вы хотите. Это кладж на пластыре. Просто не используйте его.   -  person William Pursell    schedule 14.11.2019
comment
Спасибо, я перефразировал вопрос на основе ваших комментариев.   -  person krispet krispet    schedule 14.11.2019
comment
false не return false. Вы возвращаете ноль, поэтому родительская оболочка считает, что ваша функция выполнена успешно.   -  person KamilCuk    schedule 14.11.2019


Ответы (3)


В: Как бы вы реализовали это без set -e (т. е. печатать вывод функции только в том случае, если что-то пошло не так при ее выполнении)?

Вы можете использовать этот способ, проверив возвращаемое значение функции:

#!/usr/bin/env bash

foo() {
  local n=$RANDOM
  echo "Foo working with random=$n ..."
  (($n % 2))
}

echo "Doing something that could fail..."
a="$(foo 2>&1)"
code=$?
if (($code == 0)); then
  echo "Success!"
else
  printf '{"ErrorCode": %d, "ErrorMessage": "%s"}\n' $code "$a"
  exit $code
fi

Теперь запустите его как:

$> ./errScript.sh
Doing something that could fail...
Success!
$> ./errScript.sh
Doing something that could fail...
{"ErrorCode": 1, "ErrorMessage": "Foo working with random=27662 ..."}
$> ./errScript.sh
Doing something that could fail...
Success!
$> ./errScript.sh
Doing something that could fail...
{"ErrorCode": 1, "ErrorMessage": "Foo working with random=31864 ..."}

Этот фиктивный код функции возвращает ошибку, если $RANDOM — четное число, и успех, если $RANDOM — нечетное число.


Оригинальный ответ на оригинальный вопрос

Вам также необходимо включить set -e в подстановке команд:

#!/usr/bin/env bash
set -eu
set -o pipefail

foo() {
  printf "Foo working... "
  echo "Failed!"
  false  # point of interest #1
  true   # point of interest #2
}

printf "Doing something that could fail... "
a="$(set -e; foo)"
code=$?
if (($code == 0)); then
  echo "Success!"
else
  echo "Error:"
  printf "${a}"
  exit $code
fi

Затем используйте его как:

./errScript.sh; echo $?
Doing something that could fail... 1

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

Проверьте этот важный пост на set -e

person anubhava    schedule 14.11.2019
comment
Код в разделе Исходный ответ на исходный вопрос не работает (проверено с Bash 4.2). Поскольку в преамбуле установлено errexit, статус ошибки из foo приводит к выходу программы из строки a="$(set -e; foo)". Когда errexit активно, обычно нет смысла сохранять или проверять значение $?, потому что программа уже завершила работу, если оно было ненулевым. - person pjh; 14.11.2019
comment
Да все верно. Он будет работать только тогда, когда set -e не установлен в вызывающем скрипте. - person anubhava; 14.11.2019

Как бы вы реализовали это без set -e (т.е. печатать вывод функции, только если что-то пошло не так во время ее выполнения)?

Верните ненулевой статус возврата из вашей функции, чтобы указать на ошибку/сбой.

foo() {
  printf "Foo working... "
  echo "Failed!"
  return 1  # point of interest #1
  return 0   # point of interest #2
}

if a="$(foo 2>&1)"; then
  echo "Success!"
else
  code=$?
  echo "Error:"
  printf "${a}"
  exit $code
fi
person KamilCuk    schedule 14.11.2019

Как заявляли другие, errexit не является надежным способом устранения ошибок в программах. Просто одна из больших проблем с ним заключается в том, что он автоматически отключается в нескольких распространенных ситуациях, в том числе при подстановке команд.

Если вы все еще хотите использовать errexit, есть несколько способов добиться желаемого эффекта.

Один из способов сделать это — временно отключить errexit в основном коде, явно включить errexit в подстановке команд (как показано в ответе @anubhava), получить код выхода подстановки команд из $? и повторно включить errexit в основной код.

Другой возможный способ сделать это (после преамбулы и кода определения foo в вопросе):

shopt -s lastpipe

printf "Doing something that could fail... "
set +o pipefail
foo 2>&1 | { read -r -d '' a || true; }
code=${PIPESTATUS[0]}
set -o pipefail

if (( code == 0 )); then
    echo "Success!"
else
    echo "Error:"
    printf '%s\n' "$a"
    exit "$code"
fi
  • shopt -s lastpipe вызывает выполнение последней команды конвейеров в оболочке верхнего уровня. Это означает, что переменные, установленные в командах в конце конвейеров (например, a в данном случае), могут использоваться позже в программе. lastpipe был введен в Bash 4.2, поэтому этот код не будет работать со старыми версиями Bash.
  • set +o pipefail (временно) отключает pipefail, чтобы предотвратить сбой foo в начале конвейера, вызывающий сбой всего конвейера.
  • read -r -d '' a считывает весь свой ввод (предполагается, что он не содержит символа NUL), включая внутренние символы новой строки, в переменную a.
  • { ... || true; } вокруг read скрывает ненулевой статус, возвращаемый read, когда он встречает EOF на своем входе, тем самым предотвращая сбой конвейера.
  • code=${PIPESTATUS[0]} фиксирует состояние первой команды в конвейере (foo).
  • set -o pipefail повторно включает pipefail, чтобы он был включен для остальной части программы.
  • В код вопроса было внесено несколько изменений, чтобы отключить предупреждения Shellcheck.
person pjh    schedule 14.11.2019