Uscita da uno script di shell con loop nidificati


11

Ho uno script di shell con loop nidificati e ho appena scoperto che "exit" in realtà non esce dallo script, ma solo il loop corrente. Esiste un altro modo per uscire completamente dallo script a una determinata condizione di errore?

Non voglio usare "set -e", perché ci sono errori accettabili e richiederebbe una riscrittura eccessiva.

In questo momento, sto usando kill per terminare manualmente il processo, ma sembra che ci dovrebbe essere un modo migliore per farlo.


1
Cosa vuoi dire che "exit" non esce davvero dallo script? Lo fa, basta provare bash -c 'for x in y z; do exit; done; echo "This never gets printed"'.
Chris Down,

Hai ragione, normalmente dovrebbe uscire dai loop nidificati, ma quando uso exit il mio script continua con il ciclo esterno. Non riesco a pubblicare la sceneggiatura.
user923487

2
Perché non riesci a scrivere uno script che mostri il problema e pubblicarlo qui? Mi sembra improbabile.
Toby Speight,

1
È il caso che il ciclo interno abbia luogo in una sotto-shell nel tuo codice?
Toby Speight,

@Toby La maggior parte dello script si trova in una sub shell a scopo di registrazione, ma entrambi i loop e il resto del codice si trovano nella stessa sub shell.
user923487

Risposte:


19

Il tuo problema non sono i loop nidificati, di per sé. È che uno o più dei tuoi loop interni è in esecuzione in una subshell .

Questo funziona:

#!/bin/bash

for i in $(seq 1 100); do
        echo i $i
        for j in $(seq 1 10) ; do
                echo j $j
                sleep 1
                [[ $j = 3 ]] && { echo "I've had enough!" 1>&2; exit 1; }
        done
        echo "After the j loop."
done
echo "After all the loops."

produzione:

i 1
j 1
j 2
j 3
I've had enough!

Questo presenta il problema che hai descritto:

#!/bin/bash

for i in $(seq 1 100); do
        echo i $i
        cat /etc/passwd | while read line; do
                echo LINE $line
                sleep 1
                [[ "$line" = "daemon:x:2:2:daemon:/sbin:/sbin/nologin" ]] && { echo "I've had enough!" 1>&2; exit 1; }
        done
        echo "After the j loop."
done    
echo "After all the loops."

produzione:

i 1
LINE root:x:0:0:root:/root:/bin/bash
LINE bin:x:1:1:bin:/bin:/sbin/nologin
LINE daemon:x:2:2:daemon:/sbin:/sbin/nologin
I've had enough!
After the j loop.
i 2
LINE root:x:0:0:root:/root:/bin/bash
LINE bin:x:1:1:bin:/bin:/sbin/nologin
LINE daemon:x:2:2:daemon:/sbin:/sbin/nologin
I've had enough!
After the j loop.
i 3
LINE root:x:0:0:root:/root:/bin/bash
(...etc...)

Ecco la soluzione; devi testare il valore di ritorno dei loop interni eseguiti in subshells:

#!/bin/bash

for i in $(seq 1 100); do
        echo i $i
        cat /etc/passwd | while read line; do
                echo LINE $line
                sleep 1
                [[ "$line" = "daemon:x:2:2:daemon:/sbin:/sbin/nologin" ]] && { echo "I've had enough!" 1>&2; exit 1; }
        done
        err=$?; [[ $err != 0 ]] && exit $err
        echo "After the j loop."
done
echo "After all the loops."

Nota il test: [[ $? != 0 ]] && exit $?

produzione:

i 1
LINE root:x:0:0:root:/root:/bin/bash
LINE bin:x:1:1:bin:/bin:/sbin/nologin
LINE daemon:x:2:2:daemon:/sbin:/sbin/nologin
I've had enough!

Modifica: per verificare in quale subshell ci si trova, modificare lo script "answer" per dirti qual è l'ID del processo della shell corrente. NOTA: funziona solo con bash 4:

#!/bin/bash

for i in $(seq 1 100); do
        echo pid $BASHPID i $i
        cat /etc/passwd | while read line; do
                echo pid $BASHPID LINE $line
                sleep 1
                [[ "$line" = "daemon:x:2:2:daemon:/sbin:/sbin/nologin" ]] && { echo "I've had enough!" 1>&2; exit 1; }
        done
        err=$?; [[ $err != 0 ]] && echo pid $BASHPID && exit $err
        echo "After the j loop."
done
echo "After all the loops."

produzione:

pid 31793 i 1
pid 31796 LINE root:x:0:0:root:/root:/bin/bash
pid 31796 LINE bin:x:1:1:bin:/bin:/sbin/nologin
pid 31796 LINE daemon:x:2:2:daemon:/sbin:/sbin/nologin
I've had enough!
pid 31793

Le variabili "i" e "j" fornite per gentile concessione di Fortran. Buona giornata. :-)


Sia il ciclo interno che quello esterno sono in esecuzione nella stessa sotto shell, quindi uscire da quello interno dovrebbe uscire da entrambi? Tuttavia, ho difficoltà a riprodurre il problema da solo. Appena rimuovo la maggior parte della logica del programma, il problema scompare. Ad ogni modo, lo segnerò come risposta per ora, perché è probabile che debba farci qualcosa.
user923487

Dopo aver letto il link che hai fornito, penso di aver trovato il problema. Nel ciclo esterno sto facendo "file cat | while read line". Piping crea una sotto shell. Non lo sapevo.
user923487

@ user923487 - Vedi la mia risposta aggiornata. Se hai bash 4, puoi fare eco (o printf, se sei moderno) al pid della subshell e verificare se sei in una subshell o meno. Per vedere la tua versione bash, digita bash --versiondalla riga di comando.
Mike S,

Molto utile. Grazie! Anche se devo dire "killall scriptname.sh" sembra essere il modo più semplice per risolverlo.
user923487

2

Una risposta in precedenza suggerisce di utilizzare [[ $? != 0 ]] && exit $?comunque questo non lo farà abbastanza lavoro come previsto, perché la [[ $? != 0 ]]prova si resetta $?a zero, il che significa che, anche se lo script verrà precoce uscita come previsto, sarà sempre uscita con il codice 0 (non previsto) . Inoltre, sarebbe meglio usare il -netest di confronto numerico, piuttosto che il !=test di confronto delle stringhe. Quindi IMHO una soluzione migliore è usare:

err=$?; [[ $err -ne 0 ]] && exit $err

poiché ciò assicurerà che il codice di uscita effettivo sia impostato correttamente.


Buon riscontro Ho corretto il codice.
Mike S

1

È possibile utilizzare break.

Da help break:

Exit a FOR, WHILE or UNTIL loop.  If N is specified, break N enclosing loops.

Quindi, per uscire da tre loop racchiusi, ad esempio se hai due loop nidificati all'interno di uno principale, usa questo per uscire da tutti:

break 3

C'è più codice dopo i loop, quindi uscire da quelli da soli non è abbastanza.
user923487

1
fantastico grazie! esempiofor((i=0;i<3;i++));do echo A;for((j=0;j<2;j++));do echo B;break 2;done;done
Aquarius Power

0

exit termina l'intera shell, o la sub-shell corrente:

$ bash -c 'for i in 1 2 3; do for j in 4 5 6; do echo $i; exit 1; echo $j; done; done'
1
$ echo $?
1
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.