11

I have a shell script with nested loops and just found out that "exit" doesn't really exit the script, but only the current loop. Is there another way to completely exit the script on a certain error condition?

I don't want to use "set -e", because there are acceptable errors and it would require too much rewriting.

Right now, I am using kill to manually kill the process, but it seems there should be a better way to do this.

  • 1
    What do you mean that "exit" doesn't really exit the script? It does, just try bash -c 'for x in y z; do exit; done; echo "This never gets printed"'. – Chris Down Jun 30 '15 at 15:52
  • You're right, it normally should exit out of nested loops, but when I use exit my script continues with the outer loop. I can't post the script. – user923487 Jun 30 '15 at 15:59
  • 2
    Why can't you write a script that shows the problem and post it here? That sounds unlikely to me. – Toby Speight Jun 30 '15 at 16:04
  • 1
    Is it the case that the inner loop takes place in a sub-shell in your code? – Toby Speight Jun 30 '15 at 16:04
  • @Toby Most of the script is in a sub shell for logging purposes, but both loops and the rest of the code are in the same sub shell. – user923487 Jun 30 '15 at 16:04
19

Your problem is not nested loops, per se. It's that one or more of your inner loops is running in a subshell.

This works:

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

output:

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

This presents the problem you have described:

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

output:

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

Here is the solution; you have to test the return value of inner loops that run 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
        [[ $? != 0 ]] && exit $?
        echo "After the j loop."
done
echo "After all the loops."

Note the test: [[ $? != 0 ]] && exit $?

output:

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!

Edit: To verify what subshell you're in, modify the "answer" script to tell you what the process ID of your current shell is. NOTE: This only works in 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
        [[ $? != 0 ]] && echo pid $BASHPID && exit $?
        echo "After the j loop."
done
echo "After all the loops."

output:

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

The variables "i" and "j" brought to you courtesy of Fortran. Have a nice day. :-)

  • Both the inner and outer loop are running in the same sub shell, so exiting the inner one should exit both? I do have trouble reproducing the problem myself though. Soon as I remove most of the program logic, the problem is gone. Anyway, I'll mark this as answer for now, because it's likely to have to do something with it. – user923487 Jul 1 '15 at 11:55
  • After reading the link you provided, I think I found the problem. In the outer loop I'm doing "cat file | while read line". Piping creates a sub shell. I didn't know that. – user923487 Jul 1 '15 at 12:08
  • @user923487 - See my updated answer. If you have bash 4, you can echo (or printf, if you're modern) the subshell pid and verify whether you are in a subshell or not. To see your bash version, type bash --version on the command line. – Mike S Jul 1 '15 at 13:35
  • Very helpful. Thanks! Although I got to say "killall scriptname.sh" seems to be the most straightforward way to solve this. – user923487 Jul 2 '15 at 8:37
1

You can use break.

From help break:

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

So for exiting from three enclosing loops i.e. if you have two nested loops inside main one, use this to exit from all of them:

break 3
  • There's more code after the loops, so breaking out of those alone is not enough. – user923487 Jun 30 '15 at 15:45
  • 1
    cool thx! example for((i=0;i<3;i++));do echo A;for((j=0;j<2;j++));do echo B;break 2;done;done – Aquarius Power Feb 4 '16 at 21:20
1

An earlier answer suggests using [[ $? != 0 ]] && exit $? however this won't quite work as expected, because the [[ $? != 0 ]] test will reset $? back to zero, which means that although the script will early-exit as expected, it'll always exit with code 0 (as not expected). Also, it'd be better to use the -ne numeric comparison test, rather than the != string comparison test. So therefore IMHO a better solution is to use:

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

as that'll ensure that the actual exit code is set correctly.

0

exit does terminate the whole shell, or the current sub-shell:

$ 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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.