Book HomeBook TitleSearch this book

5.5. while and until

The remaining two flow control constructs the Korn shell provides are while and until. These are similar; both allow a section of code to be run repetitively while (or until) a certain condition holds true. They also resemble analogous constructs in Pascal (while/do and repeat/until) and C (while and do/while).

while and until are actually most useful when combined with features we will see in the next chapter, such as arithmetic, input/output of variables, and command-line processing. Yet we can show a useful example even with the machinery we have covered so far.

The syntax for while is:

while condition
do
    statements ...
done

For until, just substitute until for while in the above example. As with if, the condition is really a list of statements that are run; the exit status of the last one is used as the value of the condition. You can use a conditional with [[ and ]] here, just as you can with if.

NOTE: The only difference between while and until is the way the condition is handled. In while, the loop executes as long as the condition is true; in until, it runs as long as the condition is false. So far, so familiar. However, the until condition is checked at the top of the loop, not at the bottom as it is in analogous constructs in C and Pascal.

The result is that you can convert any until into a while simply by negating the condition. The only place where until might be better is something like this:

until command; do
    statements ...
done

The meaning of this is essentially, "Do statements until command runs correctly." This is occasionally useful, such as when waiting for the occurrence of a particular event. However, we use while throughout the rest of this book.

Task 5-5 is a good candidate for while.

Task 5-5

Implement a simplified version of the shell's built-in whence command.

By "simplified," we mean that we will implement only the part that checks all the directories in your PATH for the command you give as argument (we won't implement checking for aliases, built-in commands, etc.).

We can do this by picking off the directories in PATH one by one, using one of the shell's pattern-matching operators, and seeing if there is a file with the given name in the directory that you have permission to execute. Here is the code:

path=$PATH:
dir=${path%%:*}
while [[ -n $path ]]; do
    if [[ $dir == "" ]] ; then
        dir="."
    fi
    if [[ -x $dir/$1 && ! -d $dir/$1 ]]; then
        print "$dir/$1"
        exit 0
    fi
    path=${path#*:}
    dir=${path%%:*}
done
exit 1

The first line of this code saves $PATH in path, our own temporary copy. We append a colon to the end so that every directory in $path ends in a colon (in $PATH, colons are used only between directories); subsequent code depends on this being the case.

The next line picks the first directory off $path by using the operator that deletes the longest match of the pattern given. In this case, we delete the longest match of the pattern :*, i.e., a colon followed by anything. This gives us the first directory in $path, which we store in the variable dir.

The condition in the while loop checks if $path is non-null. If it is not null, it first checks that $dir is not null. This could happen for two adjacent colons, which represent the current directory. In this case, dir is explicitly set to dot. Next, the script constructs the full pathname $dir/$1 and sees if there is a file by that name for which you have execute permission (and that is not a directory). If so, it prints the full pathname and exits the routine with a 0 ("OK") exit status.

If a file is not found, this code is run:

path=${path#*:}
dir=${path%%:*}

The first of these uses another shell string operator: this one deletes the shortest match of the pattern given from the front of the string. By now, this type of operator should be familiar. This line deletes the front directory from $path and assigns the result back to path. The second line is the same as before the while: it finds the (new) front directory in $path and assigns it to dir. This sets up the loop for another iteration.

Thus, the code loops through all of the directories in PATH. It exits when it finds a matching executable file or when it has "eaten up" the entire PATH. If no matching executable file is found, it prints nothing and exits with an error status.

We can enhance this script a bit by taking advantage of the Unix utility file(1). file examines files given as arguments and determines what type they are, based on the file's magic number and various heuristics (educated guesses). A magic number is a field in the header of an executable file that the linker sets to identify what type of executable it is.

If filename is an executable program (compiled from C or some other language), typing file filename produces output similar to this:

filename: ELF 32-bit LSB executable 80386 Version 1

However, if filename is not an executable program, it will examine the first few lines and try to guess what kind of information the file contains. If the file contains text (as opposed to binary data), file will look for indications that it is English, shell commands, C, Fortran, troff(1) input, and various other things. file is wrong sometimes, but it is usually correct.

Assume that fred is an executable file in the directory /usr/bin, and that bob is a shell script in /usr/local/bin. Typing file /usr/bin/fred produces this output:

/usr/bin/fred: ELF 32-bit LSB executable 80386 Version 1

Typing file /usr/local/bin/bob has this result:

/usr/local/bin/bob: commands text

We can just substitute file for print to print a more informative message in our script:

path=$PATH:
while [[ -n $path ]]; do
    dir=${path%%:*}
    if [[ $dir == "" ]] ; then
        dir="."
    fi
    if [[ -x $dir/$1 && ! -d $dir/$1 ]]; then
          file $dir/$1
          exit 0
    fi
    path=${path#*:}
done
exit 1

Notice that by moving the statement dir=${path%%:*} to the top of the loop body, it needs to be done only once.

Finally, just to show how little difference there is between while, and until, we note that the line:

until [[ ! -n $path ]]; do

can be used in place of:

while [[ -n $path ]]; do

with identical results.

We'll see additional examples of while in the next chapter.

5.5.1. break and continue

Earlier in this chapter, we saw the break statement used with the select construct for breaking out of a loop. break can be used with any looping construct: for, select, while and until.

The continue statement is related; its job is to skip any remaining statements in the body of the loop and start the next iteration.

Both the break and continue statements take an optional numeric argument (which can be a numeric expression). This indicates how many enclosing loops should be broken out of or continued. For example:

while condition1; do             Outer loop
    ...
    while condition2; do         Inner loop
        ...
        break 2                  Breaks out of outer loop
    done
done
...                              Execution continues here after break

Programmers will note that the break and continue statements, particularly with the ability to break or continue multiple loop levels, compensate in a very clean fashion for the lack of a goto keyword in the shell language.



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.