В bash, как захватить stdout и код завершения команды, когда активен флаг -e?

Вопрос или проблема

Чтобы захватить stdout и код выхода, когда флаг -e (выйти из оболочки немедленно, если команда завершилась неудачно) не установлен, я бы использовал

OUTPUT="$(my_command)"
exit_code=$?

Чтобы захватить код выхода при установленном -e, я бы использовал

exit_code=0
my_command || exit_code=$?

Но комбинировать два подхода не получится:

exit_code=0
OUTPUT="$(my_command || exit_code=$?)"

Это не установит exit_code, потому что он присваивается в подсистеме, и копия в родительской оболочке не обновляется.

Так как же мне захватить stdout и код выхода? Я мог бы отключить -e или, например, записать код выхода во временный файл, но я хотел бы что-то проще.

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

Решение:

exit_code=0
OUTPUT="$(my_command)" || exit_code=$?

то есть, переместите || exit_code=$? за пределы подсистемы.

Обратите внимание: не добавляйте перед этим export или local, так как это перезапишет код выхода my_command. Устанавливать переменные как local или export можно в отдельной команде.

Это сводится к как отключить эффект errexit для одной команды, здесь для присваивания переменной.

errexit отменяется, когда команда участвует в условии, как в

  • if cmd; then... (то же самое с while/until; то же в if cmd; other-cmd; then...)
  • ! cmd
  • cmd || ...
  • cmd && ...

В output=$(cmd) у вас две команды:

  1. простая команда присвоения переменной
  2. внутри подсистемы, вывод которой захватывается для создания значения этого присваивания, еще одна простая команда cmd:

При включенном errexit, если cmd завершается неудачно, подсистема выходит (с тем же статусом выхода), например, вы обнаружите, что в var=$(false; echo здесь>&2), echo здесь не выполняется; статус выхода присваивания переменной будет статусом этой подсистемы, поэтому также неудачным, и процесс оболочки, интерпретирующий это присваивание, тоже выйдет.

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

В:

if
  ! output=$(cmd)
then
  echo>&2 Это неудача
fi

errexit отключен как для присваивания переменной, так и для cmd, и если cmd сама является функцией, например, для каждой команды, которую эта функция выполняет, даже в подсистемах, даже если эта функция явно вызывает set -o errexit / set -e сама.

Здесь, если вы хотите обработать сбой cmd, независимо от того, включен errexit или нет, вы можете использовать если ! output=$(cmd); затем выше или:

output=$(cmd) || {
  echo>&2 Обработка сбоя
  exit 1
}

Если вам необходимо получить фактическое значение статуса выхода в обработчике, вы можете сделать:

if
  output=$(cmd)
  exit_status=$?
  [ "$exit_status" -ne 0 ]
then
  echo>&2 "Это неудача со статусом $exit_status"
  ...
fi

Если вы просто хотите записать статус выхода пока, возможно для обработки его позже, канонический способ просто отменить errexit для одной команды (с оговоркой, как указано выше, что это применяется рекурсивно), заключается в добавлении && true.

cmd || true

Это означает, что вы хотите полностью игнорировать любые сбои cmd. Не только errexit отменяется, но и общий статус выхода будет отражать успех. В то время как:

cmd && true

Отменяет errexit, но также сохраняет статус выхода cmd в статусе выхода всего списка и/или. Итак:

output=$(cmd) && true
exit_status=$?
other_commands
if [ "$exit_status" -ne 0 ]; then
  echo>&2 "О, кстати, cmd неудачно завершился ранее со статусом $exit_status"
fi

Если код был output=$(cmd1; cmd2), вы, вероятно, захотите изменить его на output=$(cmd1 && cmd2), потому что, с отмененным errexit, cmd2 в противном случае будет выполнен даже если cmd1 завершился неудачно.

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

Один из возможных способов — использовать команду вроде этой:

OUTPUT="$(my_command)" ; exit_code=$?

С этим вы избегаете одной строки и напрямую присваиваете код выхода

Если установлен -e, вы можете временно отключить его, выполнить команду и затем установить его снова. Здесь вы можете найти пример скрипта для этого.

Ответ или решение

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

Проблема

Флаг -e является важным инструментом в Bash-скриптах, поскольку он позволяет обнаруживать и реагировать на ошибки. Однако его использование затрудняет захват кода завершения при выполнении команд, поскольку при возникновении ошибки, выполнение скрипта прерывается. В стандартной форме присвоения переменной, такой как:

OUTPUT="$(my_command)"
exit_code=$?

Этот код не сработает, если my_command завершится с ошибкой, потому что выполнение остановится до того, как будет выполнена операция получения кода завершения.

Решение

Проблему можно решить, переместив проверку кода завершения за пределы подсчета (subshell):

exit_code=0
OUTPUT="$(my_command)" || exit_code=$?

Этот метод позволяет сначала присвоить значение переменной OUTPUT, а затем, в случае неудачи, установить переменную exit_code.

Дополнительные нюансы

  • Не используйте export или local: Если вы используете ключевое слово local или export, это может перезаписать значение exit-кода my_command, что приведет к нежелательным последствиям. Важно сохранять переменную exit_code как локальную для вашей логики.

  • Отключение errexit для одной команды: errexit отменяется при использовании условия, например:

    if ! OUTPUT="$(my_command)"; then
      echo "Команда завершилась неудачно"
    fi

    В этом случае errexit будет временно отключен, и вы сможете обрабатывать ошибки без выхода из скрипта.

  • Работа с кодами завершения: Если вам нужно захватить код завершения для дальнейшей обработки, можно использовать следующий пример:

    if OUTPUT="$(my_command)"; then
      exit_status=0
    else
      exit_status=$?
      echo "Ошибка выполнения команды с кодом: $exit_status"
    fi
  • Игнорирование ошибок: Использование конструкций cmd || true позволяет игнорировать ошибки, однако чаще всего лучше обрабатывать ошибки и принимать соответствующие меры.

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...