Przechwytywanie kodów błędów w rurze powłoki

97

Obecnie mam skrypt, który robi coś takiego

./a | ./b | ./c

Chcę go zmodyfikować, aby w przypadku wyjścia a, b lub c z kodem błędu wydrukowałem komunikat o błędzie i zatrzymałem się zamiast przesyłać do przodu złe wyjście.

Jaki byłby najprostszy / najczystszy sposób, aby to zrobić?

hugomg
źródło
7
Naprawdę musi istnieć coś takiego, &&|co oznaczałoby „kontynuowanie potoku tylko wtedy, gdy poprzednie polecenie się powiodło”. Przypuszczam, że mógłbyś również mieć, |||co oznaczałoby „kontynuuj potok, jeśli poprzednie polecenie zawiodło” (i prawdopodobnie potokuj komunikat o błędzie jak Bash 4 |&).
Wstrzymano do odwołania.
6
@DennisWilliamson, nie można „zatrzymać rurę”, ponieważ a, b, cpolecenia nie są uruchamiane sekwencyjnie, ale równolegle. Innymi słowy, dane płynie kolejno od ado c, ale rzeczywiste a, bi czacząć polecenia (w przybliżeniu) w tym samym czasie.
Giacomo

Odpowiedzi:

20

Jeśli naprawdę nie chcesz, aby drugie polecenie było kontynuowane, dopóki pierwsze nie zakończy się sukcesem, prawdopodobnie musisz użyć plików tymczasowych. Prosta wersja to:

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

Przekierowanie „1> & 2” można również skrócić „> & 2”; jednak stara wersja powłoki MKS błędnie radziła sobie z przekierowaniem błędu bez poprzedzającej je cyfry „1”, więc od wieków używam tej jednoznacznej notacji dla zapewnienia niezawodności.

To powoduje wyciek plików, jeśli coś przerwiesz. Programowanie pocisków odpornych na bomby (mniej lub bardziej) wykorzystuje:

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

Pierwsza linia pułapki mówi „uruchom komendy” rm -f $tmp.[12]; exit 1, gdy wystąpi którykolwiek z sygnałów 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE lub 15 SIGTERM, lub 0 (gdy powłoka kończy pracę z dowolnego powodu). Jeśli piszesz skrypt powłoki, ostatnia pułapka musi tylko usunąć pułapkę na 0, która jest pułapką wyjściową powłoki (możesz pozostawić inne sygnały na miejscu, ponieważ proces i tak wkrótce się zakończy).

W pierwotnym potoku możliwe jest, że „c” będzie czytać dane z „b” przed zakończeniem „a” - jest to zwykle pożądane (na przykład daje wiele rdzeni do wykonania). Jeśli „b” jest fazą „sortowania”, to nie ma to zastosowania - „b” musi zobaczyć wszystkie swoje dane wejściowe, zanim będzie mógł wygenerować jakiekolwiek dane wyjściowe.

Jeśli chcesz wykryć, które polecenia zawodzą, możesz użyć:

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

Jest to proste i symetryczne - trywialne jest rozszerzenie do potoku 4-częściowego lub N-częściowego.

Proste eksperymentowanie z „set -e” nie pomogło.

Jonathan Leffler
źródło
1
Polecam użycie mktemplub tempfile.
Wstrzymano do odwołania.
@Dennis: tak, przypuszczam, że powinienem przyzwyczaić się do poleceń takich jak mktemp czy tmpfile; nie istniały na poziomie powłoki, kiedy się tego nauczyłem, och, tak wiele lat temu. Zróbmy szybkie sprawdzenie. Znalazłem mktemp na MacOS X; Mam mktemp na Solarisie, ale tylko dlatego, że zainstalowałem narzędzia GNU; wydaje się, że mktemp jest obecny na starym HP-UX. Nie jestem pewien, czy istnieje powszechne wywołanie mktemp, które działa na różnych platformach. POSIX nie standaryzuje ani mktemp, ani tmpfile. Nie znalazłem tmpfile na platformach, do których mam dostęp. W związku z tym nie będę mógł używać poleceń w przenośnych skryptach powłoki.
Jonathan Leffler
1
Podczas korzystania trapnależy pamiętać, że użytkownik zawsze może wysłać SIGKILLdo procesu natychmiastowe zakończenie, w którym to przypadku pułapka nie zadziała. To samo dotyczy sytuacji, gdy w systemie wystąpi awaria zasilania. Podczas tworzenia plików tymczasowych upewnij się, że używasz, mktempponieważ spowoduje to umieszczenie plików gdzieś, gdzie zostaną wyczyszczone po ponownym uruchomieniu (zwykle do /tmp).
josch
156

W bashu możesz użyć set -ei set -o pipefailna początku pliku. Kolejne polecenie ./a | ./b | ./czakończy się niepowodzeniem, gdy zawiedzie którykolwiek z trzech skryptów. Kod powrotu będzie kodem powrotu pierwszego skryptu, który zakończył się niepowodzeniem.

Pamiętaj, że pipefailnie jest dostępny w standardowym sh .

Michel Samia
źródło
Nie wiedziałem o pipefail, naprawdę przydatne.
Phil Jackson
Ma to na celu umieszczenie go w skrypcie, a nie w powłoce interaktywnej. Twoje zachowanie można uprościć, ustawiając -e; fałszywe; kończy również proces powłoki, co jest oczekiwanym zachowaniem;)
Michel Samia
11
Uwaga: to nadal spowoduje wykonanie wszystkich trzech skryptów i nie zatrzyma potoku przy pierwszym błędzie.
hyde
1
@ n2liquid-GuilhermeVieira Btw, przez „różne warianty”, konkretnie miałem na myśli usunięcie jednej lub obu set(łącznie dla 4 różnych wersji) i zobaczenie, jak to wpływa na wynik ostatniej echo.
hyde
1
@josch Znalazłem tę stronę, szukając informacji, jak to zrobić w bash w Google, i chociaż: „Dokładnie tego szukam”. Podejrzewam, że wiele osób, które zagłosują za odpowiedziami, przechodzi podobny proces myślowy i nie sprawdza tagów ani ich definicji.
Troy Daniels
44

Możesz też sprawdzić ${PIPESTATUS[]}tablicę po pełnym wykonaniu, np. Jeśli uruchomisz:

./a | ./b | ./c

Następnie ${PIPESTATUS}pojawi się tablica kodów błędów z każdego polecenia w potoku, więc jeśli środkowe polecenie zawiodło, echo ${PIPESTATUS[@]}zawierałoby coś takiego:

0 1 0

i coś takiego wykonujemy po poleceniu:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

pozwoli ci sprawdzić, czy wszystkie polecenia w potoku powiodły się.

Imron
źródło
11
To jest bashish - to rozszerzenie bash i nie jest częścią standardu Posix, więc inne powłoki, takie jak dash i ash, nie będą go obsługiwać. Oznacza to, że możesz wpaść w kłopoty, jeśli spróbujesz go użyć w skryptach, które się uruchamiają #!/bin/sh, ponieważ jeśli shnie jest to bash, to nie zadziała. (Łatwe do naprawienia, pamiętając o używaniu #!/bin/bashzamiast tego.)
David Given
2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- zwraca 0, jeśli $PIPESTATUS[@]zawiera tylko 0 i spacje (jeśli wszystkie polecenia w potoku się powiodły).
MattBianco,
@MattBianco To dla mnie najlepsze rozwiązanie. Działa również z && np. command1 && command2 | command3Jeśli którykolwiek z nich zawiedzie, twoje rozwiązanie zwraca wartość niezerową.
DavidC,
8

Niestety, odpowiedź Johnathana wymaga plików tymczasowych, a odpowiedzi Michela i Imrona wymagają basha (mimo że to pytanie jest oznaczone jako powłoka). Jak już zauważyli inni, nie jest możliwe przerwanie potoku przed rozpoczęciem późniejszych procesów. Wszystkie procesy są uruchamiane od razu i dlatego będą działać, zanim będzie można zgłosić jakiekolwiek błędy. Ale tytuł pytania dotyczył również kodów błędów. Można je odzyskać i zbadać po zakończeniu rury, aby ustalić, czy którykolwiek z zaangażowanych procesów zawiódł.

Oto rozwiązanie, które wyłapuje wszystkie błędy w rurze, a nie tylko błędy ostatniego komponentu. Tak to jest jak pipefail atakujących, tuż mocniejsze w tym sensie, że można odzyskać wszystkie kody błędów.

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Aby wykryć, czy coś nie powiodło, echopolecenie wypisuje na standardowym wyjściu błędów w przypadku dowolnego polecenia nie powiedzie się. Następnie połączone standardowe wyjście błędów jest zapisywane $resi analizowane później. Dlatego też standardowy błąd wszystkich procesów jest przekierowywany na standardowe wyjście. Możesz również wysłać te dane wyjściowe /dev/nulllub pozostawić je jako kolejny wskaźnik, że coś poszło nie tak. Możesz zastąpić ostatnie przekierowanie /dev/nullplikiem, jeśli nie chcesz przechowywać danych wyjściowych ostatniego polecenia w dowolnym miejscu.

Aby grać więcej z tego konstruktu i przekonać się, że to naprawdę robi to, co powinien, wymieniłem ./a, ./ba ./cprzez podpowłok, które wykonują echo, catoraz exit. Możesz użyć tego, aby sprawdzić, czy ta konstrukcja rzeczywiście przekazuje wszystkie dane wyjściowe z jednego procesu do drugiego i czy kody błędów są poprawnie rejestrowane.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
josch
źródło