sections in this module City College of San Francisco - CS160B
Unix/Linux Shell Scripting
Module: Loops1
module list

The
while loop

The general form of the while loop is

while command; do
commands to execute
done

Use a while loop whenever the loop exit is controlled by an external event, or when the amount of data to be processed is large, or when the things to iterate on are read from a file or from a pipe. (We will go over using pipes and files with while loops in the next module.) 

I will divide the major uses of a while loop into two groups:

We will use second case to discuss the while loop and learn some of its intricies. 

Using a while loop to validate data

In our previous  shell scripts, many events were fatal. This is common in a command-line program, where there is not the ability to request corrected data. In an interactive program, however, if the user enters an incorrect data value, we could certainly ask them to re-enter it. Let's consider a snippet of shell code:

read -p "Enter the filename:" file
if [ ! -f "$file" -o ! -r "$file" ]; then
echo "'$file' is not a readable file" >&2
exit 1
fi

This is rather unfortunate. The user made a typo well into the program and we just had to give up. Adding a while loop allows us to repeat the question until the input is valid. Here is the algorithm:

while the input is not valid 
output error message
get new input
done

Let's look at such a loop in actual code:

while [ ! -f "$file" -o ! -r "$file" ]; do
echo "'$file' is not a readable file" >&2
read -p "Enter the filename:" file
done

Going over a few issues about this loop we notice:

This last issue can be resolved by repeating the input question outside the loop:

read -p "Enter the filename:" file
while [ ! -f "$file" -o ! -r "$file" ]; do
echo "'$file' is not a readable file" >&2
read -p "Enter the filename:" file
done

Much better. Although we have to repeat a bit of the loop code outside the loop, this loop is very clear and quite compact. This occurred because the code to verify the data was incorrect was simple and there was no preparation for re-entering it. The harder it is to determine the data is bad, the more difficult it is to write a simple loop like this or the more code must be repeated outside of the loop.

Controlling the loop with continue and break

In the loop above, the command that controlled the loop was the command that validated our input. This allowed us to write a loop that flowed naturally: the loop body repeats until the controlling command failed, then control resumed after the loop body. It's nice when this happens, but massaging loops into this form can be difficult.

To see this, consider what would happen if we added a single requirement on $file: that it contained a particular type of data, say, it was a colon-delimited file with four fields. We could still write the loop, but it would be very ugly

read -p "Enter the filename:" file
while [ ! -f "$file" -o ! -r "$file" ] || !grep ":.*:.*:" > /dev/null; do
echo "'$file' is not a readable file" >&2
read -p "Enter the filename:" file
done

This worked, but what do we do about the error message? When we enter the loop each time, we do not know which error caused the failure unless we repeat some of the code. This gets worse as the complexity increases.

Although it is an affront to everything you would learn in other programming classes, while loops in the shell are often written as infinite loops with multiple exit points. This is accomplished by the use of two statements: break and continue

Execution of a break command causes the exit of the most recently entered while loop. Execution of a continue command causes the termination of the current iteration, and the beginning of a new iteration. 

In our loop, we will use break in the loop body to exit the loop. This means that our controlling expression should be something that always succeeds. You have two choices here:

We will use true as it makes the loop easier to read: while true

Now that our loop is always entered, we have no need of repititious code outside the loop. A little restructuring and we have

while true; do
read -p "Enter the filename:" file
if [ -f "$file" -a -r "$file" ]; then
break
fi
echo "'$file' is not a readable file - reenter it." >&2
done

Another slight modification makes this loop even simpler:

while true; do
read -p "Enter the filename:" file
[ -f "$file" -a -r "$file" ] && break
echo "'$file' is not a readable file - reenter it." >&2
done

Although this is less structured according to standard programming practices it has several advantages:

Let's look at a slightly different case to illustrate these points. We will write a loop that repeatedly asks the user for a filename, then, once the filename is verified as being a readable file, displays the file. The loop will stop when the user enters an empty filename. 

First, let's write the loop using a controlling command

read -p "Enter filename (empty to quit):" file
while [ -n "$file" ]; do
if [ -f "$file" -a -r "$file" ]; then
cat "$file"
else 
echo "'$file' is not a readable file. reenter" >&2
fi
read -p "Enter filename (empty to quit):" file
done

Not bad, really. This was pretty easy to write in the standard way. Let's look at how this could be written using an infinite loop:

while true; do
read -p "Enter filename (empty to quit):" file
[ -z "$file" ] && break
[ ! -f "$file" -o ! -r "$file" ] && \
echo "'$file' is not a readable file. reenter" >&2 && continue
cat "$file"
done

You could certainly argue that the first method was more straightforward. However, existing shell scripts will favor the second form. Why? Well, when we get to more complicated loops it will be more obvious that restricting the loop control to the top of the loop creates problems as there are often several loop conditions that need testing during each iteration. Any of these loop conditions may force the end of the loop or the beginning of another iteration. Also, shell programmers have a fondness for concise cryptic code, which this certainly is.

Another issue should be mentioned here: the fondness of shell programmers for the && and || construct. Any if statement with a single command in the then or else clause will be written as a && or || statement. If you are not comfortable with that syntax, practice it.

Processing commandline arguments

The while loop is often the loop of choice for processing commandline arguments, as it avoids the problem with retokenization of the command-line that we will see with the for loop, and it allows the analysis of complex options that take more than one commandline argument.

The algorithm for processing commandline arguments in a while loop goes like this:

while there are arguments left
examine the first argument and decide what to do with it
shift
done

Here, deciding what to do with it could involve setting a flag to indicate the option was seen, saving the option in a variable, or outputting an error message. Let's try a simple example:

You are writing a shell script whileex1 that takes two options and one required directory. The options are -x and -y, each is independent, and optional. The directory is to be used to store file(s) resulting from the execution of whileex1, so it must be writable. As will all Unix commands, the paths must follow the options. Write the code to process the arguments for whileex1

Since we only have two options, we will use an if construct to handle it. Let's discuss the types of questions we must ask:

Here is some sample code for this problem:

#!/bin/bash
#
error() {
echo "$(basename "$0"): ERROR - $*" >&2
echo "syntax: $(basename "$0") [-x] [-y] directory" >&2
exit 1
}
xflag=false
yflag=false
while [ $# -gt 0 ]; do
if [ "$1" = '-x' ]; then
xflag=true
elif [ "$1" = '-y' ]; then
yflag=true
else 
# assume it's the filename
[ ! -d "$1" -o ! -w "$1" ] && error "'$1' is not a writable directory"
[ $# -ne 1 ] && error "the directory must be the last argument"
file="$1"
fi
shift
done
[ -z "$file" ] && error "required writable directory argument missing"

There are several things to note about whileex1:

First, let's fix the issue about assuming the first illegal argument is the filename. The more conservative approach is to assume any argument with a leading dash is an option, and to complain about illegal options. Let's first modify the existing code. This is whileex2:

#!/bin/bash
#
error() {
echo "$(basename "$0"): ERROR - $*" >&2
echo "syntax: $(basename "$0") [-x] [-y] directory" >&2
exit 1
}
dir=
xflag=false
yflag=false
while [ $# -gt 0 ]; do
if [ "$1" = '-x' ]; then
xflag=true
elif [ "$1" = '-y' ]; then
yflag=true
elif echo "$1" | grep -q '^-'; then
error "illegal option '$1'"
else 
# assume it's the filename
[ ! -d "$1" -o ! -w "$1" ] && error "'$1' is not a writable directory"
[ $# -ne 1 ] && error "the directory must be the last argument"
file="$1"
fi
shift
done
[ -z "$file" ] && error "required writable directory argument missing"

But our code still gives up at the first error. To fix this problem, we will have to delay exiting until we have examined all the arguments and detected all the possible errors. When we detect an error we will set a flagvariable errors to indicate an error was found and use this flag to exit after the loop is complete. The variable errors will be set by our function error:

#!/bin/bash
#
error() {
#
# modifies the global variable errors
#
echo "$(basename "$0"): ERROR - $*" >&2

echo "syntax: $(basename "$0") [-x] [-y] directory" >&2
errors=true
}
dir=
xflag=false
yflag=false
errors=false
while [ $# -gt 0 ]; do
if [ "$1" = '-x' ]; then
xflag=true
elif [ "$1" = '-y' ]; then
yflag=true
elif echo "$1" | grep -q '^-'; then
error "illegal option '$1'"
else 
# assume it's the filename
[ ! -d "$1" -o ! -w "$1" ] && error "'$1' is not a writable directory"
[ $# -ne 1 ] && error "the directory must be the last argument"
file="$1"
fi
shift
done
[ -z "$file" ] && error "required writable directory argument missing"
if [ "$errors" = true ] ; then
    echo "failed" >&2
    exit 1
fi

Our shell script looks better. However, if we add any options to it, it requires modifying our if statement. The typical way to write option-handling code is with a case statement. Let's rewrite this code using a case statement and see how it looks:

#!/bin/bash
#
error() {
#
# modifies the global variable errors
#
echo "$(basename "$0"): ERROR - $*" >&2

echo "syntax: $(basename "$0") [-x] [-y] directory" >&2
errors=true
}
dir=
xflag=false
yflag=false
errors=false
while [ $# -gt 0 ]; do
case "$1" in
    -x) xflag=true;;
    -y) yflag=true;;
    -*) error "illegal option '$1'";;
     *) # assume it's the filename
        [ ! -d "$1" -o ! -w "$1" ] && error "'$1' is not a writable directory"
        [ $# -ne 1 ] && 
error "the directory must be the last argument"
        file="$1";;
esac
shift

done
[ -z "$file" ] && error "required writable directory argument missing"
if [ "$errors" = true ] ; then
    echo "failed" >&2
    exit 1
fi

Adding a new option to our shell program now just involves adding a new clause on our case statement. One last thing: our syntax message is output once per error. This is kind of silly. The syntax message should be output once when the shell script aborts. Can you fix this?

The examples whileex* are in this module's directory beneath examples on our public data area on hills.

The until loop

The until loop is very similar to the while loop. The only difference is the loop condition. The while loop repeats while the loop condition succeeds. The until loop continues until the loop condition succeeds. We will illustrate by rewriting our first example of a while loop:

read -p "Enter the filename:" file
until [  -f "$file" -a -r "$file" ]; do
echo "'$file' is not a readable file" >&2
read -p "Enter the filename:" file
done

Choosing a while or until loop

Any time you have a loop that must repeat until some external event occurs, you must use a while or until loop. It is often easier to design the loop as an infinite loop. Just be sure to alter the loop exit condition (or to use break) during the loop. while and until loops are also the loops of choice for processing command-line arguments.

Preview question: If you can create a list of things to process and the list is not "too large", a for loop is the loop of choice. Write a for loop to cat each file in a directory. How can you avoid cat-ing non-text files?

Prev This page was made entirely with free software on linux:  
Kompozer
and Openoffice.org    
Next

Copyright 2010 Greg Boyd - All Rights Reserved.

Document made with Kompozer