Wyjście potoku i status wyjścia przechwytywania w Bash


420

Chcę wykonać długo działającą komendę w Bash, i zarówno przechwytują jej status wyjścia, jak i tee wyjściowe.

Więc robię to:

command | tee out.txt
ST=$?

Problem polega na tym, że zmienna ST przechwytuje status wyjścia, teea 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.


1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Dopasowano - znaleziono błąd" || echo -e „Brak dopasowania - wszystko dobre” Spowoduje to przetestowanie wszystkich wartości tablicy jednocześnie i wyświetli komunikat o błędzie, jeśli którakolwiek z zwracanych wartości potoku nie będzie równa zero. Jest to dość solidne uogólnione rozwiązanie do wykrywania błędów w potokowej sytuacji.
Brian S. Wilson

Odpowiedzi:


519

Istnieje wewnętrzna zmienna Bash o nazwie $PIPESTATUS; jest to tablica, która przechowuje status wyjścia każdego polecenia w ostatnim szeregu poleceń na pierwszym planie.

<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 nie działa z zshpowodu nieco innej składni.


21
Istnieje dobre wytłumaczenie z przykładami PIPESTATUSA i Pipefaila tutaj: unix.stackexchange.com/a/73180/7453 .
slm

18
Uwaga: $ PIPESTATUS [0] przechowuje status wyjścia pierwszego polecenia w potoku, $ PIPESTATUS [1] status wyjścia drugiego polecenia i tak dalej.
simpleuser 24.09.2013

18
Oczywiście musimy pamiętać, że jest to specyficzne dla Bash: gdybym (na przykład) napisał skrypt do uruchomienia na implementacji „sh” BusyBox na moim urządzeniu z Androidem lub na innej wbudowanej platformie przy użyciu innego „sh” wariant nie zadziałałby.
Asfand Qazi

4
Dla tych, którzy są zaniepokojeni niekwotowanym rozszerzeniem zmiennej: stanem wyjścia jest zawsze w Bash 8-bitowa liczba całkowita bez znaku , dlatego nie ma potrzeby cytowania go. Dotyczy to również Uniksa, gdzie status wyjścia jest zdefiniowany jako 8-bitowy jawnie , i zakłada się, że jest on niepodpisany nawet przez sam POSIX, np. Podczas definiowania jego logicznej negacji .
Palec

3
Możesz także użyć exit ${PIPESTATUS[0]}.
Chaoran

142

set -o pipefailPomocne jest używanie bash

pipefail: zwracana wartość potoku jest statusem ostatniego polecenia do wyjścia ze statusem niezerowym lub zerowym, jeśli żadne polecenie nie zostało zakończone ze stanem niezerowym


23
Jeśli nie chcesz modyfikować ustawienia pipefail całego skryptu, możesz ustawić tę opcję tylko lokalnie:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan,

7
@Jaan Uruchomiłoby to podpowłokę. Jeśli chcesz tego uniknąć, możesz zrobić, set -o pipefaila następnie wykonać polecenie, a następnie natychmiast zrobić, set +o pipefailaby wyłączyć tę opcję.
Linus Arver

2
Uwaga: plakat z pytaniami nie chce „ogólnego kodu wyjścia” potoku, chce kodu powrotu „polecenia”. Z -o pipefailwiedziałby, czy rura nie powiedzie się, ale jeśli zarówno „polecenia” i „tee” nie, mógłby otrzymać kod wyjścia z „tee”.
t0r0X,

@LinusArver nie wyczyściłby kodu wyjścia, ponieważ jest to polecenie, które się powiedzie?
carlin.scott,

126

Głupie rozwiązanie: łączenie ich przez nazwaną potok (mkfifo). Następnie polecenie można uruchomić na drugim miejscu.

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

20
Jest to jedyna odpowiedź w tym pytaniu, która działa również dla prostej powłoki sh Unix. Dzięki!
JamesThomasMoon1979

3
@DaveKennedy: Głupi jak w „oczywistym, nie wymagającym skomplikowanej znajomości składni bash”
EFraim

10
Podczas gdy odpowiedzi bash są bardziej eleganckie, gdy masz przewagę dodatkowych możliwości bash, jest to rozwiązanie bardziej wieloplatformowe. Jest to również coś, o czym ogólnie warto pomyśleć, ponieważ za każdym razem, gdy wykonujesz długo działające polecenie, potok nazw jest często najbardziej elastyczny. Warto zauważyć, że niektóre systemy nie mają mkfifoi mogą wymagać, mknod -pjeśli dobrze pamiętam.
Haravikk

3
Czasami na przepełnieniu stosu są odpowiedzi, które głosowałbyś sto razy, aby ludzie przestali robić inne rzeczy, które nie mają sensu, to jedna z nich. Dziękuję Panu.
Dan Chase

1
W przypadku, gdy ktoś ma problem z mkfifolub mknod -p: w moim przypadku było właściwe polecenie utworzenia pliku potoku mknod FILE_NAME p.
Karol Gil

36

Istnieje tablica, która podaje status wyjścia 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

26

To rozwiązanie działa bez użycia specyficznych funkcji bash lub plików tymczasowych. Premia: w końcu status wyjścia jest tak naprawdę statusem wyjścia, a nie ciągiem znaków w pliku.

Sytuacja:

someprog | filter

chcesz status wyjścia z someprogi wyjście zfilter .

Oto moje rozwiązanie:

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

echo $?

Zobacz moją odpowiedź na to samo pytanie na unix.stackexchange.com, aby uzyskać szczegółowe wyjaśnienie i alternatywę bez podpowłoki i niektórych zastrzeżeń.


20

Łącząc PIPESTATUS[0]wynik exitpolecenia i jego wykonanie w podpowłoce, można uzyskać bezpośredni dostęp do zwracanej wartości 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 tobie:

return value: 1


4
Dzięki, dzięki temu mogłem użyć konstrukcji: VALUE=$(might_fail | piping)która nie ustawi PIPESTATUSA w powłoce master, ale ustawi poziom błędu. Korzystając z: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})Chcę dostać chciałem.
vaab

@vaab, ta składnia wygląda naprawdę ładnie, ale nie rozumiem, co w twoim kontekście oznacza „piping”? Czy to jest właśnie miejsce, w którym można wykonać „tee” lub cokolwiek innego przetwarzając dane wyjściowe z potęgi_wybicia? ty!
AnneTheAgile

1
@AnneTheAgile „piping” w moim przykładzie oznacza polecenia, z których nie chcesz widzieć errlvl. Na przykład: jedna lub dowolna kombinacja potokowa „tee”, „grep”, „sed”, ... To nie jest tak rzadkie, że te polecenia potokowe mają na celu formatowanie lub wyodrębnianie informacji z większego wyjścia lub logu głównego polecenie: jesteś wtedy bardziej zainteresowany błędem głównego polecenia (tego, które w moim przykładzie nazwałem „may_fail”), ale bez mojej konstrukcji całe przypisanie zwraca errlvl ostatniego polecenia, które jest tutaj bez znaczenia. Czy to jest jaśniejsze?
vaab

command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}w przypadku nie tee ale filtrowania grep
user1742529

12

Chciałem więc udzielić odpowiedzi podobnej do lesmany, ale myślę, że moja jest może nieco prostsza i nieco bardziej korzystna dla powłoki z czystej powłoki Bourne'a:

# 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 najlepiej to wyjaśnić od środka - polecenie 1 wykona i wydrukuje swoje standardowe wyjście na stdout (deskryptor pliku 1), a następnie, gdy to zrobi, printf wykona i wydrukuje kod wyjścia icommand1 na swoim standardowym wyjściu, ale to standardowe wyjście jest przekierowane do deskryptor pliku 3.

Gdy polecenie1 jest uruchomione, jego standardowe wyjście jest przesyłane potokowo do polecenia2 (dane wyjściowe printf nigdy nie powodują przejścia do polecenia2, ponieważ wysyłamy go do deskryptora pliku 3 zamiast 1, co odczytuje potok). Następnie przekierowujemy dane wyjściowe polecenia 2 do deskryptora pliku 4, tak aby pozostało ono również poza deskryptorem pliku 1 - ponieważ chcemy, aby deskryptor pliku 1 był wolny przez nieco później, ponieważ sprowadzimy dane wyjściowe printf z deskryptora pliku 3 z powrotem do deskryptora pliku 1 - ponieważ to jest to, co przechwyci podstawienie polecenia (backticks) i to zostanie umieszczone w zmiennej.

Ostatnia odrobina magii jest pierwsza exec 4>&1 zrobiliśmy to jako osobne polecenie - otwiera deskryptor pliku 4 jako kopię standardowego wejścia zewnętrznej powłoki. Podstawienie polecenia uchwyci wszystko, co jest napisane na zewnątrz z perspektywy poleceń w nim zawartych - ale ponieważ dane wyjściowe polecenia 2 przechodzą do deskryptora pliku 4, jeśli chodzi o podstawienie polecenia, podstawienie polecenia nie przechwytuje go - jednak raz pobiera „z” podstawienia polecenia, nadal skutecznie przechodzi do ogólnego deskryptora pliku skryptu 1.

( exec 4>&1Musi to być osobne polecenie, ponieważ wielu powszechnym powłokom się to nie podoba, gdy próbujesz zapisać deskryptor pliku w podstawieniu polecenia, które jest otwierane w poleceniu „zewnętrznym”, które używa podstawienia. Więc to jest najprostszy przenośny sposób to zrobić.)

Możesz na to spojrzeć w mniej techniczny i bardziej zabawny sposób, tak jakby wyjścia poleceń przeskakiwały między sobą: polecenie 1 potokuje do polecenia 2, następnie dane wyjściowe printf przeskakują nad poleceniem 2, aby polecenie 2 go nie złapało, a następnie dane wyjściowe polecenia 2 przeskakują w górę i w dół od podstawienia polecenia, tak jak printf ląduje w samą porę, aby zostać przechwyconym przez podstawienie, tak że kończy się w zmiennej, a wyjście polecenia 2 idzie wesoło zapisywane na standardowe wyjście, tak jak w normalnej rurze.

Ponadto, jak 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 polecenie2 powinno zostać rozpowszechnione - to, i nie musi definiować dodatkowej funkcji, dlatego myślę, że może to być nieco lepsze rozwiązanie niż zaproponowane przez lesmana.

Zgodnie ze wspomnianymi przez lesmana zastrzeżeniami możliwe jest, że polecenie1 kiedyś skończy z deskryptorami plików 3 lub 4, więc aby być bardziej niezawodnym, wykonaj następujące czynności:

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

Zauważ, że używam poleceń złożonych w moim przykładzie, ale podpowłoki (używanie ( )zamiast { }będzie również działać, chociaż może być mniej wydajne).

Polecenia dziedziczą deskryptory plików z procesu, który je uruchamia, więc cała druga linia odziedziczy deskryptor pliku czwarty, a następnie złożone polecenie 3>&1odziedziczy deskryptor pliku trzy. Dlatego 4>&-upewnia się, że wewnętrzna komenda złożona nie odziedziczy deskryptora pliku cztery, i 3>&-nie odziedziczy deskryptora pliku trzy, więc polecenie1 otrzymuje „czystsze”, bardziej standardowe środowisko. Możesz także przesunąć wnętrze 4>&-obok 3>&-, ale myślę, że nie ograniczaj jego zakresu tak bardzo, jak to możliwe.

Nie jestem pewien, jak często rzeczy bezpośrednio używają deskryptora pliku trzeciego i czwartego - myślę, że większość programów używa syscall, które zwracają nieużywane w danym momencie deskryptory plików, ale czasami kod zapisuje bezpośrednio do deskryptora pliku 3, ja zgadnij (mógłbym sobie wyobrazić program sprawdzający deskryptor pliku, aby sprawdzić, czy jest otwarty, i używający go, jeśli jest, lub zachowujący się odpowiednio inaczej, jeśli nie jest). To drugie jest więc prawdopodobnie najlepsze, o którym należy pamiętać i stosować w przypadkach ogólnego przeznaczenia.


Ładne wyjaśnienie!
selurvedu,

6

W Ubuntu i Debian możesz apt-get install moreutils. Zawiera narzędzie zwane, mispipektóre zwraca status wyjścia pierwszego polecenia w potoku.


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

W przeciwieństwie do odpowiedzi @ cODAR, zwraca oryginalny kod wyjścia pierwszego polecenia, a nie tylko 0 dla sukcesu i 127 dla niepowodzenia. Ale jak zauważył @Chaoran, możesz po prostu zadzwonić ${PIPESTATUS[0]}. Ważne jest jednak, aby wszystko było ujęte w nawiasy.


4

Poza bash możesz:

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

Jest to przydatne na przykład w skryptach ninja, w których powinna znajdować się powłoka /bin/sh.


3

PIPESTATUS [@] należy skopiować do tablicy natychmiast po powrocie polecenia potoku. Wszelkie odczyty PIPESTATUS [@] kasują zawartość. Skopiuj go do innej tablicy, jeśli planujesz sprawdzić status wszystkich poleceń potoku. „$?” to ta sama wartość, co ostatni element „$ {PIPESTATUS [@]}”, a odczytanie go niszczy „$ {PIPESTATUS [@]}”, ale nie do końca to zweryfikowałem.

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

To nie zadziała, jeśli potok znajduje się w podpowłoce. Aby znaleźć rozwiązanie tego problemu,
zobacz bash pipestatus w poleceniu sprawdzonym wstecz?


3

Najprostszym sposobem na zrobienie tego w zwykłym bashu jest użycie podstawienia procesu zamiast potoku. Istnieje kilka różnic, ale prawdopodobnie nie mają one większego znaczenia dla Twojego przypadku użycia:

  • Podczas uruchamiania potoku bash czeka na zakończenie wszystkich procesów.
  • Wysłanie Ctrl-C do bash powoduje, że zabija wszystkie procesy potoku, nie tylko główny.
  • pipefailOpcji oraz PIPESTATUSzmienne mają znaczenia dla zastąpienia procesu.
  • Prawdopodobnie więcej

Dzięki podstawieniu procesu bash po prostu uruchamia proces i zapomina o nim, nie jest nawet widoczny w jobs.

Wspomniane różnice na bok consumer < <(producer)i producer | consumersą zasadniczo równoważne.

Jeśli chcesz odwrócić, który z nich jest „głównym” procesem, po prostu odwróć polecenia i kierunek podstawienia 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 w wyrażeniu potoku. Proces może nigdy nie zostać zatrzymany, chyba że jest wrażliwy na zamykanie się rury. W szczególności może ciągle pisać rzeczy na standardowe wyjście, co może być mylące.


1

Rozwiązanie Pure Shell:

% 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 catzastąpionym 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

Pamiętaj, że pierwszy kot również zawiedzie, ponieważ jego stdout zostaje na nim zamknięty. Kolejność nieudanych poleceń w dzienniku jest poprawna w tym przykładzie, ale nie należy na nim polegać.

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


1

Oprzyj się na odpowiedzi @ brian-s-wilson; ta funkcja pomocnika bash:

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

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

używane w ten sposób:

1: get_bad_things musi odnieść sukces, ale nie powinno generować żadnych wyników; ale chcemy zobaczyć wyniki, które produkuje

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

2: cały potok musi się powieść

thing | something -q | thingy
pipeinfo || return

1

Czasami użycie polecenia zewnętrznego może być prostsze i bardziej zrozumiałe niż zagłębianie się w szczegóły bash. rurociąg , z minimalnym skryptowy proces języka execline , wyjścia z kodem powrotu drugiego polecenia *, podobnie jak shrurociąg robi, ale w przeciwieństwie sh, umożliwia zmianę kierunku rury, tak, że możemy uchwycić kod powrotny producenta proces (poniżej wszystko jest w shwierszu poleceń, ale z execlinezainstalowanym):

$ # 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 pipelinema takie same różnice w stosunku do natywnych potoków bash, jak podstawienie procesu bash zastosowane w odpowiedzi # 43972501 .

* Właściwie pipelinewcale nie wychodzi, chyba że wystąpi błąd. Wykonuje się na drugie polecenie, więc jest to drugie polecenie, które powraca.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.