Wyjście rury i przechwytywanie statusu wyjścia w Bash

Chcę wykonać długo działające polecenie w Bash, i zarówno przechwycić jego status wyjścia, jak i tee jego wyjście.

Więc robię to:

command | tee out.txt
ST=$?

Problem polega na tym, że zmienna ST przechwytuje status wyjścia tee, a nie polecenia. Jak mogę to rozwiązać?

Zauważ, że polecenie działa długo i przekierowanie wyjścia do pliku, aby wyświetlić go później, nie jest dla mnie dobrym rozwiązaniem.

Author: codeforester, 2009-08-03

15 answers

Istnieje wewnętrzna zmienna Bash o nazwie $PIPESTATUS; jest to tablica, która przechowuje status zakończenia każdego polecenia w ostatnim pierwszoplanowym potoku poleceń.

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

Lub inną alternatywą, która działa również z innymi powłokami (jak zsh), byłoby włączenie pipefail:

set -o pipefail
...

Pierwsza opcja działa nie z zsh ze względu na nieco inną składnię.

 431
Author: cODAR,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-21 17:17:14

Using bash ' s set -o pipefail is helpful

Pipefail: zwracaną wartością potoku jest status ostatnie polecenie wyjścia ze statusem niezerowym, or zero if no command exited with a non-zero status

 127
Author: Felipe Alvarez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-04 08:14:11

Głupie rozwiązanie: połączenie ich przez nazwaną rurę (mkfifo). Następnie polecenie można uruchomić jako drugie.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
 99
Author: EFraim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-14 15:26:39

Istnieje tablica, która daje status zakończenia każdego polecenia w potoku.

$ 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
 34
Author: Stefano Borini,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-30 08:41:45

To rozwiązanie działa bez użycia specyficznych funkcji bash lub plików tymczasowych. Bonus: na końcu status zakończenia jest rzeczywiście statusem zakończenia, a nie jakimś łańcuchem w pliku.

Sytuacja:

someprog | filter

Chcesz mieć status wyjścia z someprog i wyjście z filter.

Oto moje rozwiązanie:

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

echo $?

Zobacz moja odpowiedź na to samo pytanie na unix.stackexchange.com szczegółowe wyjaśnienie, jak to działa i pewne zastrzeżenia.

 21
Author: lesmana,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-10 13:27:29

Poprzez Połączenie PIPESTATUS[0] i wyniku wykonania polecenia exit w podshell, możesz bezpośrednio uzyskać dostęp do wartości zwracanej początkowego polecenia:

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

Oto przykład:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

Da ci:

return value: 1

 17
Author: par,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-08-18 03:56:19

Więc chciałem dodać odpowiedź jak lesmana, ale myślę, że moja jest może trochę prostsze i nieco bardziej korzystne rozwiązanie 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.

Myślę, że jest to najlepiej wyjaśnione od środka-command1 wykona i wydrukuje swoje zwykłe wyjście na stdout( deskryptor pliku 1), a następnie po zakończeniu, printf wykona i wydrukuje kod zakończenia icommand1 na swoim stdout, ale ten stdout zostanie przekierowany do deskryptora pliku 3.

Podczas uruchamiania command1, jego stdout jest przesyłany do command2 (wyjście printf nigdy nie trafia do command2, ponieważ wysyłamy go do deskryptora pliku 3 zamiast 1, Co odczytuje ten pipe). Następnie przekierowujemy wyjście command2 do deskryptora pliku 4, tak, że pozostaje on również poza deskryptorem pliku 1 - ponieważ chcemy, aby deskryptor pliku 1 był wolny na trochę później, ponieważ przeniesiemy wyjście printf z deskryptora pliku 3 z powrotem do deskryptora pliku 1 - ponieważ to jest to, co zastępowanie poleceń (backticks), spowoduje, że plik 1 będzie wolny. przechwytywanie i to, co zostanie umieszczone w zmiennej.

Ostatnim bitem magii jest to, że pierwsze exec 4>&1 zrobiliśmy jako osobne polecenie - otwiera deskryptor pliku 4 jako kopię zewnętrznego wyjścia powłoki. Podstawianie poleceń przechwyci to, co jest zapisane na standardowym wyjściu z perspektywy komend wewnątrz niego - ale ponieważ wyjście command2 trafi do deskryptora pliku 4, jeśli chodzi o podstawianie poleceń, podstawianie poleceń tego nie przechwyci - jednak po "wyjściu" z podstawiania poleceń, nadal przechodzi do ogólnego deskryptora pliku skryptu 1.

(exec 4>&1 musi być osobnym poleceniem, ponieważ wiele popularnych powłok nie lubi tego, gdy próbuje się zapisać do deskryptora pliku wewnątrz podstawiania poleceń, które jest otwierane w poleceniu "external", które używa podstawiania. Jest to więc najprostszy przenośny sposób na to.)

Można spojrzeć na to w mniej techniczny i bardziej zabawny sposób, jakby wyjścia poleceń przeskakują ze sobą: command1 przeskakuje do command2, następnie wyjście printf przeskakuje nad command 2 tak, że command2 go nie złapie, a następnie wyjście command 2 przeskakuje nad i z podstawiania poleceń tak jak printf ląduje w samą porę, aby zostać przechwyconym przez podstawianie, tak że kończy się w zmiennej, a wyjście command2 idzie na swoją szczęśliwą drogę jest zapisywane do standardowego wyjścia, tak jak w normalnym potoku.

Również, jak to rozumiem, $? nadal będzie zawierać kod powrotu drugiego polecenia w potoku, ponieważ przypisania zmiennych, podstawienia poleceń i polecenia złożone są skutecznie przezroczyste dla kodu powrotu polecenia wewnątrz nich, więc status powrotu polecenia command2 powinien zostać rozpowszechniony - to, i nie musi definiować dodatkowej funkcji, dlatego myślę, że może to być nieco lepsze rozwiązanie niż to zaproponowane przez lesmana.

Zgodnie z zastrzeżeniami, o których wspomina lesmana, możliwe jest, że command1 w pewnym momencie będzie używać deskryptorów plików 3 lub 4, więc aby być bardziej solidnym, wykonałbyś:

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

Zauważ, że w moim przykładzie używam poleceń złożonych, ale podpowiedzi (użycie ( ) zamiast { } również będą działać, choć może być mniej wydajne.)

Polecenia dziedziczą deskryptory plików od procesu, który je uruchamia, więc cała druga linia dziedziczy deskryptor pliku czwarty, a polecenie złożone, po którym następuje 3>&1 dziedziczy deskryptor pliku trzeci. Tak więc 4>&- upewnia się, że wewnętrzne polecenie złożone nie odziedziczy deskryptora pliku czwartego, a 3>&- nie odziedziczy deskryptora pliku trzeciego, więc command1 otrzymuje "czystsze", bardziej standardowe środowisko. Możesz także przesunąć wewnętrzny 4>&- obok 3>&-, ale pomyślałem, dlaczego nie ograniczyć jego zakresu tak bardzo, jak to możliwe.

Nie jestem pewien, jak często rzeczy używają deskryptora pliku 3 i 4 bezpośrednio - myślę, że większość programów używa syscalls, które zwracają nie-używane-w-chwili deskryptory plików, ale czasami kod zapisuje bezpośrednio do deskryptora pliku 3 (wyobrażam sobie program sprawdzający deskryptor pliku, aby sprawdzić, czy jest otwarty i używając go, jeśli jest, lub zachowując się inaczej, jeśli nie jest). Więc to ostatnie jest prawdopodobnie najlepiej pamiętać i używać w przypadkach ogólnego przeznaczenia.

 8
Author: mtraceur,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-13 06:33:11

W Ubuntu i Debianie możesz apt-get install moreutils. Zawiera narzędzie o nazwie mispipe, które zwraca status zakończenia pierwszego polecenia w potoku.

 4
Author: Bryan Larsen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-13 18:33:04

PIPESTATUS [@] musi być skopiowany do tablicy natychmiast po powrocie polecenia pipe. Każdy odczyt PIPESTATUS [@] usunie zawartość. Skopiuj go do innej tablicy, jeśli planujesz sprawdzić stan wszystkich komend potoku. "$?"jest tą samą wartością co ostatni element" ${PIPESTATUS[@]}", a czytanie tego zdaje się niszczyć " ${PIPESTATUS [@]}", ale nie do końca to zweryfikowałem.

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

To nie zadziała, jeśli rura jest w sub-powłoce. Dla rozwiązania tego problem,
zobacz Bash pipestatus w poleceniu backticked?

 3
Author: maxdev137,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:59
(command | tee out.txt; exit ${PIPESTATUS[0]})

W przeciwieństwie do odpowiedzi @cODAR zwraca oryginalny kod zakończenia pierwszego polecenia, a nie tylko 0 dla powodzenia i 127 dla niepowodzenia. Ale jak zauważył @ Chaoran możesz po prostu zadzwonić ${PIPESTATUS[0]}. Ważne jest jednak, aby wszystko było umieszczone w nawiasach.

 3
Author: jakob-r,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-25 08:42:27

Poza bashem możesz zrobić:

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

Jest to przydatne na przykład w skryptach ninja, gdzie oczekuje się, że powłoka będzie /bin/sh.

 2
Author: Anthony Scemama,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-17 18:53:38

Najprostszym sposobem na to jest użycie zastępowania procesów zamiast potoku. Istnieje kilka różnic, ale prawdopodobnie nie mają one większego znaczenia dla Twojego przypadku użycia: {]}

  • podczas uruchamiania potoku bash czeka aż wszystkie procesy zostaną zakończone.
  • wysłanie Ctrl - C do Basha powoduje, że zabija wszystkie procesy potoku, a nie tylko główny.
  • opcja pipefail i zmienna PIPESTATUS są nieistotne dla procesu zastępstwo.
  • prawdopodobnie więcej

Z podstawieniem procesu, bash po prostu uruchamia proces i zapomina o nim, nie jest nawet widoczny w jobs.

Pomijając wspomniane różnice, consumer < <(producer) i producer | consumer są zasadniczo równoważne.

Jeśli chcesz odwrócić, który z nich jest "głównym" procesem, po prostu odwróć polecenia i kierunek substytucji na producer > >(consumer). W Twoim przypadku:

command > >(tee out.txt)

Przykład:

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

Jak powiedziałem, istnieją różnice od ekspresja rur. Proces może nigdy nie przestać działać, chyba że jest wrażliwy na zamknięcie rury. W szczególności, może nadal pisać rzeczy na stdout, co może być mylące.

 2
Author: clacke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-15 07:09:44

Roztwór czystej powłoki:

% 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

A teraz z drugim cat zastąpione przez 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

Zwróć uwagę, że pierwszy kot również się nie powiedzie, ponieważ jest zamknięty na stdout. Kolejność nieudanych poleceń w dzienniku jest poprawna w tym przykładzie, ale nie polegaj na tym.

Ta metoda pozwala na przechwytywanie stdout i stderr dla poszczególnych poleceń, dzięki czemu można to również zrzucać do pliku dziennika, jeśli wystąpi błąd, lub po prostu usunąć go, jeśli nie ma błędu (jak wyjście z dd).

 1
Author: Coroos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-31 10:08:58

Base on @ brian-s-wilson 's answer; this Bash helper function:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n "$*"
  then test "$*" = "${S[*]}"
  else ! [[ "${S[@]}" =~ [^0\ ] ]]
  fi
}

Użyte tak:

1: get_bad_things musi się udać, ale nie powinien produkować wyjścia; ale chcemy zobaczyć wyjście, które produkuje

get_bad_things | grep '^'
pipeinfo 0 1 || return

2: cały rurociąg musi się udać

thing | something -q | thingy
pipeinfo || return
 1
Author: Sam Liddicott,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-15 15:29:41

Czasami prostsze i jaśniejsze może być użycie zewnętrznego polecenia, a nie zagłębianie się w szczegóły Basha. pipeline , z minimalnego języka skryptowego procesu execline , kończy się kodem zwracanym przez drugie polecenie*, tak jak robi to rurociąg sh, ale w przeciwieństwie do sh, umożliwia odwrócenie kierunku potoku, dzięki czemu możemy przechwycić kod zwracany przez proces produkcyjny (poniżej wszystko znajduje się w linii poleceń sh, ale z execline zainstalowane):

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

Użycie pipeline ma takie same różnice w stosunku do natywnych potoków bash, jak substytucja procesu bash użyta w odpowiedzi #43972501.

* właściwie pipeline w ogóle nie wychodzi, chyba że wystąpił błąd. Wykonuje drugą komendę, więc jest to druga Komenda, która zwraca.

 1
Author: clacke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-15 07:26:41