Выявление кодов ошибок в оболочке трубы

97

В настоящее время у меня есть сценарий, который делает что-то вроде

./a | ./b | ./c

Я хочу изменить его так, чтобы при выходе любого из a, b или c с кодом ошибки я печатал сообщение об ошибке и останавливался вместо того, чтобы передавать плохой вывод вперед.

Каким будет самый простой способ сделать это?

hugomg
источник
7
На самом деле должно быть что-то вроде того, &&|что означало бы «продолжать конвейер только в том случае, если предыдущая команда была успешной». Я полагаю, вы также могли бы иметь, |||что означало бы «продолжить конвейер, если предыдущая команда не удалась» (и, возможно, передать сообщение об ошибке, как в Bash 4 |&).
Приостановлено до дальнейшего уведомления.
6
@DennisWilliamson, вы не можете «остановить трубу» , потому что a, b, cкоманды не выполняются последовательно , а параллельно. Другими словами, потоки данных последовательно от aдо c, но фактически a, bи cкоманды запуска (примерно) в то же самое время.
Джакомо

Ответы:

20

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

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

Перенаправление «1> & 2» можно также обозначить как «> & 2»; однако старая версия оболочки MKS неправильно обрабатывала перенаправление ошибок без предшествующей «1», поэтому я использовал это недвусмысленное обозначение надежности на протяжении многих лет.

Это приводит к утечке файлов, если вы что-то прерываете. В защищенном от взрыва (более или менее) программировании оболочки используются:

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

В первой строке прерывания написано «выполнить команды» rm -f $tmp.[12]; exit 1при появлении любого из сигналов 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE или 15 SIGTERM или 0 (при выходе из оболочки по любой причине). Если вы пишете сценарий оболочки, последней ловушке нужно только удалить ловушку на 0, которая является ловушкой выхода из оболочки (вы можете оставить другие сигналы на месте, поскольку процесс в любом случае завершится).

В исходном конвейере 'c' может считывать данные из 'b' до завершения 'a' - обычно это желательно (например, это дает работу нескольким ядрам). Если «b» - это фаза «сортировки», то это не применимо - «b» должен увидеть весь свой ввод, прежде чем он сможет сгенерировать какой-либо из своих выходных данных.

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

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

Это просто и симметрично - его легко расширить до 4-х или N-сегментного конвейера.

Простые эксперименты с set -e не помогли.

Джонатан Леффлер
источник
1
Я бы рекомендовал использовать mktempили tempfile.
Приостановлено до дальнейшего уведомления.
@Dennis: да, полагаю, мне следует привыкнуть к таким командам, как mktemp или tmpfile; когда я узнал об этом, их не существовало на уровне оболочки много лет назад. Сделаем быструю проверку. Я нахожу mktemp на MacOS X; У меня есть mktemp в Solaris, но только потому, что я установил инструменты GNU; похоже, что mktemp присутствует на античном HP-UX. Я не уверен, есть ли общий вызов mktemp, который работает на разных платформах. POSIX не стандартизирует ни mktemp, ни tmpfile. Я не нашел tmpfile на платформах, к которым у меня есть доступ. Следовательно, я не смогу использовать команды в переносимых сценариях оболочки.
Джонатан Леффлер,
1
При использовании trapпомните, что пользователь всегда может послать SIGKILLпроцессу, чтобы немедленно завершить его, и в этом случае ваша ловушка не сработает. То же самое происходит, когда ваша система испытывает отключение электроэнергии. При создании временных файлов обязательно используйте, mktempпотому что это поместит ваши файлы куда-нибудь, где они будут очищены после перезагрузки (обычно в /tmp).
josch
158

В bash вы можете использовать set -eи set -o pipefailв начале вашего файла. Последующая команда ./a | ./b | ./cзавершится ошибкой при сбое любого из трех сценариев. Код возврата будет кодом возврата первого неудачного сценария.

Обратите внимание, что pipefailэто недоступно в стандартном sh .

Мишель Самиа
источник
Я не знал о провале трубы, очень удобно.
Фил Джексон
Он предназначен для помещения его в сценарий, а не в интерактивную оболочку. Ваше поведение можно упростить, установив -e; ложный; он также выходит из процесса оболочки, что является ожидаемым поведением;)
Мишель Самиа
11
Примечание: это все равно выполнит все три сценария и не остановит конвейер при первой ошибке.
hyde
1
@ n2liquid-GuilhermeVieira Между прочим, под «разными вариациями» я конкретно имел в виду удаление одного или обоих set(всего 4 разных версии) и посмотрите, как это повлияет на вывод последнего echo.
hyde
1
@josch Я нашел эту страницу, когда искал, как это сделать в bash из Google, и при этом сказал: «Это именно то, что я ищу». Я подозреваю, что многие люди, которые голосуют за ответы, мыслят аналогично и не проверяют теги и определения тегов.
Troy Daniels
44

Вы также можете проверить ${PIPESTATUS[]}массив после полного выполнения, например, если вы запустите:

./a | ./b | ./c

Затем ${PIPESTATUS}будет массив кодов ошибок каждой команды в конвейере, поэтому, если средняя команда не удалась, echo ${PIPESTATUS[@]}будет содержать что-то вроде:

0 1 0

и что-то вроде этого запускается после команды:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

позволит вам проверить, что все команды в канале выполнены успешно.

Имрон
источник
11
Это глупо - это расширение bash, а не часть стандарта Posix, поэтому другие оболочки, такие как dash и ash, его не поддерживают. Это означает, что вы можете столкнуться с проблемами, если попытаетесь использовать его в скриптах, которые запускаются #!/bin/sh, потому что, если shэто не bash, он не будет работать. (Легко исправить, если не забыть использовать #!/bin/bashвместо этого.)
Дэвид Гивен
2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- возвращает 0, если $PIPESTATUS[@]содержит только 0 и пробелы (если все команды в канале выполнены успешно).
MattBianco,
@MattBianco Это лучшее решение для меня. Он также работает с &&, например, command1 && command2 | command3если какой-либо из них не работает, ваше решение вернет ненулевое значение.
DavidC
8

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

Вот решение, которое ловит все ошибки в трубе, а не только ошибки последнего компонента. Это похоже на pipefail в bash, только более мощный в том смысле, что вы можете получить все коды ошибок.

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Чтобы определить, не удалось ли что-нибудь, echoкоманда выводит стандартную ошибку в случае сбоя какой-либо команды. Затем объединенный стандартный вывод ошибок сохраняется $resи исследуется позже. По этой же причине стандартная ошибка всех процессов перенаправляется на стандартный вывод. Вы также можете отправить этот вывод /dev/nullили оставить его как еще один индикатор того, что что-то пошло не так. Вы можете заменить последнее перенаправление на /dev/nullфайл, если вам не нужно хранить вывод последней команды где угодно.

Чтобы больше поиграть с этой конструкцией и убедить себя, что она действительно делает то, что должно, я заменил ./a, ./bи ./cподоболочки, которые выполняют echo, catи exit. Вы можете использовать это, чтобы убедиться, что эта конструкция действительно перенаправляет весь вывод от одного процесса к другому и что коды ошибок записываются правильно.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
йош
источник