Loop Control

Bash Unix Shell Scripting:
Chapter 11. Loops and Branches

Loop Control

 

Tournez cent tours, tournez mille tours,

Tournez souvent et tournez toujours . . .

--Verlaine, "Chevaux de bois"

Commands affecting loop behavior

break, continue

The break and continue loop control commands correspond exactly to their counterparts in other programming languages. The break command terminates the loop (breaks out of it), while continue causes a jump to the next of the loop, skipping all the remaining commands in that particular loop cycle.

Example 11-21. Effects of break and continue in a loop

#!/bin/bash
LIMIT=19  # Upper limit
echo
echo "Printing Numbers 1 through 20 (but not 3 and 11)."
a=0
while [ $a -le "$LIMIT" ]
do
 a=$(($a+1))
 if [ "$a" -eq 3 ] || [ "$a" -eq 11 ]  # Excludes 3 and 11.
 then
   continue      # Skip rest of this particular loop iteration.
 fi
 echo -n "$a "   # This will not execute for 3 and 11.
done 
# Exercise:
# Why does the loop print up to 20?
echo; echo
echo Printing Numbers 1 through 20, but something happens after 2.
##################################################################
# Same loop, but substituting 'break' for 'continue'.
a=0
while [ "$a" -le "$LIMIT" ]
do
 a=$(($a+1))
 if [ "$a" -gt 2 ]
 then
   break  # Skip entire rest of loop.
 fi
 echo -n "$a "
done
echo; echo; echo
exit 0

The break command may optionally take a parameter. A plain break terminates only the innermost loop in which it is embedded, but a break N breaks out of N levels of loop.

Example 11-22. Breaking out of multiple loop levels

#!/bin/bash
# break-levels.sh: Breaking out of loops.
# "break N" breaks out of N level loops.
for outerloop in 1 2 3 4 5
do
  echo -n "Group $outerloop:   "
  # --------------------------------------------------------
  for innerloop in 1 2 3 4 5
  do
    echo -n "$innerloop "
    if [ "$innerloop" -eq 3 ]
    then
      break  # Try   break 2   to see what happens.
             # ("Breaks" out of both inner and outer loops.)
    fi
  done
  # --------------------------------------------------------
  echo
done  
echo
exit 0

The continue command, similar to break, optionally takes a parameter. A plain continue cuts short the current iteration within its loop and begins the next. A continue N terminates all remaining iterations at its loop level and continues with the next iteration at the loop, N levels above.

Example 11-23. Continuing at a higher loop level

#!/bin/bash
# The "continue N" command, continuing at the Nth level loop.
for outer in I II III IV V           # outer loop
do
  echo; echo -n "Group $outer: "
  # --------------------------------------------------------------------
  for inner in 1 2 3 4 5 6 7 8 9 10  # inner loop
  do
    if [[ "$inner" -eq 7 && "$outer" = "III" ]]
    then
      continue 2  # Continue at loop on 2nd level, that is "outer loop".
                  # Replace above line with a simple "continue"
                  # to see normal loop behavior.
    fi  
    echo -n "$inner "  # 7 8 9 10 will not echo on "Group III."
  done  
  # --------------------------------------------------------------------
done
echo; echo
# Exercise:
# Come up with a meaningful use for "continue N" in a script.
exit 0

Example 11-24. Using continue N in an actual task

# Albert Reiner gives an example of how to use "continue N":
# ---------------------------------------------------------
#  Suppose I have a large number of jobs that need to be run, with
#+ any data that is to be treated in files of a given name pattern
#+ in a directory. There are several machines that access
#+ this directory, and I want to distribute the work over these
#+ different boxen.
#  Then I usually nohup something like the following on every box:
while true
do
  for n in .iso.*
  do
    [ "$n" = ".iso.opts" ] && continue
    beta=${n#.iso.}
    [ -r .Iso.$beta ] && continue
    [ -r .lock.$beta ] && sleep 10 && continue
    lockfile -r0 .lock.$beta || continue
    echo -n "$beta: " `date`
    run-isotherm $beta
    date
    ls -alF .Iso.$beta
    [ -r .Iso.$beta ] && rm -f .lock.$beta
    continue 2
  done
  break
done
exit 0
#  The details, in particular the sleep N, are particular to my
#+ application, but the general pattern is:
while true
do
  for job in {pattern}
  do
    {job already done or running} && continue
    {mark job as running, do job, mark job as done}
    continue 2
  done
  break        # Or something like `sleep 600' to avoid termination.
done
#  This way the script will stop only when there are no more jobs to do
#+ (including jobs that were added during runtime). Through the use
#+ of appropriate lockfiles it can be run on several machines
#+ concurrently without duplication of calculations [which run a couple
#+ of hours in my case, so I really want to avoid this]. Also, as search
#+ always starts again from the beginning, one can encode priorities in
#+ the file names. Of course, one could also do this without `continue 2',
#+ but then one would have to actually check whether or not some job
#+ was done (so that we should immediately look for the next job) or not
#+ (in which case we terminate or sleep for a long time before checking
#+ for a new job).
Caution

The continue N construct is difficult to understand and tricky to use in any meaningful context. It is probably best avoided.

Notes

These are shell , whereas other loop commands, such as and , are .