Состояние выхода выхода и захвата трубы в Bash

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

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

command | tee out.txt
ST=$?

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

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

15 ответов


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

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

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

set -o pipefail
...

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


С помощью Баша set -o pipefail поможет

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


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

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?

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

$ 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


поэтому я хотел внести свой вклад в ответ, как лесмана, но я думаю, что мой, возможно, немного проще и немного более выгодное решение pure-Bourne-shell:

# 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 работает, его stdout передается в command2 (вывод printf никогда не делает его command2, потому что мы отправляем его в файловый дескриптор 3 вместо 1, который читает канал). Затем мы перенаправляем вывод command2 в файловый дескриптор 4, чтобы он также оставался вне файлового дескриптора 1-потому что мы хотим, чтобы файловый дескриптор 1 был свободен немного позже, потому что мы вернем вывод printf в файловый дескриптор 3 обратно в файловый дескриптор 1-потому что это то, что замена команды (backticks), захватит, и это то, что будет помещено в переменную.

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

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

вы можете посмотреть его в меньше технические и более игровой форме, а если выходы команд перепрыгнуть друг друга: КОМАНДА1 команда2 труб, то е выход прыжков за 2 команды, так что команда2 не поймать его, а затем команда 2 выходных прыгает и из командной подстановки только как printf земли как раз вовремя, чтобы попасть в плен к замене, так что она попадает в переменную, и команда2 выход идет на его веселый путь записывается на стандартный вывод, как в обычных труба.

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

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

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

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

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

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


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


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

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

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


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

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


вне bash, вы можете сделать:

bash -o pipefail  -c "command1 | tee output"

это полезно, например, в сценариях ниндзя, где оболочка должна быть /bin/sh.


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

  • при запуске конвейера bash ожидает завершения всех процессов.
  • отправка Ctrl-C в bash заставляет его убивать все процессы конвейера, а не только основной.
  • на и 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

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


чистая раковина решения:

% 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).


база на ответ @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

иногда может быть проще и понятнее использовать внешнюю команду, а не копаться в деталях bash. трубопровод, от минимального языка сценариев процесса execline, выход с кодом возврата второй команды*, как и делает, но в отличие от 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 не выходит вообще, если нет ошибки. Он выполняется во второй команде, поэтому это вторая команда, которая выполняет возврат.