Предотвращение преждевременного завершения скрипта “bash -e” из-за grep

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

Этот скрипт не выводит after:

#!/bin/bash -e

echo "до"

echo "что угодно" | grep e # он бы вывел, если бы я искал 'y' вместо этого

echo "после"
exit

Я мог бы решить это, удалив опцию -e в строке shebang, но я хочу оставить её, чтобы мой скрипт останавливался в случае ошибки. Я не считаю отсутствие совпадения в grep ошибкой. Как я могу предотвратить его резкое завершение?

echo "что угодно" | { grep e || true; }

Объяснение:

  • Это вызовет ошибку
    $ echo "что угодно" | grep e
    $ echo $?
    1
    
  • Это не вызовет ошибку
    $ echo "что угодно" | { grep e || true; }
    $ echo $?
    0
    
  • Версия “no-op” от DopeGhoti (потенциально избегает создания процесса, если true не является встроенной командой), это не вызовет ошибку
    $ echo "что угодно" | { grep e || :; }
    $ echo $?
    0
    

|| означает “или”. Если первая часть команды “проваливается” (то есть grep e возвращает ненулевой код выхода), то выполняется часть после ||, успешна и возвращает ноль как код выхода (true всегда возвращает ноль).

Надежный способ безопасно и опционально использовать grep для сообщений:

echo что угодно | grep e || [[ $? == 1 ]] ## выводит 'что угодно', $? равно 0
echo что угодно | grep x || [[ $? == 1 ]] ## нет вывода, $? равно 0
echo что угодно | grep --неправильный-аргумент e || [[ $? == 1 ]] ## вывод stderr, $? равно 1

Согласно posix мануалу, код выхода:

  • 1 означает, что не было выбранных строк.
  • > 1 означает ошибку.

Если вы используете grep с set -euo pipefail и не хотите выходить, если запись не найдена (код выхода = 1), но все же хотите, чтобы он завершился, если это другой код выхода, вы можете использовать это:

#!/bin/bash
set -euo pipefail

echo "что угодно" | { grep e || test $? = 1; } | { grep e2 || test $? = 1; }

Грепы внутри пайпов будут ИСКЛЮЧИТЕЛЬНО игнорировать код выхода = 1. Таким образом, вам не нужно терять преимущества использования set -euo pipefail.

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

Обратитесь к посту myrdd там.

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

echo "что угодно" | grep e | cat

Поскольку cat теперь является последней командой в пайплайне, код выхода будет определяться не grep, а cat.

Другой вариант:

...
set +e
echo "что угодно" | grep e
set -e
...

Решение

#!/bin/bash -e

echo "до"

echo "что угодно" | grep e || : # он бы вывел, если бы я искал 'y' вместо этого

echo "после"
exit

Объяснение

set -e или set -o errexit

Выходить немедленно, если пайплайн (который может состоять из одной простой команды), список или составная команда (см. SHELL GRAMMAR выше) завершится с ненулевым статусом. Оболочка не завершится, если команда, которая завершилась с ошибкой, является частью списка команд, непосредственно следующего за ключевыми словами while или until, частью условия после зарезервированных слов if или elif, частью любой команды, выполняемой в списке && или ||, кроме команды, следующие за последним && или ||, любой команды в пайплайне, кроме последней, или если значение возврата команды инвертируется с помощью !. Если составная команда, отличная от подсистемы, возвращает ненулевой статус из-за того, что команда завершилась с ошибкой, когда -e игнорировалась, оболочка не завершится. Захват на ERR, если установлен, выполняется перед выходом оболочки. Эта опция применяется к окружению оболочки и к каждой среде подсистемы отдельно (см. COMMAND EXECUTION ENVIRONMENT выше) и может вызвать выход из подсистемы до выполнения всех команд в подсистеме.

Кроме того, : это команда без эффекта в Bash.

Альтернатива: используйте sed вместо grep:

echo "что угодно" | sed -n '/e/p'  # не выводит ничего
echo "что угодно" | sed -n '/y/p'  # выводит "что угодно"

Объяснение: sed заставляет подавлять любой ввод (-n), кроме как для вывода (p), когда он соответствует регулярному выражению.

Возвратное значение равно 0 в обоих случаях. Работает на современном Linux и старом Solaris 8.

Проблема, которую я имею с большинством, а возможно, и со всеми ответами здесь, заключается в том, что они теряют истинный код выхода ($?) и в основном возвращают $? как 0. Нет способа отличить, было ли совпадение (код выхода 0) или не было совпадения (код выхода 1). Это решение решает эту проблему:

#!/bin/bash -e

echo "до"

exit_code=0
echo "что угодно" | grep e || exit_code=$?; if [[ $exit_code -ne 1 ]]; then (exit $exit_code); fi

echo "после"
exit

Объяснено

Код состояния grep — это либо 0, 1 или 2:

  • 0 означает, что было совпадение
  • 1 означает, что совпадения не было
  • 2 означает, что произошла ошибка

Теперь, с приведенным выше решением:

  • Если grep возвращает 0, тест не выполняется и возвращает истинный код выхода $? равный 0, а также устанавливает $exit_code=0.
  • Если grep возвращает 1, тест выполняется и возвращает фиктивный код выхода $? равный 0, но мы также устанавливаем $exit_code=1, который может быть использован будущим кодом для различения совпадения и отсутствия совпадения.
  • Если grep возвращает любое другое значение, тест выполняется и возвращает истинный код выхода больше 1, который должен правильно остановить скрипт, мы также устанавливаем $exit_code соответствующим образом, но это не имеет значения, потому что скрипт остановится.

Большое спасибо @rouble за то, что поделились своим решением – спасли меня, я боролся с этим в GitHub Workflow больше дня.

Для тех, кто хочет перехватить не только правильный код выхода, но и вывод команды grep, вот что сработало для меня:

echo "до"

grep_exit_code=0
grep_output=$(echo "a" | grep "b") || grep_exit_code=$? ; if [[ $grep_exit_code -ne 1 ]]; then (exit $grep_exit_code); fi

echo "grep_exit_code=$grep_exit_code"
echo "grep_output=$grep_output"

echo "после"

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

Предотвращение преждевременного завершения скрипта Bash с использованием grep

Введение

Скрипты на Shell очень популярны в системном администрировании и автоматизации задач. Однако использование set -e (или -o errexit) может привести к неожиданным результатам при работе с командами, которые возвращают ненулевые коды выхода. Это, в частности, проявляется при использовании команды grep, когда она не находит совпадений. В данном руководстве мы обсудим, как избежать преждевременного завершения скрипта при использовании grep, сохраняя при этом возможность отслеживать ошибки.

Пример проблемы

Рассмотрим следующий скрипт:

#!/bin/bash -e

echo "before"

echo "anything" | grep e # ничего не выводит, если искать 'y'

echo "after"
exit

Вывод будет следующим:

before

Скрипт не доходит до строки с echo "after" потому что grep возвращает код выхода 1, указывающий на отсутствие совпадений, что и приводит к завершению скрипта.

Решение проблемы

1. Используйте || true

Один из простых способов обработки этого случая — использовать || true, чтобы гарантировать успешное завершение команды:

echo "anything" | grep e || true

Таким образом, если grep не найдет совпадений, код выхода будет равен 0.

2. Ненавязчивое игнорирование кода выхода

Можно также использовать конструкцию с фигурными скобками, чтобы контролировать поведение выполнения:

echo "anything" | { grep e || true; }

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

3. Условная проверка кода выхода

Для более точного контроля можно использовать условную проверку кода выхода:

exit_code=0
echo "anything" | grep e || exit_code=$?
if [[ $exit_code -ne 1 ]]; then
    exit $exit_code
fi

Таким образом, вы сможете различать успешное выполнение (0), отсутствие совпадений (1) и ошибки (>1).

4. С использованием sed

Другой возможный подход — использование sed вместо grep, если вам не критично, которая утилита используется:

echo "anything" | sed -n '/e/p'  # Ничего не выводит
echo "anything" | sed -n '/y/p'  # Выводит "anything"

sed не возвращает код выхода 1 при отсутствии совпадений, что делает проблему с set -e менее ощутимой.

Заключение

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

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

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