11.3. 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 [1] 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 iteration of the loop, skipping all the remaining commands in that particular loop cycle.


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

   1 #!/bin/bash
   2 
   3 LIMIT=19  # Upper limit
   4 
   5 echo
   6 echo "Printing Numbers 1 through 20 (but not 3 and 11)."
   7 
   8 a=0
   9 
  10 while [ $a -le "$LIMIT" ]
  11 do
  12  a=$(($a+1))
  13 
  14  if [ "$a" -eq 3 ] || [ "$a" -eq 11 ]  # Excludes 3 and 11.
  15  then
  16    continue      # Skip rest of this particular loop iteration.
  17  fi
  18 
  19  echo -n "$a "   # This will not execute for 3 and 11.
  20 done 
  21 
  22 # Exercise:
  23 # Why does the loop print up to 20?
  24 
  25 echo; echo
  26 
  27 echo Printing Numbers 1 through 20, but something happens after 2.
  28 
  29 ##################################################################
  30 
  31 # Same loop, but substituting 'break' for 'continue'.
  32 
  33 a=0
  34 
  35 while [ "$a" -le "$LIMIT" ]
  36 do
  37  a=$(($a+1))
  38 
  39  if [ "$a" -gt 2 ]
  40  then
  41    break  # Skip entire rest of loop.
  42  fi
  43 
  44  echo -n "$a "
  45 done
  46 
  47 echo; echo; echo
  48 
  49 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

   1 #!/bin/bash
   2 # break-levels.sh: Breaking out of loops.
   3 
   4 # "break N" breaks out of N level loops.
   5 
   6 for outerloop in 1 2 3 4 5
   7 do
   8   echo -n "Group $outerloop:   "
   9 
  10   # --------------------------------------------------------
  11   for innerloop in 1 2 3 4 5
  12   do
  13     echo -n "$innerloop "
  14 
  15     if [ "$innerloop" -eq 3 ]
  16     then
  17       break  # Try   break 2   to see what happens.
  18              # ("Breaks" out of both inner and outer loops.)
  19     fi
  20   done
  21   # --------------------------------------------------------
  22 
  23   echo
  24 done  
  25 
  26 echo
  27 
  28 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

   1 #!/bin/bash
   2 # The "continue N" command, continuing at the Nth level loop.
   3 
   4 for outer in I II III IV V           # outer loop
   5 do
   6   echo; echo -n "Group $outer: "
   7 
   8   # --------------------------------------------------------------------
   9   for inner in 1 2 3 4 5 6 7 8 9 10  # inner loop
  10   do
  11 
  12     if [[ "$inner" -eq 7 && "$outer" = "III" ]]
  13     then
  14       continue 2  # Continue at loop on 2nd level, that is "outer loop".
  15                   # Replace above line with a simple "continue"
  16                   # to see normal loop behavior.
  17     fi  
  18 
  19     echo -n "$inner "  # 7 8 9 10 will not echo on "Group III."
  20   done  
  21   # --------------------------------------------------------------------
  22 
  23 done
  24 
  25 echo; echo
  26 
  27 # Exercise:
  28 # Come up with a meaningful use for "continue N" in a script.
  29 
  30 exit 0


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

   1 # Albert Reiner gives an example of how to use "continue N":
   2 # ---------------------------------------------------------
   3 
   4 #  Suppose I have a large number of jobs that need to be run, with
   5 #+ any data that is to be treated in files of a given name pattern
   6 #+ in a directory. There are several machines that access
   7 #+ this directory, and I want to distribute the work over these
   8 #+ different boxen.
   9 #  Then I usually nohup something like the following on every box:
  10 
  11 while true
  12 do
  13   for n in .iso.*
  14   do
  15     [ "$n" = ".iso.opts" ] && continue
  16     beta=${n#.iso.}
  17     [ -r .Iso.$beta ] && continue
  18     [ -r .lock.$beta ] && sleep 10 && continue
  19     lockfile -r0 .lock.$beta || continue
  20     echo -n "$beta: " `date`
  21     run-isotherm $beta
  22     date
  23     ls -alF .Iso.$beta
  24     [ -r .Iso.$beta ] && rm -f .lock.$beta
  25     continue 2
  26   done
  27   break
  28 done
  29 
  30 exit 0
  31 
  32 #  The details, in particular the sleep N, are particular to my
  33 #+ application, but the general pattern is:
  34 
  35 while true
  36 do
  37   for job in {pattern}
  38   do
  39     {job already done or running} && continue
  40     {mark job as running, do job, mark job as done}
  41     continue 2
  42   done
  43   break        # Or something like `sleep 600' to avoid termination.
  44 done
  45 
  46 #  This way the script will stop only when there are no more jobs to do
  47 #+ (including jobs that were added during runtime). Through the use
  48 #+ of appropriate lockfiles it can be run on several machines
  49 #+ concurrently without duplication of calculations [which run a couple
  50 #+ of hours in my case, so I really want to avoid this]. Also, as search
  51 #+ always starts again from the beginning, one can encode priorities in
  52 #+ the file names. Of course, one could also do this without `continue 2',
  53 #+ but then one would have to actually check whether or not some job
  54 #+ was done (so that we should immediately look for the next job) or not
  55 #+ (in which case we terminate or sleep for a long time before checking
  56 #+ 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

[1]

These are shell builtins, whereas other loop commands, such as while and case, are keywords.