Трубный выход и состояние захвата выхода в Bash - PullRequest
375 голосов
/ 03 августа 2009

Я хочу выполнить длительную команду в Bash, и обе записать ее состояние выхода, а tee ее вывод.

Итак, я делаю это:

command | tee out.txt
ST=$?

Проблема в том, что переменная ST фиксирует состояние выхода tee, а не команды. Как я могу решить это?

Обратите внимание, что команда выполняется долго, и перенаправление вывода в файл для последующего просмотра не является хорошим решением для меня.

Ответы [ 15 ]

473 голосов
/ 03 августа 2009

Существует внутренняя переменная Bash, которая называется $PIPESTATUS; это массив, в котором хранится состояние выхода каждой команды в вашем последнем конвейере команд переднего плана.

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

Или другой вариант, который также работает с другими оболочками (например, zsh), - включить pipefail:

set -o pipefail
...

Первый параметр не работает с zsh из-за немного другого синтаксиса.

137 голосов
/ 06 ноября 2013

использование bash's set -o pipefail полезно

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

112 голосов
/ 03 августа 2009

Тупое решение: соединить их через именованный канал (mkfifo). Затем команда может быть запущена второй.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
34 голосов
/ 03 августа 2009

Есть массив, который дает вам статус выхода каждой команды в канале.

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1
22 голосов
/ 14 мая 2013

Это решение работает без использования специальных функций bash или временных файлов. Бонус: в конце концов, статус выхода на самом деле является статусом выхода, а не какой-либо строкой в ​​файле.

Положение:

someprog | filter

вы хотите статус выхода из someprog и выход из filter.

Вот мое решение:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

См. мой ответ на тот же вопрос на unix.stackexchange.com для подробного объяснения и альтернативы без подоболочек и некоторых оговорок.

19 голосов
/ 18 августа 2013

Комбинируя PIPESTATUS[0] и результат выполнения команды exit в подоболочке, вы можете напрямую получить доступ к возвращаемому значению вашей исходной команды:

command | tee ; ( exit ${PIPESTATUS[0]} )

Вот пример:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

даст вам:

return value: 1

9 голосов
/ 05 июня 2015

Итак, я хотел бы дать ответ, подобный ответу Лесманы, но я думаю, что мой, пожалуй, немного проще и немного более выгодно решение с чистой оболочкой Борна:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

Я думаю, что это лучше всего объяснить изнутри - command1 выполнит и напечатает свой обычный вывод на stdout (дескриптор файла 1), затем, как только это будет сделано, printf выполнит и напечатает код выхода icommand1 на своем stdout, но этот stdout перенаправляется в файловый дескриптор 3.

Когда команда command1 выполняется, ее стандартный вывод передается по команде command2 (вывод printf никогда не попадает в command2, потому что мы отправляем его в файловый дескриптор 3 вместо 1, который читает канал). Затем мы перенаправляем вывод команды 2 в файловый дескриптор 4, чтобы он также не входил в файловый дескриптор 1 - потому что мы хотим, чтобы файловый дескриптор 1 был немного позже, потому что мы приведем вывод printf для файлового дескриптора 3 обратно в файловый дескриптор 1 - потому что это то, что команда замещения (обратные метки), захватит, и это то, что будет помещено в переменную.

Последнее волшебство в том, что сначала exec 4>&1 мы сделали отдельной командой - он открывает файловый дескриптор 4 как копию стандартного вывода внешней оболочки. Подстановка команд будет захватывать все, что написано в стандарте, с точки зрения команд внутри него - но поскольку выходные данные команды 2 собираются в файловом дескрипторе 4, если речь идет о подстановке команд, подстановка команд не захватывает это - однако, как только это произойдет "выходит" из подстановки команд, она фактически все еще идет к общему файловому дескриптору скрипта 1.

(exec 4>&1 должна быть отдельной командой, потому что многим распространенным оболочкам не нравится, когда вы пытаетесь записать в файловый дескриптор внутри подстановки команды, которая открывается во «внешней» команде, использующей замена. Так что это самый простой переносимый способ сделать это.)

Вы можете посмотреть на это менее технически и более игриво, как если бы выходы команд перепрыгивали друг друга: команда1 перенаправляет на команду2, затем вывод printf перепрыгивает через команду 2, так что команда2 не может ее перехватить , а затем выходные данные команды 2 перепрыгивают из подстановки команд, точно так же, как printf приземляется как раз вовремя, чтобы быть захваченным подстановкой, так что она попадает в переменную, а выходные данные команды 2 идут своим веселым путем, записываясь в стандартный вывод как в обычной трубе.

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

В соответствии с предостережениями, которые упоминает лесмена, вполне возможно, что в какой-то момент команда1 будет использовать файловые дескрипторы 3 или 4, поэтому для большей надежности вы должны сделать:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

Обратите внимание, что я использую составные команды в моем примере, но подоболочки (использование ( ) вместо { } также будет работать, хотя, возможно, будет менее эффективным.)

Команды наследуют файловые дескрипторы от процесса, который их запускает, поэтому вся вторая строка будет наследовать файловый дескриптор четыре, а составная команда, за которой следует 3>&1, будет наследовать файловый дескриптор три. Таким образом, 4>&- гарантирует, что внутренняя составная команда не будет наследовать дескриптор файла четыре, а 3>&- не будет наследовать дескриптор файла три, поэтому команда1 получает «более чистую», более стандартную среду. Вы также можете переместить внутренний 4>&- рядом с 3>&-, но я понимаю, почему бы просто не ограничить его область настолько, насколько это возможно.

Я не уверен, как часто вещи используют файловый дескриптор три и четыре напрямую - я думаю, что в большинстве случаев программы используют системные вызовы, которые возвращают неиспользуемые в данный момент файловые дескрипторы, но иногда код записывает в файловый дескриптор 3 Я полагаю, непосредственно (я мог бы представить программу, проверяющую дескриптор файла, чтобы увидеть, открыт ли он, и использующую его, если он есть, или соответствующим образом ведущий себя иначе, если это не так). Поэтому последнее, вероятно, лучше всего учитывать и использовать в случаях общего назначения.

6 голосов
/ 13 декабря 2013

В Ubuntu и Debian вы можете apt-get install moreutils. Он содержит утилиту с именем mispipe, которая возвращает состояние выхода первой команды в канале.

3 голосов
/ 25 сентября 2017
(command | tee out.txt; exit ${PIPESTATUS[0]})

В отличие от ответа @ cODAR, он возвращает исходный код завершения первой команды, а не только 0 для успеха и 127 для ошибки. Но, как заметил @Chaoran, вы можете просто позвонить ${PIPESTATUS[0]}. Однако важно, чтобы все было заключено в квадратные скобки.

3 голосов
/ 02 ноября 2013

PIPESTATUS [@] должен быть скопирован в массив сразу после возврата команды pipe. Любые чтения PIPESTATUS [@] сотрут содержимое. Скопируйте его в другой массив, если вы планируете проверить состояние всех команд конвейера. "$?" то же значение, что и последний элемент "$ {PIPESTATUS [@]}", и чтение его, похоже, уничтожает «$ {PIPESTATUS [@]}», но я не совсем это подтвердил.

declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=( "${PIPESTATUS[@]}" )

Это не будет работать, если труба находится в подоболочке. Для решения этой проблемы
см. bash pipestatus в команде с обратным символом?

...