Catturare i codici di errore in un tubo di shell


97

Al momento ho uno script che fa qualcosa di simile

./a | ./b | ./c

Voglio modificarlo in modo che se uno qualsiasi di a, boc esce con un codice di errore, stampo un messaggio di errore e mi fermo invece di inviare un output errato in avanti.

Quale sarebbe il modo più semplice / pulito per farlo?


7
Ci deve davvero essere qualcosa di simile &&|che significherebbe "continuare il pipe solo se il comando precedente ha avuto successo". Suppongo che potresti anche avere |||che significherebbe "continuare la pipe se il comando precedente fallisce" (e possibilmente pipe il messaggio di errore come quello di Bash 4 |&).
In pausa fino a nuovo avviso.

6
@DennisWilliamson, non è possibile "fermare il tubo" perché a, b, ci comandi non vengono eseguiti in sequenza, ma in parallelo. In altre parole, flussi di dati in sequenza da aa c, ma l'attuale a, be ccomandi start (approssimativamente) allo stesso tempo.
Giacomo

Risposte:


20

Se davvero non vuoi che il secondo comando proceda fino a quando il primo non è noto per avere successo, allora probabilmente devi usare file temporanei. La versione semplice di questo è:

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]

Il reindirizzamento "1> & 2" può anche essere abbreviato "> & 2"; tuttavia, una vecchia versione della shell MKS gestiva in modo errato il reindirizzamento dell'errore senza il precedente "1", quindi ho usato quella notazione univoca per l'affidabilità per anni.

Questo perde file se interrompi qualcosa. La programmazione della shell a prova di bomba (più o meno) utilizza:

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

La prima riga di trap dice "esegui i comandi" rm -f $tmp.[12]; exit 1quando si verifica uno dei segnali 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE o 15 SIGTERM, o 0 (quando la shell esce per qualsiasi motivo). Se stai scrivendo uno script di shell, la trappola finale deve solo rimuovere la trappola su 0, che è la trappola di uscita dalla shell (puoi lasciare gli altri segnali sul posto poiché il processo sta per terminare comunque).

Nella pipeline originale, è possibile che "c" legga i dati da "b" prima che "a" abbia finito - questo di solito è desiderabile (ad esempio, dà lavoro a più core). Se "b" è una fase di "ordinamento", questo non si applica: "b" deve vedere tutti i suoi input prima di poter generare uno qualsiasi dei suoi output.

Se vuoi rilevare quali comandi falliscono, puoi usare:

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

Questo è semplice e simmetrico: è banale estenderlo a una pipeline di 4 o N parti.

La semplice sperimentazione con 'set -e' non ha aiutato.


1
Consiglierei di usare mktempo tempfile.
In pausa fino a nuovo avviso.

@ Dennis: sì, suppongo che dovrei abituarmi a comandi come mktemp o tmpfile; non esistevano a livello di shell quando l'ho imparato, oh così tanti anni fa. Facciamo un rapido controllo. Trovo mktemp su MacOS X; Ho mktemp su Solaris ma solo perché ho installato strumenti GNU; sembra che mktemp sia presente sugli antichi HP-UX. Non sono sicuro che esista una chiamata comune di mktemp che funziona su tutte le piattaforme. POSIX non standardizza né mktemp né tmpfile. Non ho trovato tmpfile sulle piattaforme a cui ho accesso. Di conseguenza, non sarò in grado di utilizzare i comandi negli script di shell portatili.
Jonathan Leffler,

1
Quando si utilizza, traptenere presente che l'utente può sempre inviare SIGKILLa un processo per interromperlo immediatamente, nel qual caso il trap non avrà effetto. Lo stesso vale per quando il tuo sistema subisce un'interruzione di corrente. Quando crei file temporanei, assicurati di usarli mktempperché ciò metterà i tuoi file da qualche parte, dove verranno puliti dopo un riavvio (di solito in /tmp).
Josch

156

In bash puoi usare set -ee set -o pipefailall'inizio del tuo file. Un comando successivo ./a | ./b | ./cfallirà quando uno dei tre script fallisce. Il codice di ritorno sarà il codice di ritorno del primo script non riuscito.

Nota che pipefailnon è disponibile nello standard sh .


Non sapevo di pipefail, davvero utile.
Phil Jackson

Ha lo scopo di metterlo in uno script, non nella shell interattiva. Il tuo comportamento può essere semplificato impostando -e; falso; esce anche dal processo di shell, che è un comportamento previsto;)
Michel Samia

11
Nota: questo eseguirà comunque tutti e tre gli script e non interromperà il pipe al primo errore.
hyde

1
@ n2liquid-GuilhermeVieira Btw, con "diverse varianti", intendevo specificamente, rimuovi uno o entrambi set(per 4 versioni diverse in totale), e guarda come questo influisce sull'output dell'ultimo echo.
hyde

1
@josch Ho trovato questa pagina durante la ricerca di come farlo in bash da google, e tuttavia, "Questo è esattamente quello che sto cercando". Sospetto che molte persone che danno un voto positivo alle risposte seguano un processo di pensiero simile e non controllano i tag e le definizioni dei tag.
Troy Daniels

44

Puoi anche controllare l' ${PIPESTATUS[]}array dopo l'esecuzione completa, ad esempio se esegui:

./a | ./b | ./c

Quindi ${PIPESTATUS}ci sarà un array di codici di errore da ogni comando nella pipe, quindi se il comando centrale fallisce, echo ${PIPESTATUS[@]}conterrà qualcosa come:

0 1 0

e qualcosa del genere viene eseguito dopo il comando:

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

ti permetterà di controllare che tutti i comandi nella pipe siano stati eseguiti correttamente.


11
Questo è bashish --- è un'estensione bash e non fa parte dello standard Posix, quindi altre shell come dash e ash non lo supporteranno. Ciò significa che puoi avere problemi se provi a usarlo in script che si avviano #!/bin/sh, perché se shnon è bash, non funzionerà. (Facilmente risolvibile ricordandosi di usare #!/bin/bashinvece).
David Given

2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- restituisce 0 se $PIPESTATUS[@]contiene solo 0 e spazi (se tutti i comandi nella pipe hanno esito positivo).
MattBianco

@ MattBianco Questa è la soluzione migliore per me. Funziona anche con && es. command1 && command2 | command3Se qualcuno di questi fallisce, la tua soluzione restituisce un valore diverso da zero.
DavidC

8

Sfortunatamente, la risposta di Johnathan richiede file temporanei e le risposte di Michel e Imron richiedono bash (anche se questa domanda è contrassegnata come shell). Come già sottolineato da altri, non è possibile interrompere il pipe prima che vengano avviati processi successivi. Tutti i processi vengono avviati contemporaneamente e verranno quindi eseguiti tutti prima che venga comunicato qualsiasi errore. Ma il titolo della domanda riguardava anche i codici di errore. Questi possono essere recuperati e analizzati dopo che il tubo è terminato per capire se uno dei processi coinvolti non è riuscito.

Ecco una soluzione che cattura tutti gli errori nel tubo e non solo gli errori dell'ultimo componente. Quindi questo è come il pipefail di bash, solo più potente nel senso che puoi recuperare tutti i codici di errore.

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

Per rilevare se qualcosa non è riuscito, viene echostampato un comando in caso di errore standard nel caso in cui un comando fallisca. Quindi l'output di errore standard combinato viene salvato $rese analizzato in seguito. Questo è anche il motivo per cui l'errore standard di tutti i processi viene reindirizzato allo standard output. Puoi anche inviare quell'output /dev/nullo lasciarlo come un altro indicatore che qualcosa è andato storto. È possibile sostituire l'ultimo reindirizzamento a /dev/nullcon un file se non è necessario memorizzare l'output dell'ultimo comando ovunque.

Per giocare di più con questo costrutto e convincerti che questo fa davvero quello che dovrebbe, ho sostituito ./a, ./be ./cda subshell che eseguono echo, cate exit. È possibile utilizzarlo per verificare che questo costrutto inoltri davvero tutto l'output da un processo a un altro e che i codici di errore vengano registrati correttamente.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.