6. Looping

  1. For Loops
  2. While Loops
  3. Until Loops
  4. Loop Control

6.1 For Loops

With functions, variables, branching, and soon looping under the proverbial belt, all the tools to create useful shell scripts are at hand.  But first, one must understand how to build and control shell loops. A loop repeats a command block based upon a set of criteria that determines when to continue or terminate the loop.  Loops are a hallmark of all programming languages because they allow computers to do with great precision and speed what humans are not so terribly great at: repeating actions.

In the Bourne shell, the first type to consider is the for loop. A for loop processes multiple commands on a list of items.  This is a bit non-traditional in that languages such as C and FORTRAN employ for loops as a sort of counting mechanism; the loop continues until the variable reaches a target value.  But here, the for loop uses the intermediate variable to execute actions on a set of items such as files or the results of a command.  In this way, the shell's for loop is a more flexible and useful than in most other languages.

The syntax of a for loop is:

for variable in list
do
   command1
   :
   commandn
done

The for loop's variable may be any legal shell variable.  It need not be declared prior to the loop, and in fact, good shell programming practice mandates that the scripter should choose a unique variable that is not and will not be used elsewhere.  This is punctuated by the fact that the variable does become global in the script. As the for loop executes the command block, the value of variable changes into the next item found in listItems in list must be separated by white space (space, tab, or newline characters).  The list may be generated by many different methods including: file expansion, variable substitution, positional parameters, command substitution, or by simply providing a list of strings immediately following the in statement.

$ for file in ./*
> do
> file ${file}
> done
./bin:         directory
./ftp:         directory
./majordomo.txt:       English text
./mig29.ps:    PostScript document
./ph.dat:      ascii text
./prologues:   directory
./source:      directory

Here, the for loop takes advantage of filename expansion.  The ./* directs the loop to process all of the files located in the current directory.  The intermediate variable used is named file.  Its values are passed to the file command.  This command gives a best guess description of what type of file the argument is.  To differentiate the file command from the intermediate variable, the variable of course is surrounded by curly braces.  As for the results, bin, ftp, prologues, and source are all directories; majordomo.txt is considered to be prose; mig29.ps is a PostScript document; and ph.dat is a plain ASCII file.

The same results can be achieved using command substitution.  Surely, by using ls in the working directory, the loop processes the same arguments:

$ for file in `ls`
> do
> file ${file}
> done
./bin:         directory
./ftp:         directory
./majordomo.txt:       English text
./mig29.ps:    PostScript document
./ph.dat:      ascii text
./prologues:   directory
./source:      directory

In fact, the commands passed to a for loop as a list can be as simple or complex as a scripter wishes; however, for readability and for ease of debugging, a programmer should consider assigning the command's output to a variable and then passing the variable's value as the loop's argument list.

$ NOPASSWD=`ypcat passwd | grep "^[a-zA-Z0-9]*::" | cut -f1 -d:`
$ for user in ${NOPASSWD}
> do
> mail ${user} << END
> Please set your password.  You currently don't have one!
> END
> done

Here's a nice hackers' example, or perhaps a tool for a security conscious system administrator.  The script begins by assigning to NOPASSWD the results of a rather lengthy command filter.  The command passes the NIS password database to grepGrep searches for any entry without a password.  The passwd database consists of a set of fields delimited by colons.  The second field is the account's encrypted password; hence, an account without a password would be denoted by an entry with a double colon immediately following the account name.  The list grep generates is then passed to cut to parse out the offending account names from the database entry.  After this building the list, the subsequent for loop sends an email to each user warning them that they must correct the security problem they created.

For the last consideration of for loops, the in list portion is optional.  If omitted, the loop uses the positional parameters as its arguments by default.  This is equivalent to writing, "for var in "$@" do ..."  This technique should be used with care because it is not always immediately obvious to another reading the script what the arguments of the loop are.

$ cat printargs
#!/bin/sh
for arg
do
   echo ${arg}
done
$ printargs foo bar *
foo
bar
bin
printargs

This simple example quickly illustrates the concept.  The user executes the printargs script which echos the arguments back to the console.  The arguments are the strings foo and bar as well as the contents of the cirrent directory as given by the shell's file expansion of *.


6.2 While Loops

A while loop is different from a for loop in that is uses a test condition to determine whether or not to execute its command block. As long as the condition returns true when tested, the loop executes the commands.  If it returns false, the script skips the loop and proceeds beyond its termination point.  Consequently, the command set could conceiveably never run.  This is further illustrated by the loop's syntax:

while condition
do
   command1
   :
   commandn
done

Because the loop starts with the test, if the condition fails, then the program continues at whatever action immediately follows the loop's done statement.  If the condition passes, then the script executes the enclosed command block.  After performing the last command in the block, the loop tests the condition again and determines whether to proceed beyond the loop or fall through it once more.

$ i=1
$ while [ ${i} -le 5 ]
> do
> echo ${i}
> i=`expr ${i} + 1`
> done
1
2
3
4
5

The preceeding example demonstrates a while loop.  Given the variable i whose initial value is one, the loop starts by checking if i is less than or equal to five.  Since it is, the loop prints i's value, increments it by one as shown with the expr command, and then tests its value once more.  This continues until i reaches six.  At that point, the loop terminates, and the shell returns control to the user.

The built-in command shift works well with while loops. Shift causes the positional parameters' values to left-shift by its argument.  Shift can take any non-negative value as its argument and reset the positional parameters accordingly.  By default, shift moves the arguments by one.  When the parameters are shifted by one, the first is dropped from the list, the second parameter's value becomes that of the first, and third's becomes the second, and so on up to the ninth parameter.  This technique can be used effectively with a while loop for processing script options or function arguments.

$ cat printargs
#!/bin/sh
while [ $# -gt 0 ]
do
   echo "$@"
   shift
done
$ printargs hello world "foo bar" bye
hello world foo bar bye
world foo bar bye
foo bar bye
bye

The printargs script combines a while loop with the shift command.  The script takes an argument list and prints the entire list.  After doing so, it then shifts the arguments by one and repeats the process until it shifts the arguments out of existence.  The example demonstrates this with a four argument list of words.  The third argument is two words passed as one variable by enclosing them in double-quotes.


6.3 Until Loops

Until loops are the other while loops.  They work similarly in nature to while loops except for one major difference. An until loop executes its command block only if the test condition is false.  In this sense, an until loop can be viewed as the logical negation of a while loop.

until condition
do
   command1
   :
   commandn
done

Once again, the command block may never be executed as shown in its syntax.  If the condition evaluates to true, then the script proceeds beyond the loop.

To demonstrate, the example below shows how to rewrite the printargs script above changing the while to an until loop:

$ cat printargs2
#!/bin/sh
until [ $# -le 0 ]
do
   echo "$@"
   shift
done
$ printargs2 hello world "foo bar" bye
hello world foo bar bye
world foo bar bye
foo bar bye
bye


6.4 Loop Control

Sometimes it is necessary to end a loop before the test condition is satisfied or a list is completely processed.  In such cases, the scripter uses the break command.  When encountered, the break command instructs the shell to continue processing beyond the enclosing loop.  In other words, script execution picks up just after the done statement.

$ ls -l
total 4
-rw-r--r--   1 rsayle   users         128 Aug 16 23:00 catfiles
-rw-r--r--   1 rsayle   users          48 Aug 16 23:01 exrc
drwxr-xr-x   2 rsayle   users        1024 Aug 16 23:02 include/
-rw-r--r--   1 rsayle   users          89 Aug 16 23:02 profile
$ cat catfiles
#!/bin/sh
for file in ./*
do
   if [ ! -d ${file} ]; then
      echo "${file}:"
      cat ${file}
      echo
   else
      break
   fi
done
echo
echo "End of catfiles"

For this example, the current working directory contains four objects: the script catfiles, an exrc file containing the user's vi defaults, an include directory holding a users header files, and a profile containing the user's shell default environment.  Looking at catfiles, the script runs a for loop against all files in the working directory.  If the file is not a directory, the script prints a line with the file's name followed by a colon.  Then the program cats the file printing its contents to the screen.  If the file is a directory, however, the script breaks the for loop and prints a termination message before exiting.  Running catfiles against the directory produces:

$ catfiles
./catfiles:
#!/bin/sh
for file in ./*
do
   if [ ! -d ${file} ]; then
      echo "${file}:"
      cat ${file}
   else
      break
   fi
done

./exrc:
set number autoindent tabstop=2 showmode nowrap

End of catfiles
$

The files, as listed by the previous ls command, are processed in order.  As the script encounters each file before the include directory, it cats the file.  Once it hits include, the script completes leaving the contents of profile a mystery.

Had the programmer intended to view all files except directories, the programmer would have used the continue command instead. When the shell encounters a continue it skips the current processing step and proceeds with the loop's next iteration.  The loop does not just quit as it does with break.  It just executes the next trial until the test condition is satisfied or the argument list is exhausted.

Taking a look at catfiles using a continue instead of a break shows the difference.  This time the script steps past the include directory and dumps the contents of profile.

$ catfiles
./catfiles:
#!/bin/sh
for file in ./*
do
   if [ ! -d ${file} ]; then
      echo "${file}:"
      cat ${file}
   else
      continue
   fi
done

./exrc:
set number autoindent tabstop=2 showmode nowrap

./profile:
# setup terminal characteristics
export TERM=vt100
set -o vi
# turn off messaging
mesg n