Uscita pipe e stato di uscita acquisizione in Bash


421

Voglio eseguire un comando lungo in esecuzione in Bash, ed entrambi cattura suo stato di uscita, e la tee sua uscita.

Quindi faccio questo:

command | tee out.txt
ST=$?

Il problema è che la variabile ST acquisisce lo stato di uscita di teee non di comando. Come posso risolvere questo?

Si noti che il comando è in esecuzione da molto tempo e reindirizza l'output su un file per visualizzarlo in seguito non è una buona soluzione per me.


1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Corrispondenza - errore trovato" || echo -e "Nessuna corrispondenza - tutto buono" Questo verificherà tutti i valori dell'array in una sola volta e darà un messaggio di errore se uno qualsiasi dei valori di pipe restituiti non è zero. Questa è una soluzione generalizzata piuttosto robusta per rilevare errori in una situazione convogliata.
Brian S. Wilson,

Risposte:


519

C'è una variabile Bash interna chiamata $PIPESTATUS; è un array che contiene lo stato di uscita di ciascun comando nell'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) sarebbe quella di abilitare pipefail:

set -o pipefail
...

La prima opzione non funziona a zshcausa della sintassi leggermente diversa.


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

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

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

4
Per chi è preoccupato per l'espansione variabile non quotata: lo stato di uscita è sempre intero senza segno a 8 bit in Bash , quindi non è necessario citarlo. Ciò vale anche in Unix, in cui lo stato di uscita è definito in modo esplicito a 8 bit e si presume che sia senza segno anche da POSIX stesso, ad esempio quando si definisce la sua negazione logica .
Palec,

3
Puoi anche usare exit ${PIPESTATUS[0]}.
Chaoran,

142

usare bash set -o pipefailè utile

pipefail: il valore di ritorno di una pipeline è lo stato dell'ultimo comando per uscire con uno stato diverso da zero o zero se nessun comando è uscito con uno stato diverso da zero


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

7
@Jaan Questo eseguirà una subshell. Se si desidera evitarlo, è possibile eseguire set -o pipefaile quindi eseguire il comando, quindi immediatamente set +o pipefaildeselezionare l'opzione.
Linus Arver,

2
Nota: il poster della domanda non vuole un "codice di uscita generale" della pipe, ma vuole il codice di ritorno di "comando". Con -o pipefaillui avrebbe saputo se la pipe falliva, ma se sia "command" che "tee" fallissero, avrebbe ricevuto il codice di uscita da "tee".
t0r0X

@LinusArver non cancellerebbe il codice di uscita poiché è un comando che ha esito positivo?
carlin.scott,

127

Soluzione stupida: collegandoli tramite una pipe denominata (mkfifo). Quindi il comando può essere eseguito per secondo.

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

20
Questa è l'unica risposta a questa domanda che funziona anche con la semplice shell sh Unix. Grazie!
JamesThomasMoon1979,

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

10
Mentre le risposte bash sono più eleganti quando si ha il vantaggio delle capacità extra di bash, questa è la soluzione più multipiattaforma. È anche qualcosa a cui vale la pena pensare in generale poiché ogni volta che si esegue un comando di lunga durata, una pipa con i nomi è spesso il modo più flessibile. Vale la pena notare che alcuni sistemi non hanno mkfifoe potrebbero invece richiedere mknod -pse ricordo bene.
Haravikk,

3
A volte sullo stack overflow ci sono risposte che potresti votare un centinaio di volte in modo che la gente smetta di fare altre cose che non hanno senso, questa è una di queste. Grazie Signore.
Dan Chase,

1
Nel caso in cui qualcuno abbia un problema con mkfifoo mknod -p: nel mio caso il comando corretto per creare il file pipe è stato mknod FILE_NAME p.
Karol Gil,

36

C'è un array che ti dà 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

26

Questa soluzione funziona senza utilizzare funzionalità 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 lo stato di uscita someproge l'output da filter.

Ecco la mia soluzione:

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

echo $?

Vedi la mia risposta per la stessa domanda su unix.stackexchange.com per una spiegazione dettagliata e un'alternativa senza sottotitoli e alcuni avvertimenti.


20

Combinando PIPESTATUS[0]e il risultato dell'esecuzione del exitcomando in una subshell, è possibile accedere direttamente al valore restituito 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: VALUE=$(might_fail | piping)che non imposterà PIPESTATUS nella shell principale ma imposterà il suo livello di errore. Usando: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})ottengo ciò che volevo.
vaab,

@vaab, quella sintassi sembra davvero carina ma sono confuso su cosa significa "piping" nel tuo contesto? È proprio lì che si farebbe "tee" o qualunque elaborazione sull'output di might_fail? ty!
AnneTheAgile

1
@AnneTheAgile 'piping' nel mio esempio sta per comandi da cui non vuoi vedere l'errlvl. Ad esempio: una delle o una qualsiasi combinazione 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 registrare l'output del main comando: sei quindi più interessato al livello di errore del comando principale (quello che ho chiamato 'might_fail' nel mio esempio) ma senza il mio costrutto l'intera assegnazione restituisce l'errore dell'ultimo comando in pipe che è qui insignificante. È più chiaro?
vaab,

command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}nel caso non tee ma filtro grep
user1742529

12

Quindi volevo contribuire con una risposta come quella di Lesmana, ma penso che la mia sia forse una soluzione pura-Bourne-shell un po 'più semplice e leggermente più vantaggiosa:

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

Penso che questo sia meglio spiegato dall'interno - comando1 eseguirà e stamperà il suo normale output su stdout (descrittore di file 1), quindi una volta fatto, printf eseguirà e stamperà il codice di uscita di icommand1 sul suo stdout, ma lo stdout viene reindirizzato a descrittore di file 3.

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

L'ultimo aspetto magico è che per prima cosa exec 4>&1abbiamo fatto un comando separato: apre il descrittore di file 4 come copia dello stdout della shell esterna. La sostituzione dei comandi catturerà tutto ciò che è scritto sullo standard dalla prospettiva dei comandi al suo interno - ma poiché l'output di command2 sta andando a descrivere il descrittore di file 4 per quanto riguarda la sostituzione dei comandi, la sostituzione dei comandi non lo cattura - tuttavia una volta ottiene "fuori" dalla sostituzione del comando, in realtà sta ancora andando al descrittore di file complessivo dello script 1.

( exec 4>&1Deve essere un comando separato perché a molte shell comuni non piace quando si tenta di scrivere su un descrittore di file all'interno di una sostituzione comando, che viene aperto nel comando "esterno" che sta usando la sostituzione. Quindi questo è il modo portatile più semplice per farlo.)

Puoi guardarlo in un modo meno tecnico e più giocoso, come se gli output dei comandi si saltassero l'un l'altro: command1 si dirige verso command2, quindi l'output di printf salta sul comando 2 in modo che command2 non lo catturi, e quindi l'output del comando 2 passa sopra e fuori dalla sostituzione dei comandi proprio come printf atterra appena in tempo per essere catturato dalla sostituzione in modo che finisca nella variabile, e l'output di command2 continui nel suo modo felice di essere scritto nell'output standard, proprio come in un tubo normale.

Inoltre, a quanto ho capito, $?conterrà comunque il codice di ritorno del secondo comando nella pipe, poiché assegnazioni variabili, sostituzioni di comandi e comandi composti sono tutti effettivamente trasparenti al codice di ritorno del comando al loro interno, quindi lo stato di restituzione di command2 dovrebbe essere propagato - questo, e non dovendo definire una funzione aggiuntiva, è il motivo per cui penso che questa potrebbe essere una soluzione un po 'migliore di quella proposta da lesmana.

Secondo le avvertenze lesmana, è possibile che command1 finisca per usare i descrittori di file 3 o 4, quindi per essere più robusto, dovresti:

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

Nota che nel mio esempio uso i comandi composti, ma i subshells (usare al ( )posto di { }funzionerà anche, anche se potrebbe essere meno efficiente).

I comandi ereditano i descrittori di file dal processo che li avvia, quindi l'intera seconda riga erediterà il descrittore di file quattro e il comando composto seguito da 3>&1erediterà il descrittore di file tre. Quindi si 4>&-assicura che il comando composto interno non erediterà il descrittore di file quattro e 3>&-che non erediterà il descrittore di file tre, quindi command1 ottiene un ambiente più "pulito" e più standard. Potresti anche spostare l'interno 4>&-vicino al 3>&-, ma immagino perché non limitarne il più possibile il campo di applicazione.

Non sono sicuro di quanto spesso le cose utilizzino direttamente il descrittore di file tre e quattro - penso che la maggior parte delle volte i programmi utilizzino syscalls che restituiscono descrittori di file non utilizzati al momento, ma a volte il codice scrive direttamente sul descrittore di file 3, I suppongo (potrei immaginare un programma che controlla un descrittore di file per vedere se è aperto e usarlo se lo è, o comportarsi diversamente di conseguenza se non lo è). Quindi quest'ultimo è probabilmente il migliore da tenere a mente e utilizzare per casi di carattere generale.


Bella spiegazione!
selurvedu,

6

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


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

A differenza della risposta di @ cODAR, questo 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 semplicemente chiamare ${PIPESTATUS[0]}. È importante tuttavia che tutto sia messo tra parentesi.


4

Al di fuori di bash, puoi fare:

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

Ciò è utile ad esempio negli script ninja in cui è prevista la shell /bin/sh.


3

PIPESTATUS [@] deve essere copiato in un array immediatamente dopo il ritorno del comando pipe. Qualsiasi lettura di PIPESTATUS [@] cancellerà il contenuto. Copiarlo su un altro array se si prevede di verificare lo stato di tutti i comandi pipe. "$?" ha lo stesso valore dell'ultimo elemento di "$ {PIPESTATUS [@]}" e la lettura sembra distruggere "$ {PIPESTATUS [@]}", ma non ho verificato assolutamente questo.

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

Questo non funzionerà se la pipe si trova in una sotto-shell. Per una soluzione a questo problema,
vedi bash pipestatus nel comando backticked?


3

Il modo più semplice per eseguire questa operazione in modo semplice è utilizzare la sostituzione del processo anziché una pipeline. Esistono diverse differenze, ma probabilmente non contano molto per il tuo caso d'uso:

  • Quando esegue una pipeline, bash attende il completamento di tutti i processi.
  • L'invio di Ctrl-C a bash lo fa uccidere tutti i processi di una pipeline, non solo quello principale.
  • L' pipefailopzione e la PIPESTATUSvariabile sono irrilevanti per elaborare la sostituzione.
  • Forse di più

Con la sostituzione del processo, bash avvia il processo e se ne dimentica, non è nemmeno visibile jobs.

Differenze citate a parte consumer < <(producer)e producer | consumersostanzialmente equivalenti.

Se vuoi capovolgere qual è il processo "principale", devi semplicemente capovolgere i comandi e la direzione della sostituzione 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 all'espressione pipe. Il processo potrebbe non arrestarsi mai, a meno che non sia sensibile alla chiusura del tubo. In particolare, potrebbe continuare a scrivere cose sul tuo stdout, il che potrebbe creare confusione.


1

Soluzione di guscio puro:

% 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 il secondo catsostituito 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 noti che anche il primo gatto fallisce, perché è stdout chiuso. L'ordine dei comandi non riusciti nel registro è corretto in questo esempio, ma non fare affidamento su di esso.

Questo metodo consente di catturare stdout e stderr per i singoli comandi in modo da poterlo scaricare anche in un file di registro in caso di errore, o semplicemente eliminarlo in assenza di errori (come l'output di dd).


1

Basati sulla risposta di @ brian-s-wilson; questa funzione di supporto bash:

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

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

usato così:

1: get_bad_things deve avere esito positivo, ma non deve produrre output; ma vogliamo vedere l'output che produce

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

2: tutta la pipeline deve avere successo

thing | something -q | thingy
pipeinfo || return

1

A volte può essere più semplice e chiaro usare un comando esterno, piuttosto che 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 shpipeline, ma a differenza di shciò, consente di invertire la direzione della pipa, in modo da poter catturare il codice di ritorno del produttore processo (il seguito è tutto sulla shriga di comando, ma con execlineinstallato):

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

L'utilizzo pipelineha le stesse differenze rispetto alle pipeline bash native della sostituzione del processo bash utilizzata nella risposta # 43972501 .

* In realtà pipelinenon esce affatto a meno che non ci sia un errore. Viene eseguito nel secondo comando, quindi è il secondo comando che esegue il ritorno.

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.