Saindo de um script de shell com loops aninhados

11

Eu tenho um script de shell com loops aninhados e descobri que "exit" realmente não sai do script, mas apenas o loop atual. Existe outra maneira de sair completamente do script em uma determinada condição de erro?

Eu não quero usar "set -e", porque há erros aceitáveis ​​e isso exigiria muita reescrita.

No momento, estou usando o kill para matar manualmente o processo, mas parece que deve haver uma maneira melhor de fazer isso.

user923487
fonte
1
O que você quer dizer com "sair" realmente não sai do script? Sim, tente bash -c 'for x in y z; do exit; done; echo "This never gets printed"'.
Chris Baixo
Você está certo, normalmente deve sair de loops aninhados, mas quando eu uso exit, meu script continua com o loop externo. Não consigo postar o script.
user923487
2
Por que você não pode escrever um script que mostre o problema e publicá-lo aqui? Isso parece improvável para mim.
Toby Speight
1
É o caso do loop interno ocorrer em uma subcasca no seu código?
Toby Speight
@ Toby A maior parte do script está em um sub shell para fins de registro, mas os loops e o restante do código estão no mesmo sub shell.
user923487

Respostas:

19

Seu problema não é loops aninhados, por si só. É que um ou mais dos seus loops internos estão sendo executados em um subshell .

Isso 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."

resultado:

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

Isso apresenta o problema que você descreveu:

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

resultado:

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

Aqui está a solução; você precisa testar o valor de retorno dos loops internos que são executados em 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."

Observe o teste: [[ $? != 0 ]] && exit $?

resultado:

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 em que subshell você está, modifique o script "answer" para informar qual é o ID do processo do seu shell atual. NOTA: Isso funciona apenas no 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."

resultado:

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

As variáveis ​​"i" e "j" trazidas a você são cortesia da Fortran. Tenha um bom dia. :-)

Mike S
fonte
Tanto o loop interno quanto o externo estão rodando no mesmo sub shell, portanto, sair do interno deve sair dos dois? No entanto, tenho problemas para reproduzir o problema. Assim que removo a maior parte da lógica do programa, o problema se foi. De qualquer forma, vou marcar isso como resposta por enquanto, porque é provável que tenha que fazer algo com isso.
User923487
Depois de ler o link que você forneceu, acho que encontrei o problema. No loop externo, estou fazendo "arquivo de gato | enquanto lê a linha". Tubulação cria um sub shell. Eu não sabia disso.
User923487
@ user923487 - Veja minha resposta atualizada. Se você possui o bash 4, é possível ecoar (ou imprimir, se você for moderno) o sub-shell pid e verificar se você está em um sub-shell ou não. Para ver sua versão do bash, digite bash --versionna linha de comando.
Mike S
Muito útil. Obrigado! Embora eu tenha que dizer "killall scriptname.sh" parece ser a maneira mais direta de resolver isso.
User923487
2

Uma resposta anterior sugere o uso [[ $? != 0 ]] && exit $?no entanto isso não vai muito trabalho como esperado, porque o [[ $? != 0 ]]teste irá repor $?a zero, o que significa que, embora o script early-saída como esperado, vai sempre sair com o código 0 (como não se espera) . Além disso, seria melhor usar o -neteste de comparação numérica, em vez do !=teste de comparação de cadeias. Portanto, IMHO, uma solução melhor é usar:

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

pois isso garantirá que o código de saída real esteja definido corretamente.

Lurchman
fonte
Boa resposta. Eu corrigi o código.
Mike S
1

Você pode usar break.

De help break:

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

Portanto, para sair de três loops anexos, ou seja, se você tiver dois loops aninhados dentro do principal, use isso para sair de todos eles:

break 3
heemail
fonte
Há mais código após os loops, portanto, romper com eles sozinho não é suficiente.
user923487
1
legal thx! exemplofor((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 finaliza o shell inteiro ou o sub shell atual:

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