Classic Shell Scripting - Arnold Robbins [76]
Example 6-1. Findusers script, requires a username argument
#! /bin/sh
# finduser --- see if user named by first argument is logged in
if [ $# != 1 ]
then
echo Usage: finduser username >&2
exit 1
fi
who | grep $1
* * *
[1] C and C++ programmers take note! This is backward from what you're used to, and takes a while to get comfortable with.
[2] Mike Haertel points out that this has never been completely reliable: a_file could be changed in the interval between running test and running cat.
The case Statement
If you need to check a variable for one of many values, you could use a cascading series of if and elif tests, together with test:
if [ "X$1" = "X-f" ]
then
... Code for -f option
elif [ "X$1" = "X-d" ] || [ "X$1" = "X--directory" ] # long option allowed
then
... Code for -d option
else
echo $1: unknown option >&2
exit 1
fi
However, this is awkward to write and difficult to read. (The >&2 in the echo command sends the output to standard error. This is described in Section 7.3.2.) Instead, the shell's case construct should be used for pattern matching:
case $1 in
-f)
... Code for -f option
;;
-d | --directory) # long option allowed
... Code for -d option
;;
*)
echo $1: unknown option >&2
exit 1
# ;; is good form before `esac', but not required
esac
As can be seen, the value to be tested appears between case and in. Double-quoting the value, while not necessary, doesn't hurt either. The value is tested against each list of shell patterns in turn. When one matches, the corresponding body of code, up to the ;;, is executed. Multiple patterns may be used, separated by the | character, which in this context means "or." The patterns may contain any shell wildcard characters, and variable, command, and arithmetic substitutions are performed on the value before it is used for pattern matching.
The unbalanced right parenthesis after each pattern list is perhaps surprising; this is the only instance in the shell language of unbalanced delimiters. (In Section 14.3.7, we will see that bash and ksh actually allow a leading ( in front of the pattern list.)
It is typical, but not required, to use a final pattern of *, which acts as a default case. This is usually where you would print a diagnostic message and exit. As shown previously, the final case does not require the trailing ;;, although it's definitely good form to include it.
Looping
Besides the if and case statements, the shell's looping constructs are the workhorse facilities for getting things done.
for Loops
The for loop iterates over a list of objects, executing the loop body for each individual object in turn. The objects may be command-line arguments, filenames, or anything else that can be created in list format. In Section 3.2.7.1, we showed this two-line script to update an XML brochure file:
mv atlga.xml atlga.xml.old
sed 's/Atlanta/&, the capital of the South/' < atlga.xml.old > atlga.xml
Now suppose, as is much more likely, that we have a number of XML files that make up our brochure. In this case, we want to make the change in all the XML files. The for loop is perfect for this:
for i in atlbrochure*.xml
do
echo $i
mv $i $i.old
sed 's/Atlanta/&, the capital of the South/' < $i.old > $i
done
This loop moves each original file to a backup copy by appending a .old suffix, and then processing the file with sed to create the new file. It also prints the filename as a sort of running progress indicator, which is helpful when there are many files to process.
The in list part of the for loop is optional. When omitted, the shell loops over the command-line arguments. Specifically, it's as if you had typed for i in "$@":
for i # loop over command-line args
do
case $i in
-f) ...
;;
...
esac
done
while and until Loops
The shell's while and until loops are similar to loops in conventional programming languages. The syntax is:
while condition until condition
do do
statements
statements
done done
As for the if statement, condition may be a simple list of