it-swarm.it

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

Ho due processi foo e bar, collegati con una pipe:

$ foo | bar

bar esce sempre 0; Sono interessato al codice di uscita di foo. C'è un modo per arrivarci?

314
Michael Mrozek

Se si utilizza bash, è possibile utilizzare la variabile di array PIPESTATUS per ottenere lo stato di uscita di ciascun elemento della pipeline.

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

Se stai usando zsh, la loro matrice si chiama pipestatus (il caso conta!) E gli indici di matrice 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 a una funzione di terminare con return $retval_bash $retval_zsh (nota la mancanza di virgolette!).

277
camh

Ci sono 3 modi comuni per farlo:

Pipefail

Il primo modo è impostare l'opzione pipefail (ksh, zsh o bash). Questo è il più semplice e ciò che fa è fondamentalmente impostare lo stato di uscita $? al codice di uscita dell'ultimo programma per uscire da zero (o zero se tutti sono usciti correttamente).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

Bash ha anche una variabile array 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.

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
253
Patrick

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 da 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 {...} invece di 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 che someprog 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 possa scrivere nel 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>&- before someprog chiude il descrittore di file prima di eseguire 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:

  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 i comandi a sinistra (#part3) e destra (#part2) vengono eseguiti. 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 tutto ciò che viene stampato sul descrittore di file 3 in questa sottostruttura finirà in #part2 ea sua volta sarà lo stato di uscita dell'intero costrutto.
  4. Viene creata una pipe e i comandi a sinistra (#part5 e #part6) e destra (filter >&4) vengono eseguiti. L'output di filter viene reindirizzato al descrittore di file 4. In #part1 il descrittore di file 4 è stato reindirizzato a stdout. Ciò significa che l'output di filter è lo stdout dell'intero costrutto.
  5. Stato di uscita da #part6 viene stampato sul descrittore di file 3. In #part3 il descrittore di file 3 è stato reindirizzato a #part2. Ciò significa che lo stato di uscita da #part6 sarà lo stato di uscita finale per l'intero costrutto.
  6. someprog viene eseguito. Lo stato di uscita è preso in #part5. Lo stdout è preso dalla pipe in #part4 e inoltrato a filter. L'output di filter a sua volta raggiungerà stdout come spiegato in #part4
58
lesmana

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

[[email protected] ~]# false | true
[[email protected] ~]# echo $?
0
[[email protected] ~]# set -o pipefail
[[email protected] ~]# false | true
[[email protected] ~]# echo $?
1
37
Chris

Quello che faccio quando possibile è alimentare il codice di uscita da foo a 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 esiste un modo per far funzionare bar su tutti tranne l'ultima riga e passare l'ultima riga come codice di uscita.

Se bar è una pipeline complessa il cui output non è necessario, è possibile ignorare 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)

Dopodichè $exit_codes di solito è foo:X bar:Y, ma potrebbe essere bar:Y foo:X se bar si chiude prima di leggere tutto il suo input o se sei sfortunato. Penso che le scritture su pipe fino a 512 byte siano atomiche su tutti i unices, quindi il foo:$? e bar:$? le parti non verranno mescolate finché le stringhe di tag sono inferiori a 507 byte.

Se devi catturare 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 sando un file temporaneo per memorizzare lo stato. Semplice, ma non che semplice nella 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 sistemi seri.
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 POSIX Shell 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 virgolette intorno a $error_statuses nei miei test; senza di loro grep non può differenziarsi perché le newline vengono costrette agli spazi.

17
Jander

Quindi volevo contribuire con una risposta come quella di Lesmana, ma penso che la mia sia forse una soluzione Bourne-Shell pura 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é 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 po 'di magia è che il primo exec 4>&1 abbiamo fatto come 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 dello script 1.

(Il 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 di comando, che viene aperta nel comando "esterno" che utilizza 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 di variabili, sostituzioni di comandi e comandi composti sono tutti effettivamente trasparenti al codice di ritorno del comando al loro interno, quindi lo stato di ritorno di comando2 dovrebbe essere propagato - questo, e non dovendo definire una funzione aggiuntiva, è il motivo per cui penso che questa potrebbe essere una soluzione leggermente 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 utilizzo comandi composti, ma subshells (usando ( ) invece 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. Così la 4>&- si assicura che il comando composto interno non erediterà il descrittore di file quattro e il 3>&- non erediterà il descrittore di file tre, quindi command1 ottiene un ambiente più "pulito" e più standard. Puoi anche spostare l'interno 4>&- accanto 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.

12
mtraceur

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

11
Emanuele Aina

la soluzione di lesmana sopra può essere fatta anche 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 { .. } raggruppamento, è ancora conforme a POSIX.

7
pkeller

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 una Shell 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.
  • Shell attende il ritorno di tutti i comandi pipe.

Prima: foo | bar | baz, tuttavia restituisce solo il codice di uscita dell'ultimo comando (baz)

Ricercato: $? non deve essere 0 (true), 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:

  • Un file temporaneo viene creato 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.
  • wait è necessario per ksh, poiché ksh else non attende il completamento di tutti i comandi pipe. 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) read restituisce false, quindi true indica 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 d'ambiente per contenere il nome del tempfile
  • 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 come /proc/fd/N

Bugs:

Questo script ha un bug nel caso /tmp esaurisce lo spazio. 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 non commentato

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

  • printf "$?" lo farebbe anche, comunque printf "%1s" rileva alcuni casi angolari nel caso in cui esegui lo script su una piattaforma davvero rotta. (Leggi: se ti capita di programmare in paranoia_mode=extreme.)

  • FD 8 e FD 9 possono essere più alti su piattaforme che supportano più cifre. AFAIR una shell conforme a POSIX deve supportare solo cifre singole.

  • È stato testato con Debian 8.2 sh, bash, ksh, ash, sash e persino csh

5
Tino

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

Con un po 'di precauzione, questo dovrebbe funzionare:

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

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 memorizza stdout e stderr in "$ haconf_out". Se il valore restituito da haconf è vero, allora 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.

2
Rany Albeg Wein
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
$
1
C.G.

(Almeno con bash) combinato con set -e si 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 stampi il proprio errore, che è sufficiente per capire il motivo del fallimento)

0
noonex