Chapter 10. Loops and Branches

Operations on code blocks are the key to structured, organized shell scripts. Looping and branching constructs provide the tools for accomplishing this.

10.1. Loops

A loop is a block of code that iterates (repeats) a list of commands as long as the loop control condition is true.

for loops

for (in)

This is the basic looping construct. It differs significantly from its C counterpart.

for arg in [list]
do
 command(s)...
done

Note

During each pass through the loop, arg takes on the value of each successive variable in the list.

   1 for arg in "$var1" "$var2" "$var3" ... "$varN"  
   2 # In pass 1 of the loop, $arg = $var1	    
   3 # In pass 2 of the loop, $arg = $var2	    
   4 # In pass 3 of the loop, $arg = $var3	    
   5 # ...
   6 # In pass N of the loop, $arg = $varN
   7 
   8 # Arguments in [list] quoted to prevent possible word splitting.

The argument list may contain wild cards.

If do is on same line as for, there needs to be a semicolon after list.

for arg in [list] ; do


Example 10-1. Simple for loops

   1 #!/bin/bash
   2 # List the planets.
   3 
   4 for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
   5 do
   6   echo $planet
   7 done
   8 
   9 echo
  10 
  11 # Entire 'list' enclosed in quotes creates a single variable.
  12 for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
  13 do
  14   echo $planet
  15 done
  16 
  17 exit 0

Note

Each [list] element may contain multiple parameters. This is useful when processing parameters in groups. In such cases, use the set command (see Example 11-13) to force parsing of each [list] element and assignment of each component to the positional parameters.


Example 10-2. for loop with two parameters in each [list] element

   1 #!/bin/bash
   2 # Planets revisited.
   3 
   4 # Associate the name of each planet with its distance from the sun.
   5 
   6 for planet in "Mercury 36" "Venus 67" "Earth 93"  "Mars 142" "Jupiter 483"
   7 do
   8   set -- $planet  # Parses variable "planet" and sets positional parameters.
   9   # the "--" prevents nasty surprises if $planet is null or begins with a dash.
  10 
  11   # May need to save original positional parameters, since they get overwritten.
  12   # One way of doing this is to use an array,
  13   #        original_params=("$@")
  14 
  15   echo "$1		$2,000,000 miles from the sun"
  16   #-------two  tabs---concatenate zeroes onto parameter $2
  17 done
  18 
  19 # (Thanks, S.C., for additional clarification.)
  20 
  21 exit 0

A variable may supply the [list] in a for loop.


Example 10-3. Fileinfo: operating on a file list contained in a variable

   1 #!/bin/bash
   2 # fileinfo.sh
   3 
   4 FILES="/usr/sbin/privatepw
   5 /usr/sbin/pwck
   6 /usr/sbin/go500gw
   7 /usr/bin/fakefile
   8 /sbin/mkreiserfs
   9 /sbin/ypbind"     # List of files you are curious about.
  10                   # Threw in a dummy file, /usr/bin/fakefile.
  11 
  12 echo
  13 
  14 for file in $FILES
  15 do
  16 
  17   if [ ! -e "$file" ]       # Check if file exists.
  18   then
  19     echo "$file does not exist."; echo
  20     continue                # On to next.
  21    fi
  22 
  23   ls -l $file | awk '{ print $9 "         file size: " $5 }'  # Print 2 fields.
  24   whatis `basename $file`   # File info.
  25   echo
  26 done  
  27 
  28 exit 0

The [list] in a for loop may contain filename globbing, that is, using wildcards for filename expansion.


Example 10-4. Operating on files with a for loop

   1 #!/bin/bash
   2 # list-glob.sh: Generating [list] in a for-loop using "globbing".
   3 
   4 echo
   5 
   6 for file in *
   7 do
   8   ls -l "$file"  # Lists all files in $PWD (current directory).
   9   # Recall that the wild card character "*" matches every filename,
  10   # however, in "globbing", it doesn't match dot-files.
  11 
  12   # If the pattern matches no file, it is expanded to itself.
  13   # To prevent this, set the nullglob option
  14   # (shopt -s nullglob).
  15   # Thanks, S.C.
  16 done
  17 
  18 echo; echo
  19 
  20 for file in [jx]*
  21 do
  22   rm -f $file    # Removes only files beginning with "j" or "x" in $PWD.
  23   echo "Removed file \"$file\"".
  24 done
  25 
  26 echo
  27 
  28 exit 0

Omitting the in [list] part of a for loop causes the loop to operate on $@, the list of arguments given on the command line to the script. A particularly clever illustration of this is Example A-17.


Example 10-5. Missing in [list] in a for loop

   1 #!/bin/bash
   2 
   3 # Invoke both with and without arguments, and see what happens.
   4 
   5 for a
   6 do
   7  echo -n "$a "
   8 done
   9 
  10 #  The 'in list' missing, therefore the loop operates on '$@'
  11 #+ (command-line argument list, including whitespace).
  12 
  13 echo
  14 
  15 exit 0

It is possible to use command substitution to generate the [list] in a for loop. See also Example 12-39, Example 10-10 and Example 12-33.


Example 10-6. Generating the [list] in a for loop with command substitution

   1 #!/bin/bash
   2 # A for-loop with [list] generated by command substitution.
   3 
   4 NUMBERS="9 7 3 8 37.53"
   5 
   6 for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
   7 do
   8   echo -n "$number "
   9 done
  10 
  11 echo 
  12 exit 0

This is a somewhat more complex example of using command substitution to create the [list].


Example 10-7. A grep replacement for binary files

   1 #!/bin/bash
   2 # bin-grep.sh: Locates matching strings in a binary file.
   3 
   4 # A "grep" replacement for binary files.
   5 # Similar effect to "grep -a"
   6 
   7 E_BADARGS=65
   8 E_NOFILE=66
   9 
  10 if [ $# -ne 2 ]
  11 then
  12   echo "Usage: `basename $0` string filename"
  13   exit $E_BADARGS
  14 fi
  15 
  16 if [ ! -f "$2" ]
  17 then
  18   echo "File \"$2\" does not exist."
  19   exit $E_NOFILE
  20 fi  
  21 
  22 
  23 for word in $( strings "$2" | grep "$1" )
  24 # The "strings" command lists strings in binary files.
  25 # Output then piped to "grep", which tests for desired string.
  26 do
  27   echo $word
  28 done
  29 
  30 # As S.C. points out, the above for-loop could be replaced with the simpler
  31 #    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'
  32 
  33 
  34 # Try something like  "./bin-grep.sh mem /bin/ls"  to exercise this script.
  35 
  36 exit 0

More of the same.


Example 10-8. Listing all users on the system

   1 #!/bin/bash
   2 # userlist.sh
   3 
   4 PASSWORD_FILE=/etc/passwd
   5 n=1           # User number
   6 
   7 for name in $(awk 'BEGIN{FS=":"}{print $1}' < "$PASSWORD_FILE" )
   8 # Field separator = :    ^^^^^^
   9 # Print first field              ^^^^^^^^
  10 # Get input from password file               ^^^^^^^^^^^^^^^^^
  11 do
  12   echo "USER #$n = $name"
  13   let "n += 1"
  14 done  
  15 
  16 
  17 # USER #1 = root
  18 # USER #2 = bin
  19 # USER #3 = daemon
  20 # ...
  21 # USER #30 = bozo
  22 
  23 exit 0

A final example of the [list] resulting from command substitution.


Example 10-9. Checking all the binaries in a directory for authorship

   1 #!/bin/bash
   2 # findstring.sh:
   3 # Find a particular string in binaries in a specified directory.
   4 
   5 directory=/usr/bin/
   6 fstring="Free Software Foundation"  # See which files come from the FSF.
   7 
   8 for file in $( find $directory -type f -name '*' | sort )
   9 do
  10   strings -f $file | grep "$fstring" | sed -e "s%$directory%%"
  11   #  In the "sed" expression,
  12   #+ it is necessary to substitute for the normal "/" delimiter
  13   #+ because "/" happens to be one of the characters filtered out.
  14   #  Failure to do so gives an error message (try it).
  15 done  
  16 
  17 exit 0
  18 
  19 #  Exercise (easy):
  20 #  ---------------
  21 #  Convert this script to taking command-line parameters
  22 #+ for $directory and $fstring.

The output of a for loop may be piped to a command or commands.


Example 10-10. Listing the symbolic links in a directory

   1 #!/bin/bash
   2 # symlinks.sh: Lists symbolic links in a directory.
   3 
   4 
   5 directory=${1-`pwd`}
   6 #  Defaults to current working directory,
   7 #+ if not otherwise specified.
   8 #  Equivalent to code block below.
   9 # ----------------------------------------------------------
  10 # ARGS=1                 # Expect one command-line argument.
  11 #
  12 # if [ $# -ne "$ARGS" ]  # If not 1 arg...
  13 # then
  14 #   directory=`pwd`      # current working directory
  15 # else
  16 #   directory=$1
  17 # fi
  18 # ----------------------------------------------------------
  19 
  20 echo "symbolic links in directory \"$directory\""
  21 
  22 for file in "$( find $directory -type l )"   # -type l = symbolic links
  23 do
  24   echo "$file"
  25 done | sort                                  # Otherwise file list is unsorted.
  26 
  27 #  As Dominik 'Aeneas' Schnitzer points out,
  28 #+ failing to quote  $( find $directory -type l )
  29 #+ will choke on filenames with embedded whitespace.
  30 #  Even this will only pick up the first field of each argument.
  31 
  32 exit 0
  33 
  34 
  35 # Jean Helou proposes the following alternative:
  36 
  37 echo "symbolic links in directory \"$directory\""
  38 # Backup of the current IFS. One can never be too cautious.
  39 OLDIFS=$IFS
  40 IFS=:
  41 
  42 for file in $(find $directory -type l -printf "%p$IFS")
  43 do     #                              ^^^^^^^^^^^^^^^^
  44        echo "$file"
  45        done|sort

The stdout of a loop may be redirected to a file, as this slight modification to the previous example shows.


Example 10-11. Symbolic links in a directory, saved to a file

   1 #!/bin/bash
   2 # symlinks.sh: Lists symbolic links in a directory.
   3 
   4 OUTFILE=symlinks.list                         # save file
   5 
   6 directory=${1-`pwd`}
   7 #  Defaults to current working directory,
   8 #+ if not otherwise specified.
   9 
  10 
  11 echo "symbolic links in directory \"$directory\"" > "$OUTFILE"
  12 echo "---------------------------" >> "$OUTFILE"
  13 
  14 for file in "$( find $directory -type l )"    # -type l = symbolic links
  15 do
  16   echo "$file"
  17 done | sort >> "$OUTFILE"                     # stdout of loop
  18 #           ^^^^^^^^^^^^^                       redirected to save file.
  19 
  20 exit 0

There is an alternative syntax to a for loop that will look very familiar to C programmers. This requires double parentheses.


Example 10-12. A C-like for loop

   1 #!/bin/bash
   2 # Two ways to count up to 10.
   3 
   4 echo
   5 
   6 # Standard syntax.
   7 for a in 1 2 3 4 5 6 7 8 9 10
   8 do
   9   echo -n "$a "
  10 done  
  11 
  12 echo; echo
  13 
  14 # +==========================================+
  15 
  16 # Now, let's do the same, using C-like syntax.
  17 
  18 LIMIT=10
  19 
  20 for ((a=1; a <= LIMIT ; a++))  # Double parentheses, and "LIMIT" with no "$".
  21 do
  22   echo -n "$a "
  23 done                           # A construct borrowed from 'ksh93'.
  24 
  25 echo; echo
  26 
  27 # +=========================================================================+
  28 
  29 # Let's use the C "comma operator" to increment two variables simultaneously.
  30 
  31 for ((a=1, b=1; a <= LIMIT ; a++, b++))  # The comma chains together operations.
  32 do
  33   echo -n "$a-$b "
  34 done
  35 
  36 echo; echo
  37 
  38 exit 0

See also Example 26-11, Example 26-12, and Example A-7.

---

Now, a for-loop used in a "real-life" context.


Example 10-13. Using efax in batch mode

   1 #!/bin/bash
   2 
   3 EXPECTED_ARGS=2
   4 E_BADARGS=65
   5 
   6 if [ $# -ne $EXPECTED_ARGS ]
   7 # Check for proper no. of command line args.
   8 then
   9    echo "Usage: `basename $0` phone# text-file"
  10    exit $E_BADARGS
  11 fi
  12 
  13 
  14 if [ ! -f "$2" ]
  15 then
  16   echo "File $2 is not a text file"
  17   exit $E_BADARGS
  18 fi
  19   
  20 
  21 fax make $2              # Create fax formatted files from text files.
  22 
  23 for file in $(ls $2.0*)  # Concatenate the converted files.
  24                          # Uses wild card in variable list.
  25 do
  26   fil="$fil $file"
  27 done  
  28 
  29 efax -d /dev/ttyS3 -o1 -t "T$1" $fil   # Do the work.
  30 
  31 
  32 # As S.C. points out, the for-loop can be eliminated with
  33 #    efax -d /dev/ttyS3 -o1 -t "T$1" $2.0*
  34 # but it's not quite as instructive [grin].
  35 
  36 exit 0

while

This construct tests for a condition at the top of a loop, and keeps looping as long as that condition is true (returns a 0 exit status). In contrast to a for loop, a while loop finds use in situations where the number of loop repetitions is not known beforehand.

while [condition]
do
 command...
done

As is the case with for/in loops, placing the do on the same line as the condition test requires a semicolon.

while [condition] ; do

Note that certain specialized while loops, as, for example, a getopts construct, deviate somewhat from the standard template given here.


Example 10-14. Simple while loop

   1 #!/bin/bash
   2 
   3 var0=0
   4 LIMIT=10
   5 
   6 while [ "$var0" -lt "$LIMIT" ]
   7 do
   8   echo -n "$var0 "        # -n suppresses newline.
   9   var0=`expr $var0 + 1`   # var0=$(($var0+1)) also works.
  10 done
  11 
  12 echo
  13 
  14 exit 0


Example 10-15. Another while loop

   1 #!/bin/bash
   2 
   3 echo
   4 
   5 while [ "$var1" != "end" ]     # while test "$var1" != "end"
   6 do                             # also works.
   7   echo "Input variable #1 (end to exit) "
   8   read var1                    # Not 'read $var1' (why?).
   9   echo "variable #1 = $var1"   # Need quotes because of "#".
  10   # If input is 'end', echoes it here.
  11   # Does not test for termination condition until top of loop.
  12   echo
  13 done  
  14 
  15 exit 0

A while loop may have multiple conditions. Only the final condition determines when the loop terminates. This necessitates a slightly different loop syntax, however.


Example 10-16. while loop with multiple conditions

   1 #!/bin/bash
   2 
   3 var1=unset
   4 previous=$var1
   5 
   6 while echo "previous-variable = $previous"
   7       echo
   8       previous=$var1
   9       [ "$var1" != end ] # Keeps track of what $var1 was previously.
  10       # Four conditions on "while", but only last one controls loop.
  11       # The *last* exit status is the one that counts.
  12 do
  13 echo "Input variable #1 (end to exit) "
  14   read var1
  15   echo "variable #1 = $var1"
  16 done  
  17 
  18 # Try to figure out how this all works.
  19 # It's a wee bit tricky.
  20 
  21 exit 0

As with a for loop, a while loop may employ C-like syntax by using the double parentheses construct (see also Example 9-29).


Example 10-17. C-like syntax in a while loop

   1 #!/bin/bash
   2 # wh-loopc.sh: Count to 10 in a "while" loop.
   3 
   4 LIMIT=10
   5 a=1
   6 
   7 while [ "$a" -le $LIMIT ]
   8 do
   9   echo -n "$a "
  10   let "a+=1"
  11 done           # No surprises, so far.
  12 
  13 echo; echo
  14 
  15 # +=================================================================+
  16 
  17 # Now, repeat with C-like syntax.
  18 
  19 ((a = 1))      # a=1
  20 # Double parentheses permit space when setting a variable, as in C.
  21 
  22 while (( a <= LIMIT ))   # Double parentheses, and no "$" preceding variables.
  23 do
  24   echo -n "$a "
  25   ((a += 1))   # let "a+=1"
  26   # Yes, indeed.
  27   # Double parentheses permit incrementing a variable with C-like syntax.
  28 done
  29 
  30 echo
  31 
  32 # Now, C programmers can feel right at home in Bash.
  33 
  34 exit 0

Note

A while loop may have its stdin redirected to a file by a < at its end.

until

This construct tests for a condition at the top of a loop, and keeps looping as long as that condition is false (opposite of while loop).

until [condition-is-true]
do
 command...
done

Note that an until loop tests for the terminating condition at the top of the loop, differing from a similar construct in some programming languages.

As is the case with for/in loops, placing the do on the same line as the condition test requires a semicolon.

until [condition-is-true] ; do


Example 10-18. until loop

   1 #!/bin/bash
   2 
   3 until [ "$var1" = end ] # Tests condition here, at top of loop.
   4 do
   5   echo "Input variable #1 "
   6   echo "(end to exit)"
   7   read var1
   8   echo "variable #1 = $var1"
   9 done  
  10 
  11 exit 0