Ottieni lo stato di uscita del processo che viene reindirizzato a un altro


Risposte:


256

Se si sta utilizzando bash, è possibile utilizzare la PIPESTATUSvariabile 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 basho zshe otterrai gli stessi risultati; solo uno di retval_bashe retval_zshverrà impostato. L'altro sarà vuoto. Ciò consentirebbe di terminare una funzione return $retval_bash $retval_zsh(notare la mancanza di virgolette!).


9
E pipestatusin zsh. Sfortunatamente altre shell non hanno questa funzione.
Gilles,

8
Nota: le matrici in zsh iniziano in modo controintuitivo all'indice 1, quindi è echo "$pipestatus[1]" "$pipestatus[2]".
Christoph Wurm,

5
È possibile controllare l'intera conduttura in questo modo:if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
l0b0

7
@JanHudec: forse dovresti leggere le prime cinque parole della mia risposta. Inoltre, ricorda gentilmente dove la domanda ha richiesto una risposta solo POSIX.
Camh,

12
@JanHudec: Né è stato taggato POSIX. Perché pensi che la risposta debba essere POSIX? Non è stato specificato, quindi ho fornito una risposta qualificata. Non c'è nulla di sbagliato nella mia risposta, inoltre ci sono più risposte per affrontare altri casi.
Camh,

238

Esistono 3 modi comuni per farlo:

pipefail

Il primo modo è impostare l' pipefailopzione ( ksh, zsho 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

$ PIPESTATUS

Bash ha anche una variabile di matrice chiamata $PIPESTATUS( $pipestatusin 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.

Esecuzioni separate

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

2
Darn! Stavo solo postando su PIPESTATUS.
slm

1
Per riferimento ci sono molte altre tecniche discusse in questa domanda SO: stackoverflow.com/questions/1221833/…
slm

1
@Patrick la soluzione pipestatus funziona su bash, solo più quastion nel caso in cui uso lo script ksh pensi che possiamo trovare qualcosa di simile a pipestatus? , (nel frattempo vedo il pipestatus non supportato da ksh)
yael

2
@yael non lo uso ksh, ma da una breve occhiata alla sua manpage, non supporta $PIPESTATUSo qualcosa di simile. pipefailTuttavia supporta l' opzione.
Patrick,

1
Ho deciso di andare con pipefail in quanto mi permette di ottenere lo stato del comando fallito qui:LOG=$(failed_command | successful_command)
vmrob

51

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

il risultato di questo costrutto è stdout da filtercome stdout del costrutto e lo stato di uscita da someprogcome 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 someproge 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 someprogche erediterà il descrittore di file aperto 3 e 4. Se someprogscrive nel descrittore di file 3, questo diventerà lo stato di uscita. Lo stato di uscita reale verrà ignorato perché readlegge solo una volta.

Se temi che someprogpotresti 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 someprogchiude il descrittore di file prima di eseguirlo, someprogquindi per someprogquei 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:

  1. Viene creata una subshell con il descrittore di file 4 reindirizzato a stdout. Ciò significa che tutto ciò che viene stampato sul descrittore di file 4 nella subshell finirà come stdout dell'intero costrutto.
  2. Viene creata una pipe e vengono eseguiti i comandi a sinistra ( #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.
  3. Viene creata una subshell con il descrittore di file 3 reindirizzato a stdout. Ciò significa che qualsiasi cosa sia stampata sul descrittore di file 3 in questa sottostruttura finirà #part2a sua volta e a sua volta sarà lo stato di uscita dell'intero costrutto.
  4. Viene creata una pipe e vengono eseguiti i comandi a sinistra ( #part5e #part6) e a destra ( filter >&4). L'output di filterviene reindirizzato al descrittore di file 4. Nel #part1descrittore di file 4 è stato reindirizzato su stdout. Ciò significa che l'output di filterè lo stdout dell'intero costrutto.
  5. Lo stato di uscita da #part6viene stampato sul descrittore di file 3. Nel #part3descrittore di file 3 è stato reindirizzato a #part2. Ciò significa che lo stato di uscita #part6sarà lo stato di uscita finale per l'intero costrutto.
  6. someprogviene eseguito. Viene inserito lo stato di uscita #part5. Lo stdout viene preso dal pipe in #part4e inoltrato a filter. L'output da filtera sua volta raggiungerà stdout come spiegato in#part4

1
Bello. Per la funzione che potrei fare(read; exit $REPLY)
jthill

5
(exec 3>&- 4>&-; someprog)semplifica someprog 3>&- 4>&-.
Roger Pate,

Questo metodo funziona anche senza { { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
sottotitoli

36

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

9
set -o pipefailall'interno dello script dovrebbe essere più robusto, ad esempio nel caso in cui qualcuno esegua lo script tramite bash foo.sh.
Maxschlepzig,

Come funziona? hai un esempio?
Johan

2
Si noti che -o pipefailnon è in POSIX.
Scy

2
Questo non funziona nel mio rilascio BASH 3.2.25 (1). Nella parte superiore di / tmp / ff ho #!/bin/bash -o pipefail. L'errore è:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
Felipe Alvarez,

2
@FelipeAlvarez: Alcuni ambienti (incluso Linux) non analizzano gli spazi su #!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-pfche ha appena fatto exec /bin/bash -o pipefail "$@", e metterlo sulla #!linea, funzionerebbe. Vedi anche: en.wikipedia.org/wiki/Shebang_%28Unix%29
lindes

22

Quello che faccio quando possibile è fooinserire il codice di uscita da in bar. Ad esempio, se so che foonon 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 foonon contiene mai una riga con solo .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

Questo può sempre essere fatto se c'è un modo di barlavorare su tutti tranne l'ultima riga e passare l'ultima riga come codice di uscita.

Se si bartratta 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:Xse si barchiude 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 barnon 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:

  • Se sono in esecuzione più script contemporaneamente o se lo stesso script utilizza questo metodo in più punti, è necessario assicurarsi che utilizzino nomi di file temporanei diversi.
  • La creazione di un file temporaneo in modo sicuro in una directory condivisa è difficile. Spesso /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")

1
Quando utilizzo l'approccio file temporaneo, preferisco aggiungere una trap per EXIT che rimuove tutti i file temporanei in modo che non
rimanga

17

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_statusesnei miei test; senza di loro grepnon si può differenziare perché le newline vengono costrette agli spazi.


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 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>&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 "eliminato" dalla sostituzione del comando, si sta effettivamente andando al descrittore di file complessivo 1 dello script.

( 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 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>&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.


Sembra interessante, ma non riesco proprio a capire cosa ti aspetti da questo comando, e nemmeno il mio computer; Ho capito -bash: 3: Bad file descriptor.
G-Man,

@ G-Man Giusto, continuo a dimenticare che bash non ha idea di cosa stia facendo quando si tratta di descrittori di file, a differenza delle shell che di solito uso (il ash che viene fornito con busybox). Ti farò sapere quando penso a una soluzione alternativa che rende felice Bash. Nel frattempo, se hai una debian box a portata di mano, puoi provarla con un trattino, oppure se hai una busybox a portata di mano, puoi provarla con la busybox ash / sh.
mtraceur,

@ G-Man Quanto a cosa mi aspetto che faccia il comando, e cosa fa in altre shell, è reindirizzare stdout da command1 in modo che non venga catturato dalla sostituzione del comando, ma una volta fuori dalla sostituzione del comando, cade fd3 torna a stdout, quindi viene reindirizzato come previsto a command2. Quando il comando1 termina, printf si attiva e stampa il suo stato di uscita, che viene acquisito nella variabile dalla sostituzione del comando. Ripartizione molto dettagliata qui: stackoverflow.com/questions/985876/tee-and-exit-status/… Inoltre, quel tuo commento viene letto come se dovesse essere un po 'offensivo?
mtraceur,

Da dove comincio? (1) Mi dispiace se ti sei sentito insultato. "Sembra interessante" intendeva seriamente; sarebbe bello se qualcosa di compatto come quello funzionasse come previsto. Oltre a ciò, stavo semplicemente dicendo che non capivo cosa avrebbe dovuto fare la tua soluzione. Lavoro / gioco con Unix da molto tempo (da prima che esistesse Linux) e, se non capisco qualcosa, è una bandiera rossa che, forse, anche altre persone non lo capiranno, e che necessita di maggiori spiegazioni (IMNSHO). ... (proseguendo)
G-Man,

(Proseguendo) ... Dal momento che "... ti piace pensare ... che [tu] capisci quasi tutto più della persona media", forse dovresti ricordare che l'obiettivo di Stack Exchange non è di essere un servizio di scrittura di comandi, sfornando migliaia di soluzioni uniche a domande banalmente distinte; ma piuttosto per insegnare alla gente come risolvere i propri problemi. E, a tal fine, forse dovresti spiegare abbastanza bene le cose che una "persona media" può capirlo. Guarda la risposta di lesmana per un esempio di una spiegazione eccellente. ... (proseguendo)
G-Man,

11

Se hai installato il pacchetto moreutils puoi usare l' utility di errore di scrittura che fa esattamente quello che hai chiesto.


7

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.


Funziona davvero bene con la maggior parte delle shell con cui l'ho provato, incluso NetBSD sh, pdksh, mksh, dash, bash. Tuttavia non riesco a farlo funzionare con AT&T Ksh (93s +, 93u +) o zsh (4.3.9, 5.2), anche con set -o pipefailin ksh o qualsiasi numero di waitcomandi 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 ifper scegliere la variante di subshell per ksh ma lasciare i comandi composti per altri, fallisce .
Greg A. Woods,

4

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))

3
Questo non funziona per diversi motivi. 1. Il file temporaneo può essere letto prima di essere scritto. 2. La creazione di un file temporaneo in una directory condivisa con un nome prevedibile non è sicura (DoS banale, corsa simbolica). 3. Se lo stesso script utilizza questo trucco più volte, utilizzerà sempre lo stesso nome file. Per risolvere 1, leggere il file dopo il completamento della pipeline. Per risolvere 2 e 3, utilizzare un file temporaneo con un nome generato casualmente o in una directory privata.
Gilles,

+1 Beh, $ {PIPESTATUS [0]} è più semplice ma l'idea di base qui funziona se si conoscono i problemi menzionati da Gilles.
Johan

È possibile salvare un paio di sotto-shell: (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.
dubiousjim,

4

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:

  • Hai un guscio che non conosce $PIPESTATUSset -o pipefail
  • Si desidera utilizzare una pipe per l'esecuzione parallela, quindi nessun file temporaneo.
  • Non si desidera avere ulteriore confusione in giro se si interrompe la sceneggiatura, probabilmente per un'interruzione improvvisa di corrente.
  • Questa soluzione dovrebbe essere relativamente facile da seguire e pulita da leggere.
  • Non si desidera introdurre sottotitoli aggiuntivi.
  • Non è possibile giocherellare con i descrittori di file esistenti, quindi non è necessario toccare stdin / out / err (tuttavia è possibile introdurne uno nuovo temporaneamente)

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 | baztuttavia 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:

  • Viene creato un tempfile con mktemp. Questo di solito crea immediatamente un file in/tmp
  • Questo tempfile viene quindi reindirizzato a FD 9 per la scrittura e FD 8 per la lettura
  • Quindi il file temporaneo viene immediatamente eliminato. Rimane aperto, però, fino a quando entrambi gli FD non verranno meno.
  • Ora il tubo è avviato. Ogni passaggio si aggiunge solo a FD 9, se si è verificato un errore.
  • Il waitè necessario per ksh, perché kshaltro 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.
  • Successivamente viene letto il contenuto del file. Se è vuoto (perché tutto ha funzionato) readritorna false, quindi trueindica un errore

Questo può essere usato come un sostituto del plugin per un singolo comando e deve solo seguire:

  • FD 9 e 8 non utilizzati
  • Una singola variabile di ambiente per contenere il nome del file temporaneo
  • E questa ricetta può essere adattata praticamente a qualsiasi shell là fuori che consente il reindirizzamento IO
  • Inoltre è abbastanza indipendente dalla piattaforma e non ha bisogno di cose del genere /proc/fd/N

bugs:

Questo script ha un bug nel caso in cui lo /tmpspazio 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 0in 000dipende 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à:

  • kshe shell simili che aspettano solo l'ultimo comando pipe necessitano del waitdecommentato

  • L'ultimo esempio usa printf "%1s" "$?"invece che echo -n "$?"perché è più portatile. Non tutte le piattaforme interpretano -ncorrettamente.

  • 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, sashe anchecsh


3

Con un po 'di precauzione, questo dovrebbe funzionare:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)

Che ne dici di pulire come jlliagre? Non lasci un file dietro chiamato foo-status?
Johan

@Johan: Se preferisci il mio suggerimento, non esitare a votarlo;) Oltre a non lasciare un file, ha il vantaggio di consentire a più processi di eseguirlo contemporaneamente e la directory corrente non deve essere scrivibile.
jlliagre,

2

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 -makerwe memorizzerà il suo stdout e stderr su "$ haconf_out". Se il valore restituito da haconfè vero, il blocco 'if' verrà eseguito e grepleggerà "$ 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.


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

0

(Almeno con bash) combinato con set -euno 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 foofallisce per qualche motivo, il resto del programma non verrà eseguito e lo script uscirà con il corrispondente codice di errore. (Ciò presuppone che foostampa il proprio errore, che è sufficiente per capire il motivo del fallimento)


-1

EDIT : Questa risposta è sbagliata, ma interessante, quindi la lascerò per riferimento futuro.


L'aggiunta di !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.
# =========================================================== #

1
Penso che questo non sia correlato. Nel tuo esempio, voglio conoscere il codice di uscita di ls, non invertire il codice di uscita dibogus_command
Michael Mrozek

2
Suggerisco di ritirare quella risposta.
maxschlepzig,

3
Bene, sembra che io sia un idiota. In realtà l'ho usato in una sceneggiatura prima di pensare che facesse quello che voleva l'OP.
Meno male
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.