Ho due processi foo
e bar
, collegato con una pipe:
$ foo | bar
bar
esce sempre 0; Sono interessato al codice di uscita di foo
. C'è un modo per arrivarci?
Ho due processi foo
e bar
, collegato con una pipe:
$ foo | bar
bar
esce sempre 0; Sono interessato al codice di uscita di foo
. C'è un modo per arrivarci?
Risposte:
Se si sta utilizzando bash
, è possibile utilizzare la PIPESTATUS
variabile array per ottenere lo stato di uscita di ciascun elemento della pipeline.
$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0
Se si utilizza zsh
, viene chiamato array pipestatus
(il caso conta!) E gli indici dell'array iniziano da uno:
$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0
Per combinarli all'interno di una funzione in modo da non perdere i valori:
$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0
Esegui quanto sopra in bash
o zsh
e otterrai gli stessi risultati; solo uno di retval_bash
e retval_zsh
verrà impostato. L'altro sarà vuoto. Ciò consentirebbe di terminare una funzione return $retval_bash $retval_zsh
(notare la mancanza di virgolette!).
pipestatus
in zsh. Sfortunatamente altre shell non hanno questa funzione.
echo "$pipestatus[1]" "$pipestatus[2]"
.
if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
Esistono 3 modi comuni per farlo:
Il primo modo è impostare l' pipefail
opzione ( ksh
, zsh
o bash
). Questo è il più semplice e fondamentalmente imposta lo stato $?
di uscita sul codice di uscita dell'ultimo programma per uscire da zero (o zero se tutti sono usciti con successo).
$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1
Bash ha anche una variabile di matrice chiamata $PIPESTATUS
( $pipestatus
in zsh
) che contiene lo stato di uscita di tutti i programmi nell'ultima pipeline.
$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1
È possibile utilizzare il terzo esempio di comando per ottenere il valore specifico nella pipeline necessario.
Questa è la soluzione più ingombrante. Esegui ciascun comando separatamente e acquisisci lo stato
$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
ksh
, ma da una breve occhiata alla sua manpage, non supporta $PIPESTATUS
o qualcosa di simile. pipefail
Tuttavia supporta l' opzione.
LOG=$(failed_command | successful_command)
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 someprog
e l'output da filter
.
Ecco la mia soluzione:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
il risultato di questo costrutto è stdout da filter
come stdout del costrutto e lo stato di uscita da someprog
come stato di uscita del costrutto.
questo costrutto funziona anche con un semplice raggruppamento di comandi {...}
anziché con subshells (...)
. i subshells hanno alcune implicazioni, tra cui un costo in termini di prestazioni, che qui non è necessario. leggi il manuale di bash per maggiori dettagli: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1
Sfortunatamente la grammatica bash richiede spazi e punti e virgola per le parentesi graffe in modo che il costrutto diventi molto più spazioso.
Per il resto di questo testo userò la variante di subshell.
Esempio someprog
e filter
:
someprog() {
echo "line1"
echo "line2"
echo "line3"
return 42
}
filter() {
while read line; do
echo "filtered $line"
done
}
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
echo $?
Esempio di output:
filtered line1
filtered line2
filtered line3
42
Nota: il processo figlio eredita i descrittori di file aperti dal padre. Ciò significa someprog
che erediterà il descrittore di file aperto 3 e 4. Se someprog
scrive nel descrittore di file 3, questo diventerà lo stato di uscita. Lo stato di uscita reale verrà ignorato perché read
legge solo una volta.
Se temi che someprog
potresti scrivere sul descrittore di file 3 o 4, è meglio chiudere i descrittori di file prima di chiamare someprog
.
(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
Il exec 3>&- 4>&-
prima someprog
chiude il descrittore di file prima di eseguirlo, someprog
quindi per someprog
quei descrittori di file semplicemente non esistono.
Può anche essere scritto in questo modo: someprog 3>&- 4>&-
Spiegazione dettagliata del costrutto:
( ( ( ( someprog; #part6
echo $? >&3 #part5
) | filter >&4 #part4
) 3>&1 #part3
) | (read xs; exit $xs) #part2
) 4>&1 #part1
Dal basso in alto:
#part3
) e a destra ( #part2
). exit $xs
è anche l'ultimo comando della pipe e ciò significa che la stringa di stdin sarà lo stato di uscita dell'intero costrutto.#part2
a sua volta e a sua volta sarà lo stato di uscita dell'intero costrutto.#part5
e #part6
) e a destra ( filter >&4
). L'output di filter
viene reindirizzato al descrittore di file 4. Nel #part1
descrittore di file 4 è stato reindirizzato su stdout. Ciò significa che l'output di filter
è lo stdout dell'intero costrutto.#part6
viene stampato sul descrittore di file 3. Nel #part3
descrittore di file 3 è stato reindirizzato a #part2
. Ciò significa che lo stato di uscita #part6
sarà lo stato di uscita finale per l'intero costrutto.someprog
viene eseguito. Viene inserito lo stato di uscita #part5
. Lo stdout viene preso dal pipe in #part4
e inoltrato a filter
. L'output da filter
a sua volta raggiungerà stdout come spiegato in#part4
(read; exit $REPLY)
(exec 3>&- 4>&-; someprog)
semplifica someprog 3>&- 4>&-
.
{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
Sebbene non sia esattamente quello che hai chiesto, potresti usare
#!/bin/bash -o pipefail
in modo che i tuoi tubi restituiscano l'ultimo ritorno diverso da zero.
potrebbe essere un po 'meno di codifica
Modifica: Esempio
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
set -o pipefail
all'interno dello script dovrebbe essere più robusto, ad esempio nel caso in cui qualcuno esegua lo script tramite bash foo.sh
.
-o pipefail
non è in POSIX.
#!/bin/bash -o pipefail
. L'errore è:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
#!
righe oltre il primo, e quindi questo diventa /bin/bash
-o pipefail
/tmp/ff
, invece del necessario /bin/bash
-o
pipefail
/tmp/ff
- getopt
(o simile) analizzando usando optarg
, che è il prossimo oggetto ARGV
, come argomento a -o
, quindi fallisce. Se dovessi creare un wrapper (diciamo, bash-pf
che ha appena fatto exec /bin/bash -o pipefail "$@"
, e metterlo sulla #!
linea, funzionerebbe. Vedi anche: en.wikipedia.org/wiki/Shebang_%28Unix%29
Quello che faccio quando possibile è foo
inserire il codice di uscita da in bar
. Ad esempio, se so che foo
non produce mai una riga con solo cifre, allora posso solo virare sul codice di uscita:
{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'
O se so che l'output di foo
non contiene mai una riga con solo .
:
{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'
Questo può sempre essere fatto se c'è un modo di bar
lavorare su tutti tranne l'ultima riga e passare l'ultima riga come codice di uscita.
Se si bar
tratta di una pipeline complessa il cui output non è necessario, è possibile ignorare una parte di esso stampando il codice di uscita su un descrittore di file diverso.
exit_codes=$({ { foo; echo foo:"$?" >&3; } |
{ bar >/dev/null; echo bar:"$?" >&3; }
} 3>&1)
Dopo questo $exit_codes
è di solito foo:X bar:Y
, ma potrebbe essere bar:Y foo:X
se si bar
chiude prima di leggere tutti i suoi input o se si è sfortunati. Penso che le scritture su pipe fino a 512 byte siano atomiche su tutti gli unices, quindi le parti foo:$?
e bar:$?
non saranno mescolate finché le stringhe di tag sono inferiori a 507 byte.
Se è necessario acquisire l'output da bar
, diventa difficile. È possibile combinare le tecniche sopra organizzando che l'output di bar
non contenga mai una riga che assomigli a un'indicazione del codice di uscita, ma diventa complicata.
output=$(echo;
{ { foo; echo foo:"$?" >&3; } |
{ bar | sed 's/^/^/'; echo bar:"$?" >&3; }
} 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')
E, naturalmente, c'è la semplice opzione di utilizzare un file temporaneo per memorizzare lo stato. Semplice, ma non così semplice in produzione:
/tmp
è l'unico posto in cui uno script è in grado di scrivere file. Usa mktemp
, che non è POSIX ma al giorno d'oggi è disponibile su tutti i seri unici.foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")
A partire dalla pipeline:
foo | bar | baz
Ecco una soluzione generale che utilizza solo shell POSIX e nessun file temporaneo:
exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
(bar || echo "1:$?" >&3) |
(baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-
$error_statuses
contiene i codici di stato di tutti i processi non riusciti, in ordine casuale, con indici per indicare quale comando ha emesso ogni stato.
# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2
# test if all commands succeeded:
test -z "$error_statuses"
# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null
Nota le citazioni in giro $error_statuses
nei miei test; senza di loro grep
non si può differenziare perché le newline vengono costrette agli spazi.
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 command1 sul suo stdout, ma quello 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é 1, che è quello 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 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>&1
abbiamo 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 "eliminato" dalla sostituzione del comando, si sta effettivamente andando al descrittore di file complessivo 1 dello script.
( exec 4>&1
Deve 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 di 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>&1
erediterà 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.
-bash: 3: Bad file descriptor
.
Se hai installato il pacchetto moreutils puoi usare l' utility di errore di scrittura che fa esattamente quello che hai chiesto.
La soluzione di lesmana sopra può anche essere fatta senza il sovraccarico di avviare sottoprocessi nidificati usando { .. }
invece (ricordando che questa forma di comandi raggruppati deve sempre finire con un punto e virgola). Qualcosa come questo:
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1
Ho verificato questo costrutto con le versioni dash 0.5.5 e bash 3.2.25 e 4.2.42, quindi anche se alcune shell non supportano il { .. }
raggruppamento, è comunque conforme a POSIX.
set -o pipefail
in ksh o qualsiasi numero di wait
comandi spruzzati in entrambi. Penso che possa, almeno in parte, essere un problema di analisi per ksh, come se mi attenessi all'uso di subshells, allora funziona bene, ma anche con un if
per scegliere la variante di subshell per ksh ma lasciare i comandi composti per altri, fallisce .
Questo è portatile, cioè funziona con qualsiasi shell conforme a POSIX, non richiede che la directory corrente sia scrivibile e consenta l'esecuzione simultanea di più script usando lo stesso trucco.
(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))
Modifica: ecco una versione più forte seguendo i commenti di Gilles:
(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))
Edit2: ed ecco una variante leggermente più leggera dopo il commento dubiousjim:
(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
. @Johan: sono d'accordo che è più facile con Bash, ma in alcuni contesti, vale la pena saperlo evitare.
Di seguito è inteso come un componente aggiuntivo alla risposta di @Patrik, nel caso in cui non sia possibile utilizzare una delle soluzioni comuni.
Questa risposta presuppone che:
$PIPESTATUS
néset -o pipefail
Ipotesi aggiuntive. Puoi sbarazzarti di tutto, ma questo ostacola troppo la ricetta, quindi non è coperto qui:
- Tutto quello che vuoi sapere è che tutti i comandi nel PIPE hanno il codice di uscita 0.
- Non sono necessarie ulteriori informazioni sulla banda laterale.
- La shell attende che tutti i comandi pipe vengano restituiti.
Prima: foo | bar | baz
tuttavia restituisce solo il codice di uscita dell'ultimo comando ( baz
)
Ricercato: $?
non deve essere 0
(vero), se uno qualsiasi dei comandi nella pipe non è riuscito
Dopo:
TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"
{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"
# $? now is 0 only if all commands had exit code 0
Ha spiegato:
mktemp
. Questo di solito crea immediatamente un file in/tmp
wait
è necessario per ksh
, perché ksh
altro non aspettare che tutti i comandi di tubi per finire. Tuttavia, tieni presente che ci sono effetti collaterali indesiderati se sono presenti alcune attività in background, quindi l'ho commentato per impostazione predefinita. Se l'attesa non fa male, puoi commentarla.read
ritorna false
, quindi true
indica un erroreQuesto può essere usato come un sostituto del plugin per un singolo comando e deve solo seguire:
/proc/fd/N
bugs:
Questo script ha un bug nel caso in cui lo /tmp
spazio sia esaurito. Se hai bisogno di protezione anche da questo caso artificiale, puoi farlo come segue, tuttavia questo ha lo svantaggio, che il numero di 0
in 000
dipende dal numero di comandi nella pipe, quindi è leggermente più complicato:
TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"
{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"
Note sulla portabilità:
ksh
e shell simili che aspettano solo l'ultimo comando pipe necessitano del wait
decommentato
L'ultimo esempio usa printf "%1s" "$?"
invece che echo -n "$?"
perché è più portatile. Non tutte le piattaforme interpretano -n
correttamente.
printf "$?"
lo farebbe anche, tuttavia printf "%1s"
cattura alcuni casi angolari nel caso in cui si esegua lo script su una piattaforma davvero rotta. (Leggi: se ti capita di programmare paranoia_mode=extreme
.)
FD 8 e FD 9 possono essere più alti su piattaforme che supportano più cifre. AFAIR una shell conforme POSIX deve solo supportare cifre singole.
È stato testato con Debian 8.2 sh
, bash
, ksh
, ash
, sash
e anchecsh
Con un po 'di precauzione, questo dovrebbe funzionare:
foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
Il seguente blocco 'if' verrà eseguito solo se 'command' ha avuto esito positivo:
if command; then
# ...
fi
In particolare, puoi eseguire qualcosa del genere:
haconf_out=/path/to/some/temporary/file
if haconf -makerw > "$haconf_out" 2>&1; then
grep -iq "Cluster already writable" "$haconf_out"
# ...
fi
Che eseguirà haconf -makerw
e memorizzerà il suo stdout e stderr su "$ haconf_out". Se il valore restituito da haconf
è vero, il blocco 'if' verrà eseguito e grep
leggerà "$ haconf_out", cercando di abbinarlo a "Cluster già scrivibile".
Si noti che i tubi si puliscono automaticamente; con il reindirizzamento dovrai fare attenzione a rimuovere "$ haconf_out" al termine.
Non elegante come pipefail
, ma un'alternativa legittima se questa funzionalità non è a portata di mano.
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"
exec 8>&- 9>&-
{
{
{
{ #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8 # use exactly 1x prior to #END
#END
} 2>&1 | ${TEE} 1>&9
} 8>&1
} | exit $(read; printf "${REPLY}")
} 9>&1
exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$
(Almeno con bash) combinato con set -e
uno può usare subshell per emulare esplicitamente pipefail ed uscire in caso di errore pipe
set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program
Quindi, se foo
fallisce per qualche motivo, il resto del programma non verrà eseguito e lo script uscirà con il corrispondente codice di errore. (Ciò presuppone che foo
stampa il proprio errore, che è sufficiente per capire il motivo del fallimento)
EDIT : Questa risposta è sbagliata, ma interessante, quindi la lascerò per riferimento futuro.
!
a al comando inverte il codice di ritorno.
http://tldp.org/LDP/abs/html/exit-status.html
# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command # bash: bogus_command: command not found
echo $? # 127
! ls | bogus_command # bash: bogus_command: command not found
echo $? # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #
ls
, non invertire il codice di uscita dibogus_command