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

343

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

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

command | tee out.txt
ST=$?

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

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

  • 1
    [["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Соответствие - обнаружена ошибка" || echo -e "Нет совпадений - все хорошо". Это позволит протестировать все значения массива сразу и выдаст сообщение об ошибке, если любое из возвращенных значений канала не равно нулю. Это довольно надежное обобщенное решение для обнаружения ошибок в трубопроводной ситуации.
  • 0
    unix.stackexchange.com/questions/14270/...
Теги:
error-handling
redirect
pipe
tee

15 ответов

431
Лучший ответ

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

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

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

set -o pipefail
...

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

  • 20
    Здесь есть хорошее объяснение с примерами PIPESTATUS AND Pipefail: unix.stackexchange.com/a/73180/7453 .
  • 17
    Примечание: $ PIPESTATUS [0] содержит состояние выхода первой команды в канале, $ PIPESTATUS [1] состояние выхода второй команды и т. Д.
Показать ещё 9 комментариев
133

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

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

  • 20
    Если вы не хотите изменять параметр pipefail всего сценария, вы можете установить этот параметр только локально: ( set -o pipefail; command | tee out.txt ); ST=$?
  • 6
    @Jaan Это запустит подоболочку. Если вы хотите избежать этого, вы можете выполнить команду set -o pipefail а затем выполнить команду, и сразу же после этого выполнить set +o pipefail чтобы отменить выбор опции.
Показать ещё 2 комментария
95

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

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
  • 10
    Это единственный ответ на этот вопрос, который также работает для простой оболочки Unix sh . Спасибо!
  • 1
    Почему это глупо?
Показать ещё 3 комментария
33

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

$ 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
20

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

Ситуация:

someprog | filter

вы хотите получить статус выхода из someprog и выход из filter.

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

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

echo $?

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

17

Объединив 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

  • 4
    Спасибо, это позволило мне использовать конструкцию: VALUE=$(might_fail | piping) которая не установит PIPESTATUS в основной оболочке, но установит ее уровень ошибки. Используя: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]}) я получаю, что хотел.
  • 0
    @vaab, этот синтаксис выглядит действительно хорошо, но я не понимаю, что означает «трубопровод» в вашем контексте? Это именно то место, где можно было бы выполнить 'tee' или какую-либо другую обработку на выходе might_fail? ти!
Показать ещё 2 комментария
8

Поэтому я хотел внести свой вклад, например, lesmana's, но я считаю, что мой, возможно, немного проще и немного более выгодным чистым решением 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 exit status.

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

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

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

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

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

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

В соответствии с оговорками 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>&- не будет наследовать дескриптор файла три, поэтому command1 получает "более чистую", более стандартную среду. Вы также можете перемещать внутренний 4>&- рядом с 3>&-, но я думаю, почему бы не ограничить его область как можно больше.

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

4

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

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

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

3

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

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

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

2

Самый простой способ сделать это в обычном 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

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

2

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

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

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

1

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

1

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

Чистое решение оболочки:

% 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 закрывается на ней. Порядок неудачных команд в журнале правильный в этом примере, но не полагайтесь на него.

Этот метод позволяет захватывать stdout и stderr для отдельных команд, чтобы затем выгрузить его в файл журнала, если произошла ошибка, или просто удалить его, если нет ошибки (например, вывод dd).

Ещё вопросы

Сообщество Overcoder
Наверх
Меню