Quitter un script shell avec des boucles imbriquées

11

J'ai un script shell avec des boucles imbriquées et je viens de découvrir que "exit" ne quitte pas vraiment le script, mais seulement la boucle actuelle. Existe-t-il un autre moyen de quitter complètement le script dans une certaine condition d'erreur?

Je ne veux pas utiliser "set -e", car il y a des erreurs acceptables et cela nécessiterait trop de réécriture.

En ce moment, j'utilise kill pour tuer manuellement le processus, mais il semble qu'il devrait y avoir une meilleure façon de le faire.

user923487
la source
1
Que voulez-vous dire par "quitter" ne quitte pas vraiment le script? Oui, essayez bash -c 'for x in y z; do exit; done; echo "This never gets printed"'.
Chris Down
Vous avez raison, il devrait normalement sortir des boucles imbriquées, mais lorsque j'utilise exit, mon script continue avec la boucle externe. Je ne peux pas poster le script.
user923487
2
Pourquoi ne pouvez-vous pas écrire un script qui montre le problème et le publier ici? Cela me semble peu probable.
Toby Speight
1
Est-il vrai que la boucle interne se déroule dans un sous-shell de votre code?
Toby Speight
@Toby La plupart du script est dans un sous-shell à des fins de journalisation, mais les deux boucles et le reste du code sont dans le même sous-shell.
user923487

Réponses:

19

Votre problème n'est pas les boucles imbriquées en soi. C'est qu'une ou plusieurs de vos boucles internes s'exécutent dans un sous-shell .

Cela marche:

#!/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."

production:

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

Cela présente le problème que vous avez décrit:

#!/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."

production:

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

Voici la solution; vous devez tester la valeur de retour des boucles internes qui s'exécutent en sous-shell:

#!/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."

Notez le test: [[ $? != 0 ]] && exit $?

production:

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!

Modifier: pour vérifier dans quel sous-shell vous vous trouvez, modifiez le script «réponse» pour vous indiquer l'ID de processus de votre shell actuel. REMARQUE: cela ne fonctionne que dans 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."

production:

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

Les variables "i" et "j" vous ont été gracieusement fournies par Fortran. Bonne journée. :-)

Mike S
la source
Les boucles intérieure et extérieure s'exécutent dans le même sous-shell, donc la sortie de la boucle intérieure devrait quitter les deux? J'ai du mal à reproduire le problème moi-même. Dès que j'ai supprimé la majeure partie de la logique du programme, le problème a disparu. Quoi qu'il en soit, je vais marquer cela comme une réponse pour l'instant, car il est probable qu'il devra faire quelque chose avec.
user923487
Après avoir lu le lien que vous avez fourni, je pense avoir trouvé le problème. Dans la boucle externe, je fais "cat file | while read line". La tuyauterie crée un sous-shell. Je ne le savais pas.
user923487
@ user923487 - Voir ma réponse mise à jour. Si vous avez bash 4, vous pouvez faire écho (ou printf, si vous êtes moderne) au pid du sous-shell et vérifier si vous êtes dans un sous-shell ou non. Pour voir votre version bash, tapez bash --versionsur la ligne de commande.
Mike S
Très utile. Merci! Bien que je doive dire "killall scriptname.sh" semble être le moyen le plus simple de résoudre ce problème.
user923487
2

Une réponse antérieure suggère d'utiliser [[ $? != 0 ]] && exit $?mais cela ne fonctionnera pas tout à fait comme prévu, car le [[ $? != 0 ]]test sera réinitialisé $?à zéro, ce qui signifie que bien que le script se termine prématurément comme prévu, il se terminera toujours avec le code 0 (comme cela n'était pas prévu) . En outre, il serait préférable d'utiliser le -netest de comparaison numérique plutôt que le !=test de comparaison de chaînes. Donc, à mon humble avis, une meilleure solution consiste à utiliser:

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

car cela garantira que le code de sortie réel est correctement défini.

Lurchman
la source
Bonne rétroaction. J'ai corrigé le code.
Mike S
1

Vous pouvez utiliser break.

De help break:

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

Donc, pour sortir de trois boucles englobantes, c'est-à-dire si vous avez deux boucles imbriquées à l'intérieur de la boucle principale, utilisez-la pour quitter chacune d'elles:

break 3
heemayl
la source
Il y a plus de code après les boucles, donc en sortir seul n'est pas suffisant.
user923487
1
cool thx! exemplefor((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 ne termine pas le shell entier, ou le sous-shell actuel:

$ 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
Toby Speight
la source