Salir de un script de shell con bucles anidados

11

Tengo un script de shell con bucles anidados y acabo de descubrir que "exit" realmente no sale del script, sino solo del bucle actual. ¿Hay otra forma de salir completamente del script en una determinada condición de error?

No quiero usar "set -e", porque hay errores aceptables y requeriría demasiada reescritura.

En este momento, estoy usando kill para eliminar manualmente el proceso, pero parece que debería haber una mejor manera de hacerlo.

usuario923487
fuente
1
¿Qué quiere decir que "salir" realmente no sale del guión? Lo hace, solo inténtalo bash -c 'for x in y z; do exit; done; echo "This never gets printed"'.
Chris Down
Tienes razón, normalmente debería salir de los bucles anidados, pero cuando uso exit mi script continúa con el bucle externo. No puedo publicar el guión.
user923487
2
¿Por qué no puedes escribir un script que muestre el problema y publicarlo aquí? Eso me parece poco probable.
Toby Speight
1
¿Es el caso que el bucle interno se lleva a cabo en un sub-shell en su código?
Toby Speight
@Toby La mayor parte de la secuencia de comandos está en un subconjunto para fines de registro, pero ambos bucles y el resto del código están en el mismo subconjunto.
user923487

Respuestas:

19

Su problema no son los bucles anidados, per se. Es que uno o más de tus bucles internos se ejecutan en una subshell .

Esto funciona:

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

salida:

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

Esto presenta el problema que ha descrito:

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

salida:

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

Aquí está la solución; debe probar el valor de retorno de los bucles internos que se ejecutan en subcapas:

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

Tenga en cuenta la prueba: [[ $? != 0 ]] && exit $?

salida:

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!

Editar: para verificar en qué subshell se encuentra, modifique el script de "respuesta" para indicarle cuál es el ID de proceso de su shell actual. NOTA: Esto solo funciona en 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."

salida:

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

Las variables "i" y "j" son cortesía de Fortran. Que tengas un buen día. :-)

Mike S
fuente
Tanto el bucle interno como el externo se ejecutan en el mismo subconjunto, por lo que al salir del interno debería salir de ambos. Aunque sí tengo problemas para reproducir el problema. Tan pronto como elimino la mayor parte de la lógica del programa, el problema desaparece. De todos modos, marcaré esto como respuesta por ahora, porque es probable que tenga que hacer algo con eso.
user923487
Después de leer el enlace que proporcionó, creo que encontré el problema. En el bucle externo, estoy haciendo "cat file | while read line". La tubería crea una subcapa. No lo sabia.
user923487
@ user923487 - Ver mi respuesta actualizada. Si tiene bash 4, puede repetir (o printf, si es moderno) el pid de subshell y verificar si está en un subshell o no. Para ver su versión bash, escriba bash --versionen la línea de comando.
Mike S
Muy útil. ¡Gracias! Aunque tengo que decir "killall scriptname.sh" parece ser la forma más directa de resolver esto.
user923487
2

Una respuesta anterior sugiere que se utilice [[ $? != 0 ]] && exit $?sin embargo esto no será bastante trabajo como se esperaba, debido a que la [[ $? != 0 ]]prueba se restablecerá $?de nuevo a cero, lo que significa que aunque el guión te con salida precoz como se esperaba, que va a salir siempre con el código 0 (no esperado) . Además, sería mejor usar la -neprueba de comparación numérica, en lugar de la !=prueba de comparación de cadenas. Por lo tanto, en mi humilde opinión, una mejor solución es utilizar:

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

ya que eso asegurará que el código de salida real esté configurado correctamente.

Lurchman
fuente
Buena retroalimentación He corregido el código.
Mike S
1

Puedes usar break.

De help break:

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

Entonces, para salir de tres bucles envolventes, es decir, si tiene dos bucles anidados dentro del principal, use esto para salir de todos ellos:

break 3
heemayl
fuente
Hay más código después de los bucles, por lo que salir de esos solo no es suficiente.
user923487
1
genial gracias! ejemplofor((i=0;i<3;i++));do echo A;for((j=0;j<2;j++));do echo B;break 2;done;done
Acuario Power
0

exit termina todo el shell, o el sub-shell actual:

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