2009-08-03 15 views
313

Desidero eseguire un comando a esecuzione prolungata in Bash, ed entrambi catturano il suo stato di uscita e l'output tee.Uscita pipe e stato di uscita acquisizione in Bash

così faccio questo:

command | tee out.txt 
ST=$? 

Il problema è che la variabile ST cattura lo stato di uscita tee e non di comando. Come posso risolvere questo?

Si noti che il comando è di lunga durata e il reindirizzamento dell'output su un file per visualizzarlo in seguito non è una buona soluzione per me.

+1

[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Corrispondenza - errore rilevato" || echo -e "Nessuna corrispondenza - tutto ok" Verranno testati tutti i valori dell'array in una sola volta e verrà visualizzato un messaggio di errore se uno dei valori di pipe restituiti non è zero. Questa è una soluzione generalizzata piuttosto robusta per il rilevamento di errori in una situazione di pipe. –

+0

http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another –

risposta

408

C'è una variabile Bash interna denominata $PIPESTATUS; è una matrice che mantiene lo stato di uscita di ogni comando nella tua ultima pipeline di comandi in primo piano.

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

O un'altra alternativa che funziona anche con altre shell (come zsh) potrebbe essere quella di consentire pipefail:

set -o pipefail 
... 

La prima opzione fa non lavoro con zsh a causa di una sintassi diversa po ' .

+17

C'è una buona spiegazione con esempi di PIPESTATUS E Pipefail qui: http://unix.stackexchange.com/a/73180/7453. – slm

+14

Nota: $ PIPESTATUS [0] contiene lo stato di uscita del primo comando nella pipe, $ PIPESTATUS [1] lo stato di uscita del secondo comando e così via. – simpleuser

+16

Ovviamente, dobbiamo ricordare che questo è specifico di Bash: se dovessi (ad esempio) scrivere uno script da eseguire sull'implementazione "sh" di BusyBox sul mio dispositivo Android, o su qualche altra piattaforma incorporata usando qualche altro "sh" "variante, questo non funzionerebbe. –

87

Soluzione stupida: collegandole tramite una named pipe (mkfifo). Quindi il comando può essere eseguito secondo.

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

Questa è l'unica risposta in questa domanda che funziona anche per il semplice ** sh ** shell Unix. Grazie! – JamesThomasMoon1979

+0

Perché questo è stupido? –

+1

@DaveKennedy: stupido come in "ovvio, non richiede una conoscenza complessa della sintassi di bash" – EFraim

33

C'è una matrice che fornisce lo stato di uscita di ogni comando in una pipe.

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

Questa soluzione funziona senza utilizzare funzioni specifiche di bash o file temporanei. Bonus: alla fine lo stato di uscita è in realtà uno stato di uscita e non una stringa in un file.

Situazione:

someprog | filter 

si desidera che lo stato di uscita dalla someprog e l'uscita da filter.

Ecco la mia soluzione:

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

echo $? 

Vedi my answer for the same question on unix.stackexchange.com per una spiegazione dettagliata di come funziona e alcuni avvertimenti.

16

Combinando PIPESTATUS[0] e il risultato dell'esecuzione del comando exit in una subshell, è possibile accedere direttamente il valore di ritorno del comando iniziale:

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

Ecco un esempio:

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

ti darà:

return value: 1

+4

Grazie, questo mi ha permesso di usare il costrutto: '' VALORE = $ (might_fail | piping) '' che non imposterà PIPESTATUS nella shell principale ma imposterà il suo errorlevel. Usando: '' VALORE = $ (might_fail | piping; exit $ {PIPESTATUS [0]}) '' Ho bisogno di volerlo. – vaab

+0

@vaab, quella sintassi sembra davvero bella ma sono confuso su cosa "piping" significhi nel tuo contesto? È esattamente dove si dovrebbe fare "tee" o qualsiasi elaborazione sull'output di might_fail? ty! – AnneTheAgile

+1

@AnneTheAgile 'piping' nel mio esempio indica comandi da cui non si desidera visualizzare l'errlvl. Ad esempio: una combinazione o qualsiasi combinazione di pipe di 'tee', 'grep', 'sed', ... Non è così raro che questi comandi di piping abbiano lo scopo di formattare o estrarre informazioni da un output più grande o dall'output del log principale comando: sei più interessato al malfunzionamento del comando principale (quello che ho chiamato 'might_fail' nel mio esempio) ma senza il mio costrutto l'intera assegnazione restituisce l'errore errlvl dell'ultimo comando che è qui privo di significato. È più chiaro? – vaab

3

PIPESTATUS [@] deve essere copiato su un array immediatamente dopo il ritorno del comando pipe. Qualsiasi lettura di PIPESTATUS [@] cancella il contenuto. Copialo su un altro array se prevedi di controllare lo stato di tutti i comandi di pipe. "$?" è lo stesso valore dell'ultimo elemento di "$ {PIPESTATUS [@]}", e la lettura sembra distruggere "$ {PIPESTATUS [@]}", ma non l'ho assolutamente verificato.

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

Questo non funzionerà se la pipa si trova in una sotto-shell. Per una soluzione a questo problema,
vedere bash pipestatus in backticked command?

123

utilizza di set -o pipefail bash è utile

pipefail: il valore di ritorno di una pipeline è lo stato della l'ultimo comando per uscire con un non-zero di stato , o zero se nessun comando uscito con uno stato diverso da zero

+17

Nel caso in cui non si desideri modificare l'impostazione pipefail dell'intero script, è possibile impostare l'opzione solo localmente: '(set -o pipefail; comando | tee out.txt); ST = $? ' – Jaan

+6

@Jaan Avrebbe eseguito una subshell. Se vuoi evitarlo, puoi fare "set -o pipefail" e poi eseguire il comando, e subito dopo "set + o pipefail" per annullare l'opzione. –

+0

Nota: il manifesto della domanda non vuole un "codice di uscita generale" della pipa, vuole il codice di ritorno di "comando". Con '-o pipefail' saprebbe se la pipe fallisce, ma se entrambi 'command' e 'tee' falliscono, riceve il codice di uscita da 'tee'. – t0r0X

3

In Ubuntu e Debian, è possibile apt-get install moreutils. Questo contiene un'utilità chiamata mispipe che restituisce lo stato di uscita del primo comando nella pipe.

1

soluzione 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 

E ora con la seconda cat sostituito da 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 

Si prega di notare il primo gatto non riesce così, perché è stdout viene chiuso su di esso. L'ordine dei comandi non riusciti nel registro è corretto in questo esempio, ma non fare affidamento su di esso.

Questo metodo consente di acquisire stdout e stderr per i singoli comandi in modo da poter poi eseguire il dump in un file di registro se si verifica un errore o semplicemente eliminarlo in caso di errore (come l'output di dd).

7

così ho voluto contribuire una risposta del tipo Lesmana di, ma credo che il mio è forse un po 'più semplice e un po' più vantaggiosa soluzione pura-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. 

Credo che questo è meglio spiegato dal di dentro out - command1 eseguirà e stampare la sua uscita regolare su stdout (descrittore di file 1), poi una volta che è fatto, printf eseguirà e il codice di uscita del icommand1 di stampa sul suo stdout, ma che stdout viene rediretto nel descrittore di file 3.

Mentre command1 è in esecuzione, il suo stdout viene inviato al comando2 (l'output di printf non lo rende mai a comando2 perché diventa usiamo lo inviamo al descrittore di file 3 invece di 1, che è ciò che legge la pipa). Quindi reindirizziamo l'output di command2 al descrittore di file 4, in modo che rimanga fuori dal descrittore di file 1 - perché vogliamo che il descrittore di file 1 sia gratuito per un po 'più tardi, perché riporteremo l'output di printf sul descrittore di file 3 nel descrittore di file 1 - perché è ciò che la sostituzione di comando (i backtick), catturerà e questo è ciò che verrà inserito nella variabile.

L'ultimo bit di magia è che il primo exec 4>&1 è stato eseguito come comando separato: apre il descrittore di file 4 come copia dello stdout della shell esterna.La sostituzione di comando catturerà tutto ciò che è scritto sullo standard fuori dalla prospettiva dei comandi al suo interno - ma poiché l'output di comando2 sta per il descrittore di file 4 per quanto riguarda la sostituzione di comando, la sostituzione di comando non la cattura - tuttavia una volta ottiene "out" della sostituzione di comando che sta effettivamente andando al descrittore di file complessivo dello script 1.

(Lo exec 4>&1 deve essere un comando separato perché molte shell comuni non lo apprezzano quando si tenta di scrivere su un descrittore di file all'interno di una sostituzione di comando, che viene aperta nel comando "esterno" che utilizza la sostituzione.Questo è il modo più semplice di farlo.)

Puoi guardarlo in modo meno tecnico e più giocoso modo, come se le uscite dei comandi si scavalassero a vicenda: comando1 pipe a comando2, quindi l'output di printf salta sul comando 2 in modo che comando2 non lo prenda, e quindi l'uscita di comando di 2 salta fuori dalla sostituzione di comando proprio come printf atterra appena in tempo per essere catturato dalla sostituzione in modo che finisca nella variabile, e l'output di comando2 vada sul suo buon modo di essere scritto sullo standard output, proprio come in una pipe normale.

Inoltre, come risulta, $? conterrà ancora il codice di ritorno del secondo comando nel tubo, perché le assegnazioni, sostituzioni di comando e comandi composti variabili sono tutte effettivamente trasparenti per il codice di ritorno del comando al loro interno, quindi lo stato di ritorno di command2 dovrebbe essere propagato - questo, e non dover definire una funzione aggiuntiva, è il motivo per cui penso che questa potrebbe essere una soluzione migliore rispetto a quella proposta da lesmana.

Per i caveat Lesmana cita, è possibile che command1 sarà ad un certo punto finire con descrittori di file 3 o 4, in modo da essere più robusto, si dovrebbe fare:

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

Nota che uso comandi composti nel mio esempio, ma sottoshell (utilizzando () invece di { } sarà anche funzionare, anche se può forse essere meno efficiente.)

comandi ereditano i descrittori di file dal processo che li lancia, quindi l'intera seconda linea erediterà descrittore di file di quattro, e il comando composto seguito da 3>&1 erediterà il descrittore di file tre. Quindi lo 4>&- si assicura che il comando composto interno non erediti il ​​descrittore di file quattro, e 3>&- non erediterà il descrittore di file tre, quindi comando1 ottiene un ambiente più "pulito", più standard. È anche possibile spostare l'interno 4>&- accanto allo 3>&-, ma immagino perché non limitarlo il più possibile.

Non sono sicuro di quanto spesso le cose utilizzino direttamente il descrittore di file tre e quattro: penso che la maggior parte dei programmi temporali utilizzi syscalls che restituiscono descrittori di file non utilizzati al momento, ma a volte scrivono codice nel descrittore di file 3, immagino (potrei immaginare un programma che controlli un descrittore di file per vedere se è aperto, e usarlo se lo è, o comportarsi in modo diverso di conseguenza se non lo è). Quindi quest'ultimo è probabilmente il migliore da tenere a mente e da usare per casi generici.

1

Base sulla risposta di @ brian-s-wilson; questa funzione bash aiutante:

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

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

utilizzato così:

1: get_bad_things devono avere successo, ma dovrebbe produrre alcun output; ma vogliamo vedere in uscita che non produce

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

2: tutto gasdotto deve riuscire

thing | something -q | thingy 
pipeinfo || return 
2

Al di fuori di bash, si può fare:

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

Questo è utile per esempio, in script ninja dove ci si aspetta che la shell sia /bin/sh.

1

A volte può essere più semplice e più chiaro utilizzare un comando esterno anziché scavare nei dettagli di bash. pipeline, dal linguaggio di script di processo minimo execline, esce con il codice di ritorno del secondo comando *, proprio come fa una pipeline sh, ma a differenza di sh, consente di invertire la direzione della pipe, in modo da poter acquisire il codice di ritorno di il processo produttore (il sotto è tutto sulla linea di comando sh, ma con execline installato):

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

Uso pipeline ha le stesse differenze con tubazioni bash native come la sostituzione di processo bash usato in risposta #43972501.

* In realtà pipeline non esce affatto a meno che non si sia verificato un errore. Esegue il secondo comando, quindi è il secondo comando che esegue il ritorno.

2

Il modo più semplice per farlo in chiaro è utilizzare process substitution anziché una pipeline. Ci sono molte differenze, ma probabilmente non contano molto per il tuo caso d'uso:

  • Quando si esegue una pipeline, bash attende fino al completamento di tutti i processi.
  • L'invio da Ctrl-C a bash consente di uccidere tutti i processi di una pipeline, non solo quello principale.
  • L'opzione pipefail e la variabile PIPESTATUS sono irrilevanti per l'elaborazione della sostituzione.
  • Forse più

Con sostituzione di processo, bash appena inizia il processo e dimentica a questo proposito, non è nemmeno visibile in jobs.

Le differenze menzionate a parte, consumer < <(producer) e producer | consumer sono essenzialmente equivalenti.

Se si desidera riflettere su quale sia il processo "principale", basta capovolgere i comandi e la direzione della sostituzione su producer > >(consumer).Nel tuo caso:

command > >(tee out.txt) 

Esempio:

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

Come ho detto, ci sono differenze rispetto l'espressione del tubo. Il processo potrebbe non smettere mai di funzionare, a meno che non sia sensibile alla chiusura del tubo. In particolare, potrebbe continuare a scrivere cose sul tuo stdout, il che potrebbe essere fonte di confusione.

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

A differenza della risposta di @ cODAR, restituisce il codice di uscita originale del primo comando e non solo 0 per il successo e 127 per il fallimento. Ma come ha sottolineato @Chaoran, puoi chiamare lo ${PIPESTATUS[0]}. È importante tuttavia che tutto sia messo tra parentesi.