The remaining two flow control constructs the Korn shell provides are while and until. These are similar; they 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/until).
while and until are actually most useful when combined with features we will see in the next chapter, such as integer 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.
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 that 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. BUT: 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 by simply 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 not, in our opinion, a likely contingency. Therefore we will use while throughout the rest of this book.
Here is a task that is a good candidate for while.
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 of 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 [[ -x $dir/$1 && ! -d $dir/$1 ]]; then print "$dir/$1" return fi path=${path#*:} dir=${path%%:*} done return 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 of $path
by using the operator that deletes the longest match to the pattern
given. In this case, we delete the longest match to 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 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, then this code is run:
path=${path#*:} dir=${path%%:*}
The first of these uses another shell string operator: this one deletes the shortest match to 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), then 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 mostly correct.
We can just substitute file for print to print a more informative message in our script:
path=$PATH dir=${path%%:*} while [[ -n $path ]]; do if [[ -x $dir/$1 && ! -d $dir/$1 ]]; then file $dir/$1 return fi path=${path#*:} dir=${path%%:*} done return 1
Assume that fred is an executable file in the directory /usr/bin, and that bob is a shell script in /usr/local/bin. Then typing file fred produces this output:
/usr/bin/fred: ELF 32-bit LSB executable 80386 Version 1
And typing file bob has this result:
/usr/local/bin/bob: commands text
Before we end this chapter, we have two final notes. First, notice that the statement dir=${path%%:*} appears in two places, before the start of the loop and as the last statement in the loop's body. Some diehard C hackers are offended by this Pascal-like coding technique. Certain features of the C language allow programmers to create loops of the form:
while iterative-step; condition; do ... done
This is the same as the form of the script above: the iterative-step runs just before the condition each time around the loop.
We can write our script this way:
path=$PATH while dir=${path%%:*}; [[ -n $path ]]; do if [[ -x $dir/$1 && ! -d $dir/$1 ]]; then file $dir/$1 return fi path=${path#*:} done return 1
Although this example doesn't show great programming style, it does make the code smaller-hence its popularity with C programmers. Make sure you understand that our script is functionally identical to the previous script.
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.