Трубный выход и состояние захвата выхода в Bash
Я хочу выполнить долгосрочную команду в Bash, и и захватить ее состояние выхода, и получить его вывод.
Итак, я делаю это:
command | tee out.txt
ST=$?
Проблема в том, что переменная ST фиксирует состояние выхода tee
и не по команде. Как я могу решить это?
Обратите внимание, что команда выполняется долго и перенаправление вывода в файл для последующего просмотра не является хорошим решением для меня.
16 ответов
Существует внутренняя переменная Bash, которая называется $PIPESTATUS
; это массив, в котором хранится состояние выхода каждой команды в вашем последнем конвейере команд переднего плана.
<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0
Или другой вариант, который также работает с другими оболочками (например, zsh), - включить pipefail:
set -o pipefail
...
Первый вариант не работает с zsh
из-за немного другого синтаксиса.
Тупое решение: соединить их через именованный канал (mkfifo). Тогда команда может быть запущена второй.
mkfifo pipe
tee out.txt < pipe &
command > pipe
echo $?
Используя Баш set -o pipefail
полезно
pipefail: возвращаемое значение конвейера - это состояние последней команды, которая должна выйти с ненулевым статусом, или ноль, если ни одна команда не вышла с ненулевым статусом
Есть массив, который дает вам статус выхода каждой команды в канале.
$ 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
Это решение работает без использования специальных функций bash или временных файлов. Бонус: в конце концов, статус выхода - это фактически статус выхода, а не какая-то строка в файле.
Ситуация:
someprog | filter
Вы хотите статус выхода из someprog
и выход из filter
,
Вот мое решение:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
echo $?
Смотрите мой ответ на тот же вопрос на unix.stackexchange.com для подробного объяснения того, как это работает, и некоторых предостережений.
Объединяя 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
Поэтому я хотел дать ответ, подобный ответу Лесманы, но я думаю, что мой, пожалуй, немного проще и немного более выгодно решение с использованием чистой оболочки Борна:
# 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.
(The exec 4>&1
должна быть отдельной командой, потому что многим обычным оболочкам она не нравится, когда вы пытаетесь записать в файловый дескриптор внутри подстановки команд, которая открывается во "внешней" команде, использующей подстановку. Так что это самый простой способ сделать это.)
Вы можете посмотреть на это менее технически и более игриво, как если бы результаты команд перепрыгивали друг друга: команда 1 отправляет канал команде 2, затем вывод printf перепрыгивает через команду 2, так что команда 2 не перехватывает ее, а затем выходные данные команды 2 перепрыгивают из подстановки команд так же, как и printf приземляется как раз вовремя, чтобы быть захваченными подстановкой, так что она попадает в переменную, а выходные данные команды 2 продолжают веселиться, записываясь в стандартный вывод, так же, как в обычной трубе.
Кроме того, насколько я понимаю, $?
будет по-прежнему содержать код возврата второй команды в конвейере, потому что назначения переменных, подстановки команд и составные команды фактически прозрачны для кода возврата команды внутри них, поэтому статус возврата command2 должен распространяться - это и не нужно определять дополнительную функцию, поэтому я думаю, что это могло бы быть несколько лучшим решением, чем предложенное Лесманой.
В соответствии с предостережениями, которые упоминает лесмена, вполне возможно, что в какой-то момент команда command1 будет использовать файловые дескрипторы 3 или 4, поэтому для большей надежности вы должны сделать следующее:
exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-
Обратите внимание, что я использую составные команды в моем примере, но подуровни (используя ( )
вместо { }
также будет работать, хотя, возможно, будет менее эффективным.)
Команды наследуют файловые дескрипторы от процесса, который их запускает, поэтому вся вторая строка будет наследовать файловый дескриптор четвертый, а за составной командой следует 3>&1
унаследует файловый дескриптор три. Итак 4>&-
убедитесь, что внутренняя составная команда не будет наследовать дескриптор файла четыре, и 3>&-
не будет наследовать дескриптор файла три, поэтому command1 получает "более чистую", более стандартную среду. Вы также можете переместить внутренний 4>&-
сразу после 3>&-
, но я понимаю, почему бы просто не ограничить его возможности как можно больше.
Я не уверен, как часто вещи используют файловый дескриптор 3 и 4 напрямую - я думаю, что в большинстве случаев программы используют системные вызовы, которые возвращают неиспользуемые в данный момент файловые дескрипторы, но иногда код записывает код непосредственно в файловый дескриптор 3, я предположить (я мог бы представить программу, проверяющую дескриптор файла, чтобы узнать, открыт ли он, и использующую его, если он есть, или соответствующим образом ведущий себя иначе, если это не так). Поэтому последнее, вероятно, лучше всего учитывать и использовать в случаях общего назначения.
(command | tee out.txt; exit ${PIPESTATUS[0]})
В отличие от ответа @cODAR, он возвращает исходный код завершения первой команды, а не только 0 для успеха и 127 для ошибки. Но, как отметил @Chaoran, вы можете просто позвонить ${PIPESTATUS[0]}
, Однако важно, чтобы все было заключено в квадратные скобки.
Вне Bash вы можете сделать:
bash -o pipefail -c "command1 | tee output"
Это полезно, например, в скриптах ниндзя, где ожидается, что оболочка будет /bin/sh
,
В Ubuntu и Debian вы можете apt-get install moreutils
, Это содержит утилиту под названием mispipe
который возвращает статус выхода первой команды в канале.
Самый простой способ сделать это в обычном bash - это использовать замену процесса вместо конвейера. Есть несколько отличий, но они, вероятно, не имеют большого значения для вашего варианта использования:
- При запуске конвейера bash ожидает завершения всех процессов.
- Отправка Ctrl-C в bash заставляет его убивать все процессы конвейера, а не только основной.
pipefail
вариант иPIPESTATUS
переменные не имеют отношения к процессу замены.- Возможно больше
С заменой процесса, bash просто запускает процесс и забывает об этом, он даже не виден в jobs
,
Упомянутые различия в стороне, consumer < <(producer)
а также producer | consumer
по существу эквивалентны.
Если вы хотите переключить, какой из них является "основным" процессом, вы просто переключаете команды и направление замены на producer > >(consumer)
, В твоем случае:
command > >(tee out.txt)
Пример:
$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world
$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world
Как я уже сказал, есть отличия от выражения канала. Процесс может никогда не остановиться, если он не чувствителен к закрытию трубы. В частности, он может продолжать записывать данные в ваш стандартный вывод, что может сбивать с толку.
PIPESTATUS[@] должен быть скопирован в массив сразу после возврата команды pipe.Любое чтение PIPESTATUS[@] удалит содержимое. Скопируйте его в другой массив, если вы планируете проверять состояние всех команд конвейера. "$?" это то же значение, что и последний элемент "${PIPESTATUS[@]}", и чтение его, похоже, уничтожает "${PIPESTATUS[@]}", но я не совсем это подтвердил.
declare -a PSA
cmd1 | cmd2 | cmd3
PSA=( "${PIPESTATUS[@]}" )
Это не будет работать, если труба находится в под-оболочке. Для решения этой проблемы,
увидеть bash pipestatus в команде с обратным каналом?
Основываясь на ответе @brian-s-wilson; эта вспомогательная функция bash:
pipestatus() {
local S=("${PIPESTATUS[@]}")
if test -n "$*"
then test "$*" = "${S[*]}"
else ! [[ "${S[@]}" =~ [^0\ ] ]]
fi
}
используется таким образом:
1: get_bad_things должен быть успешным, но он не должен производить никаких выходных данных; но мы хотим увидеть результат, который он производит
get_bad_things | grep '^'
pipeinfo 0 1 || return
2: весь конвейер должен быть успешным
thing | something -q | thingy
pipeinfo || return
Чистый раствор оболочки:
% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag && (echo Some command failed: ; cat error.flag)
hello world
А теперь со вторым cat
заменен на false
:
% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141
Пожалуйста, обратите внимание, что первая кошка тоже терпит неудачу, потому что на нее закрывается стандартный вывод. Порядок неудачных команд в журнале в этом примере правильный, но не полагайтесь на него.
Этот метод позволяет захватывать stdout и stderr для отдельных команд, так что вы можете затем скопировать их в файл журнала, если возникает ошибка, или просто удалить ее, если ошибки нет (например, вывод dd).
Иногда может быть проще и понятнее использовать внешнюю команду, чем копаться в деталях bash. конвейер из минимального языка сценариев процесса execline завершается с кодом возврата второй команды *, как sh
конвейер делает, но в отличие от sh
это позволяет изменить направление канала, чтобы мы могли зафиксировать код возврата процесса производителя (ниже приведено все sh
командная строка, но с execline
установлены):
$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world
$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt
hello world
$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world
$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1
$ pipeline -w tee out.txt "" true; echo $?
0
$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world
С помощью pipeline
имеет те же отличия от собственных конвейеров bash, что и замена процесса bash, использованная в ответе # 43972501.
* На самом деле pipeline
не выходит вообще, если нет ошибки. Он выполняется во второй команде, поэтому это вторая команда, которая выполняет возврат.
Почему бы не использовать? Вот так:
(
# Our long-running process that exits abnormally
( for i in {1..100} ; do echo ploop ; sleep 0.5 ; done ; exit 5 )
echo $? 1>&2 # We pass the exit status of our long-running process to stderr (fd 2).
) | tee ploop.out
Так получаетstdout
.stderr
получает статус завершения долго выполняющегося процесса. Преимущество этого заключается в том, что он полностью совместим с POSIX.
(Ну, за исключением выражения диапазона в примере долго выполняющегося процесса, но это не совсем важно.)
Вот как это выглядит:
...
ploop
ploop
ploop
ploop
ploop
ploop
ploop
ploop
ploop
ploop
5
Обратите внимание, что код возврата5
не получает вывод в файлploop.out
.