Exiting a shell script with nested loops
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
Related videos on Youtube
user923487
Updated on September 18, 2022Comments
-
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 almost 9 yearsWhat 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 almost 9 yearsYou'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 almost 9 yearsWhy can't you write a script that shows the problem and post it here? That sounds unlikely to me.
-
Toby Speight almost 9 yearsIs it the case that the inner loop takes place in a sub-shell in your code?
-
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 almost 9 yearsI'll post an example of the problem tomorrow.
-
clerksx almost 9 yearsWell, there's your problem, then. You're exiting the subshell, not the main script.
-
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 almost 9 yearsBoth 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 almost 9 yearsAfter 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 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 almost 9 yearsVery helpful. Thanks! Although I got to say "killall scriptname.sh" seems to be the most straightforward way to solve this.
-
Aquarius Power over 8 yearscool 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 about 4 yearsGood feedback. I've corrected the code.