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

421

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

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

command | tee out.txt
ST=$?

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

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

ЭЛЕКТРОДИСТАНЦИОННАЯ СИСТЕМА УПРАВЛЕНИЯ
источник
1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Соответствие - обнаружена ошибка" || echo -e "Нет совпадений - все хорошо". Это позволит проверить все значения массива сразу и выдать сообщение об ошибке, если любое из возвращенных значений канала не равно нулю. Это довольно надежное обобщенное решение для обнаружения ошибок в трубопроводной ситуации.
Брайан С. Уилсон
unix.stackexchange.com/questions/14270/…
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

Ответы:

519

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

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

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

set -o pipefail
...

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

CODAR
источник
21
Здесь есть хорошее объяснение с примерами PIPESTATUS AND Pipefail: unix.stackexchange.com/a/73180/7453 .
SLM
18
Примечание: $ PIPESTATUS [0] содержит состояние выхода первой команды в канале, $ PIPESTATUS [1] состояние выхода второй команды и т. Д.
simpleuser
18
Конечно, мы должны помнить, что это специфично для Bash: если бы я (например) написал сценарий для запуска в shy-реализации BusyBox на моем Android-устройстве или на какой-то другой встроенной платформе, используя какую-то другую «sh» вариант, это не будет работать.
Асфанд Кази
4
Для тех, кто обеспокоен расширением переменной без кавычек : Состояние выхода - это всегда 8-разрядное целое число без знака в Bash , поэтому нет необходимости заключать его в кавычки. Это в целом справедливо и для Unix, где состояние выхода определено как явное 8-битное , и предполагается, что оно не подписано даже самим POSIX, например, при определении его логического отрицания .
Палек
3
Вы также можете использовать exit ${PIPESTATUS[0]}.
Чаоран
142

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

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

Фелипе Альварес
источник
23
Если вы не хотите изменять настройки pipefail всего скрипта, вы можете установить опцию только локально:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan
7
@Jaan Это запустит подоболочку. Если вы хотите избежать этого, вы можете выполнить set -o pipefailи затем выполнить команду, а затем сразу же выполнить команду, set +o pipefailчтобы отменить выбор.
Линус Арвер
2
Примечание: плакат с вопросом не хочет «общего кода выхода» канала, ему нужен код возврата «команды». С -o pipefailего помощью он узнает, что канал не работает, но если оба «command» и «tee» потерпят неудачу, он получит код выхода от «tee».
t0r0X
@LinusArver разве это не очистит код выхода, так как это успешная команда?
carlin.scott
127

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

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
Эфраим
источник
20
Это единственный ответ в этом вопросе, который также работает для простой оболочки Unix sh . Спасибо!
JamesThomasMoon1979
3
@DaveKennedy: тупой, как в «очевидном, не требующем сложных знаний синтаксиса bash»
EFraim
10
Хотя ответы на bash более элегантны, когда у вас есть преимущество дополнительных возможностей bash, это более кроссплатформенное решение. Об этом также стоит подумать в целом, поскольку в любое время, когда вы выполняете длительную команду, конвейер имен часто является наиболее гибким способом. Стоит отметить, что некоторые системы не имеют mkfifoи могут вместо этого потребовать, mknod -pесли я правильно помню.
Харавикк
3
Иногда при переполнении стека есть ответы, которые вы стоите выше, чтобы люди перестали делать другие вещи, которые не имеют смысла, это один из них. Спасибо, сэр.
Дэн Чейз
1
В случае, если у кого-то есть проблемы с mkfifoили mknod -p: в моем случае правильная команда для создания файла канала была mknod FILE_NAME p.
Кароль Гиль
36

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

$ 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
Стефано Борини
источник
26

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

Ситуация:

someprog | filter

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

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

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

echo $?

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

lesmana
источник
20

Комбинируя 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]})я получаю хочу я хотел.
Вааб
@vaab, этот синтаксис выглядит действительно красиво, но я не понимаю, что означает «трубопровод» в вашем контексте? Это именно то место, где можно было бы выполнить 'tee' или какую-либо другую обработку на выходе might_fail? ти!
AnneTheAgile
1
@AnneTheAgile 'piping' в моем примере обозначает команды, из которых вы не хотите видеть errlvl. Например: одна из или любая переданная по конвейеру комбинация 'tee', 'grep', 'sed', ... Это весьма необычно, что эти команды конвейера предназначены для форматирования или извлечения информации из большего вывода или вывода журнала основного команда: вас больше интересует уровень ошибки основной команды (в моем примере я назвал 'might_fail'), но без моей конструкции целое назначение возвращает errlvl последней переданной команды, который здесь не имеет смысла. Это понятнее?
Вааб
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}в случае не ти, а grep фильтрации
user1742529
12

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

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

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

mtraceur
источник
Хорошее объяснение!
selurvedu
6

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

Брайан Ларсен
источник
5
(command | tee out.txt; exit ${PIPESTATUS[0]})

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

Jakob-р
источник
4

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

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

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

Энтони Скемама
источник
3

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

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

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

maxdev137
источник
3

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

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

clacke
источник
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 и stderr для отдельных команд, так что вы можете затем скопировать их в файл журнала, если возникает ошибка, или просто удалить ее, если ошибки нет (например, вывод dd).

Coroos
источник
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

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

clacke
источник