Состояние выхода выхода и выхода канала в Bash
Я хочу выполнить длинную команду в Bash, и оба фиксируют ее статус выхода, а tee вывод.
Итак, я делаю это:
command | tee out.txt
ST=$?
Проблема заключается в том, что переменная ST фиксирует статус выхода tee
, а не команды. Как я могу это решить?
Обратите внимание, что команда длительная и перенаправление вывода в файл для просмотра позже не подходит для меня.
Ответы
Ответ 1
Существует внутренняя переменная Bash, называемая $PIPESTATUS
; его массив, который содержит статус выхода каждой команды в последнем конвейере команд переднего плана.
<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0
Или другая альтернатива, которая также работает с другими оболочками (например, zsh), заключается в том, чтобы включить pipefail:
set -o pipefail
...
Первый вариант не работает с zsh
из-за немного другого синтаксиса.
Ответ 2
использование bash set -o pipefail
полезно
pipefail: возвращаемое значение конвейера - это статус последняя команда для выхода с ненулевым статусом, или ноль, если команда не вышла с ненулевым статусом
Ответ 3
Неброское решение: подключение их по именованному каналу (mkfifo). Затем команда может быть выполнена второй.
mkfifo pipe
tee out.txt < pipe &
command > pipe
echo $?
Ответ 4
Там массив, который дает вам статус выхода каждой команды в трубе.
$ 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
Ответ 5
Это решение работает без использования специфичных для bash функций или временных файлов. Бонус: в конце концов статус выхода на самом деле является статусом выхода, а не некоторой строкой в файле.
Ситуация:
someprog | filter
вы хотите получить статус выхода из someprog
и выход из filter
.
Вот мое решение:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
echo $?
См. Мой ответ на тот же вопрос на unix.stackexchange.com для подробного объяснения того, как это работает, и некоторых оговорок.
Ответ 6
Объединив 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
Ответ 7
Поэтому я хотел внести свой вклад, например, 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 (я мог бы представить себе программу, проверяющую файловый дескриптор, чтобы увидеть, открыта ли она, и использовать ее, если она есть, или вести себя иначе, если это не так). Поэтому последнее, вероятно, лучше всего учитывать и использовать для случаев общего назначения.
Ответ 8
В Ubuntu и Debian вы можете apt-get install moreutils
. В нем содержится утилита с именем mispipe
, которая возвращает статус выхода первой команды в канале.
Ответ 9
PIPESTATUS [@] должен быть скопирован в массив сразу после возвращения команды pipe.
Любые записи PIPESTATUS [@] будут удалять содержимое.
Скопируйте его в другой массив, если вы планируете проверять состояние всех команд трубопровода.
"$?" это то же значение, что и последний элемент "$ {PIPESTATUS [@]}",
и чтение, кажется, уничтожает "$ {PIPESTATUS [@]}", но я не совсем это подтвердил.
declare -a PSA
cmd1 | cmd2 | cmd3
PSA=( "${PIPESTATUS[@]}" )
Это не будет работать, если труба находится в под-оболочке. Для решения этой проблемы,
см. bash pipestatus в команде backticked?
Ответ 10
(command | tee out.txt; exit ${PIPESTATUS[0]})
В отличие от ответа @cODAR, возвращается исходный код выхода первой команды, а не только 0 для успеха и 127 для отказа. Но, как отметил @Chaoran, вы можете просто позвонить ${PIPESTATUS[0]}
. Однако важно, чтобы все было заключено в скобки.
Ответ 11
Вне bash вы можете сделать:
bash -o pipefail -c "command1 | tee output"
Это полезно, например, в сценариях ниндзя, где ожидается, что оболочка будет /bin/sh
.
Ответ 12
Самый простой способ сделать это в обычном 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, что может сбить с толку.
Ответ 13
Чистое решение оболочки:
% 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).
Ответ 14
База на ответ @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
Ответ 15
Иногда может быть проще и понятнее использовать внешнюю команду, а не копаться в деталях 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
вообще не выходит, если не возникает ошибка. Он выполняет вторую команду, поэтому вторая команда выполняет возврат.