Exiting a shell script with nested loops

23,292

Solution 1

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
        err=$?; [[ $err != 0 ]] && exit $err
        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
        err=$?; [[ $err != 0 ]] && echo pid $BASHPID && exit $err
        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. :-)

Solution 2

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.

Solution 3

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
Share:
23,292

Related videos on Youtube

user923487
Author by

user923487

Updated on September 18, 2022

Comments

  • user923487
    user923487 over 1 year

    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.

    • clerksx
      clerksx almost 9 years
      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"'.
    • user923487
      user923487 almost 9 years
      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.
    • Toby Speight
      Toby Speight almost 9 years
      Why can't you write a script that shows the problem and post it here? That sounds unlikely to me.
    • Toby Speight
      Toby Speight almost 9 years
      Is it the case that the inner loop takes place in a sub-shell in your code?
    • user923487
      user923487 almost 9 years
      @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
      user923487 almost 9 years
      I'll post an example of the problem tomorrow.
    • clerksx
      clerksx almost 9 years
      Well, there's your problem, then. You're exiting the subshell, not the main script.
    • Mike S
      Mike S almost 9 years
      @user923487 don't forget to mark a question as accepted, assuming your question is answered here. Which it might be :-) . But if not, post some code and we can figure out why it's busted.
  • user923487
    user923487 almost 9 years
    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
    user923487 almost 9 years
    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.
  • Mike S
    Mike S almost 9 years
    @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.
  • user923487
    user923487 almost 9 years
    Very helpful. Thanks! Although I got to say "killall scriptname.sh" seems to be the most straightforward way to solve this.
  • Aquarius Power
    Aquarius Power over 8 years
    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
  • Mike S
    Mike S about 4 years
    Good feedback. I've corrected the code.