IN THIS CHAPTER
This chapter introduces a statement that is present in almost all programming languages: if
. It enables you to test a condition and then change the flow of program execution based on the result of the test.
The general format of the if
command is
if commandt then command command ... fi
where commandt is executed and its exit status is tested. If the exit status is zero, the commands that follow between the then
and the fi
are executed; otherwise, they are skipped.
Whenever any program completes execution under the Unix system, it returns an exit status back to the system. This status is a number that usually indicates whether the program successfully ran. By convention, an exit status of zero indicates that a program succeeded, and nonzero indicates that it failed. Failures can be caused by invalid arguments passed to the program, or by an error condition detected by the program. For example, the cp
command returns a nonzero exit status if the copy fails for some reason (for example, if it can't create the destination file), or if the arguments aren't correctly specified (for example, wrong number of arguments, or more than two arguments and the last one isn't a directory). In the case of grep
, an exit status of zero (success) is returned if it finds the specified pattern in at least one of the files; a nonzero value is returned if it can't find the pattern or if an error occurs (the arguments aren't correctly specified, or it can't open one of the files).
In a pipeline, the exit status is that of the last command in the pipe. So in
who | grep fred
the exit status of the grep
is used by the shell as the exit status for the pipeline. In this case, an exit status of zero means that fred
was found in who
's output (that is, fred
was logged on at the time that this command was executed).
The shell variable $?
is automatically set by the shell to the exit status of the last command executed. Naturally, you can use echo
to display its value at the terminal.
$ cp phonebook phone2 $ echo $? 0 Copy "succeeded" $ cp nosuch backup cp: cannot access nosuch $ echo $? 2 Copy "failed" $ who See who's logged on root console Jul 8 10:06 wilma tty03 Jul 8 12:36 barney tty04 Jul 8 14:57 betty tty15 Jul 8 15:03 $ who | grep barney barney tty04 Jul 8 14:57 $ echo $? Print exit status of last command (grep) 0 grep "succeeded" $ who | grep fred $ echo $? 1 grep "failed" $ echo $? 0 Exit status of last echo $
Note that the numeric result of a “failure” for some commands can vary from one Unix version to the next, but success is always signified by a zero exit status.
Let's now write a shell program called on
that tells us whether a specified user is logged on to the system. The name of the user to check will be passed to the program on the command line. If the user is logged on, we'll print a message to that effect; otherwise we'll say nothing. Here is the program:
$ cat on
#
# determine if someone is logged on
#
user="$1"
if who | grep "$user"
then
echo "$user is logged on"
fi
$
This first argument typed on the command line is stored in the shell variable user
. Then the if
command executes the pipeline
who | grep "$user"
and tests the exit status returned by grep
. If the exit status is zero, grep
found user
in who
's output. In that case, the echo
command that follows is executed. If the exit status is nonzero, the specified user is not logged on, and the echo
command is skipped. The echo
command is indented from the left margin for aesthetic reasons only (tab characters are usually used for such purposes because it's easier to type a tab character than an equivalent number of spaces). In this case, just a single command is enclosed between the then
and fi
. When more commands are included, and when the nesting gets deeper, indentation can have a dramatic effect on the program's readability. Later examples will help illustrate this point.
Here are some sample uses of on
:
$ who root console Jul 8 10:37 barney tty03 Jul 8 12:38 fred tty04 Jul 8 13:40 joanne tty07 Jul 8 09:35 tony tty19 Jul 8 08:30 lulu tty23 Jul 8 09:55 $ on tony We know he's on tony tty19 Jul 8 08:30 Where did this come from? tony is logged on $ on steve We know he's not on $ on ann Try this one joanne tty07 Jul 8 09:35 ann is logged on $
We seem to have uncovered a couple of problems with the program. When the specified user is logged on, the corresponding line from who
's output is also displayed. This may not be such a bad thing, but the program requirements called for only a message to be displayed and nothing else.
This line is displayed because not only does grep
return an exit status in the pipeline
who | grep "$user"
but it also goes about its normal function of writing any matching lines to standard output, even though we're really not interested in that. We can dispose of grep
's output by redirecting it to the system's “garbage can,” /dev/null
. This is a special file on the system that anyone can read from (and get an immediate end of file) or write to. When you write to it, the bits go to that great bit bucket in the sky!
who | grep "$user" > /dev/null
The second problem with on
appears when the program is executed with the argument ann
. Even though ann
is not logged on, grep
matches the characters ann
for the user joanne
. What you need here is a more restrictive pattern specification, which you learned how to do in Chapter 4, “Tools of the Trade,” where we talked about regular expressions. Because who
lists each username in column one of each output line, we can anchor the pattern to match the beginning of the line by preceding the pattern with the character ^
:
who | grep "^$user" > /dev/null
But that's not enough. grep
still matches a line like
bobby tty07 Jul 8 09:35
if you ask it to search for the pattern bob
. What you need to do is also anchor the pattern on the right. Realizing that who
ends each username with one or more spaces, the pattern
"^$user "
now only matches lines for the specified user
.
Let's try the new and improved version of on
:
$ cat on # # determine if someone is logged on -- version 2 # user="$1" if who | grep "^$user " > /dev/null then echo "$user is logged on" fi $ who Who's on now? root console Jul 8 10:37 barney tty03 Jul 8 12:38 fred tty04 Jul 8 13:40 joanne tty07 Jul 8 09:35 tony tty19 Jul 8 08:30 lulu tty23 Jul 8 09:55 $ on lulu lulu is logged on $ on ann Try this again $ on What happens if we don't give any arguments? $
If no arguments are specified, user
will be null. grep
will then look through who
's output for lines that start with a blank (why?). It won't find any, and so just a command prompt will be returned. In the next section, you'll see how to test whether the correct number of arguments has been supplied to a program and, if not, take some action.
A built-in shell command called test
is most often used for testing one or more conditions in an if
command. Its general format is
test expression
where expression represents the condition you're testing. test
evaluates expression, and if the result is true, it returns an exit status of zero; otherwise, the result is false, and it returns a nonzero exit status.
As an example of the use of test
, the following command returns a zero exit status if the shell variable name
contains the characters julio
:
test "$name" = julio
The =
operator is used to test whether two values are identical. In this case, we're testing to see whether the contents of the shell variable name
are identical to the characters julio
. If it is, test
returns an exit status of zero; nonzero otherwise.
Note that test
must see all operands ($name
and julio
) and operators (=
) as separate arguments, meaning that they must be delimited by one or more whitespace characters.
Getting back to the if
command, to echo
the message “Would you like to play a game?” if name
contains the characters julio
, you would write your if
command like this:
if test "$name" = julio then echo "Would you like to play a game?" fi
(Why is it better to play it safe and enclose the message that is displayed by echo
inside quotes?) When the if
command gets executed, the command that follows the if
is executed, and its exit status is tested. The test
command is passed the three arguments $name
(with its value substituted, of course), =
, and julio
. test
then tests to see whether the first argument is identical to the third argument and returns a zero exit status if it is and a nonzero exit status if it is not.
The exit
status returned by test
is then tested. If it's zero, the commands between then
and fi
are executed; in this case, the single echo
command is executed. If the exit status is nonzero, the echo
command is skipped.
It's good programming practice to enclose shell variables that are arguments to test
inside a pair of double quotes (to allow variable substitution). This ensures that test
sees the argument in the case where its value is null. For example, consider the following example:
$ name= Set name null $ test $name = julio sh: test: argument expected $
Because name
was null, only two arguments were passed to test
: =
and julio
because the shell substituted the value of name
before parsing the command line into arguments. In fact, after $name
was substituted by the shell, it was as if you typed the following:
test = julio
When test
executed, it saw only two arguments (see Figure 8.1) and therefore issued the error message.
By placing double quotes around the variable, you ensure that test
sees the argument because quotes act as a “placeholder” when the argument is null.
$ test "$name" = julio $ echo $? Print the exit status 1 $
Even if name
is null, the shell still passes three arguments to test
, the first one null (see Figure 8.2).
Other operators can be used to test character strings. These operators are summarized in Table 8.1.
You've seen how the =
operator is used. The !=
operator is similar, only it tests two strings for inequality. That is, the exit status from test
is zero if the two strings are not equal, and nonzero if they are.
Let's look at three similar examples.
$ day="monday" $ test "$day" = monday $ echo $? 0 True $
The test
command returns an exit status of 0 because the value of day
is equal to the characters monday
. Now look at the following:
$ day="monday " $ test "$day" = monday $ echo $? 1 False $
Here we assigned the characters monday
—including the space character that immediately followed—to day
. Therefore, when the previous test was made, test
returned false because the characters "monday "
were not identical to the characters "monday"
.
If you wanted these two values to be considered equal, omitting the double quotes would have caused the shell to “eat up” the trailing space character, and test
would have never seen it:
$ day="monday " $ test $day = monday $ echo $? 0 $ True
Although this seems to violate our rule about always quoting shell variables that are arguments to test
, it's okay to omit the quotes if you're sure that the variable is not null (and not composed entirely of whitespace characters).
You can test to see whether a shell variable has a null value with the third operator listed in Table 8.1:
test "$day"
This returns true if day
is not null and false if it is. Quotes are not necessary here because test doesn't care whether it sees an argument in this case. Nevertheless, you are better off using them here as well because if the variable consists entirely of whitespace characters, the shell will get rid of the argument if not enclosed in quotes.
$ blanks=" " $ test $blanks Is it not null? $ echo $? 1 False—it's null $ test "$blanks" And now? $ echo $? 0 True—it's not null $
In the first case, test
was not passed any arguments because the shell ate up the four spaces in blanks
. In the second case, test
got one argument consisting of four space characters; obviously not null.
In case we seem to be belaboring the point about blanks and quotes, realize that this is a sticky area that is a frequent source of shell programming errors. It's good to really understand the principles here to save yourself a lot of programming headaches in the future.
There is another way to test whether a string is null, and that's with either of the last two operators listed previously in Table 8.1. The -n
operator returns an exit status of zero if the argument that follows is not null. Think of this operator as testing for nonzero length.
The -z
operator tests the argument that follows to see whether it is null and returns an exit status of zero if it is. Think of this operator as testing to see whether the following argument has zero length.
test -n "$day"
returns an exit status of 0 if day
contains at least one character. The command
test -z "$dataflag"
returns an exit status of 0 if dataflag
doesn't contain any characters.
Be forewarned that both of the preceding operators expect an argument to follow; therefore, get into the habit of enclosing that argument inside double quotes.
$ nullvar= $ nonnullvar=abc $ test -n "$nullvar" Does nullvar have nonzero length? $ echo $? 1 No $ test -n "$nonnullvar" And what about nonnullvar? $ echo $? 0 Yes $ test -z "$nullvar" Does nullvar have zero length? $ echo $? 0 Yes $ test -z "$nonnullvar" And nonnullvar? $ echo $? 1 No $
Note that test
can be picky about its arguments. For example, if the shell variable symbol
contains an equals sign, look at what happens if you try to test it for zero length:
$ echo $symbol = $ test -z "$symbol" sh: test: argument expected $
The =
operator has higher precedence than the -z
operator, so test
expects an argument to follow. To avoid this sort of problem, you can write your command as
test X"$symbol" = X
which will be true if symbol
is null, and false if it's not. The X
in front of symbol
prevents test
from interpreting the characters stored in symbol
as an operator.
The test
command is used so often by shell programmers that an alternative format of the command is recognized. This format improves the readability of the command, especially when used in if
commands.
You'll recall that the general format of the test
command is
test expression
This can also be expressed in the alternative format as
[ expression ]
The [
is actually the name of the command (who said anything about command names having to be alphanumeric characters?). It still initiates execution of the same test
command, only in this format, test
expects to see a closing ]
at the end of the expression. Naturally, spaces must appear after the [
and before the ]
.
You can rewrite the test
command shown in a previous example with this alternative format as shown:
$ [ -z "$nonnullvar" ] $ echo $? 1 $
When used in an if
command, this alternative format looks like this:
if [ "$name" = julio ] then echo "Would you like to play a game?" fi
Which format of the if
command you use is up to you; we prefer the [
...]
format, so that's what we'll use throughout the remainder of the book.
test
has an assortment of operators for performing integer comparisons. Table 8.2 summarizes these operators.
For example, the operator -eq
tests to see whether two integers are equal. So if you had a shell variable called count
and you wanted to see whether its value was equal to zero, you would write
[ "$count" -eq 0 ]
Other integer operators behave similarly, so
[ "$choice" -lt 5 ]
tests to see whether the variable choice
is less than 5; the command
[ "$index" -ne "$max" ]
tests to see whether the value of index
is not equal to the value of max
; and, finally
[ "$#" -ne 0 ]
tests to see whether the number of arguments passed to the command is not equal to zero.
The test
command interprets the value as an integer when an integer operator is used, and not the shell, so these comparisons work regardless of the shell variable's type.
Let's reinforce the difference between test
's string and integer operators by taking a look at a few examples.
$ x1="005" $ x2=" 10" $ [ "$x1" = 5 ] String comparison $ echo $? 1 False $ [ "$x1" -eq 5 ] Integer comparison $ echo $? 0 True $ [ "$x2" = 10 ] String comparison $ echo $? 1 False $ [ "$x2" -eq 10 ] Integer comparison $ echo $? 0 True $
The first test
[ "$x1" = 5 ]
uses the string comparison operator =
to test whether the two strings are identical. They're not, because the first string is composed of the three characters 005
, and the second the single character 5
.
In the second test, the integer comparison operator -eq
is used. Treating the two values as integers, 005
is equal to 5
, as verified by the exit status returned by test
.
The third and fourth tests are similar, only in this case you can see how even a leading space stored in the variable x2
can influence a test made with a string operator versus one made with an integer operator.
Virtually every shell program deals with one or more files. For this reason, a wide assortment of operators is provided by test
to enable you to ask various questions about files. Each of these operators is unary in nature, meaning that they expect a single argument to follow. In all cases, this argument is the name of a file (and that includes a directory file, of course).
Table 8.3 lists the commonly used file operators.
The command
[ -f /users/steve/phonebook ]
tests whether the file /users/steve/phonebook
exists and is an ordinary file (that is, not a directory and not a special file).
[ -r /users/steve/phonebook ]
tests whether the indicated file exists and is also readable by you.
The command
[ -s /users/steve/phonebook ]
tests whether the indicated file contains at least one byte of information in it. This is useful, for example, if you create an error log file in your program and you want to see whether anything was written to it:
if [ -s $ERRFILE ] then echo "Errors found:" cat $ERRFILE fi
A few more test
operators, when combined with the previously described operators, enable you to make more complex types of tests.
The unary logical negation operator !
can be placed in front of any other test
expression to negate the result of the evaluation of that expression. For example,
[ ! -r /users/steve/phonebook ]
returns a zero exit status (true) if /users/steve/phonebook
is not readable; and
[ ! -f "$mailfile" ]
returns true if the file specified by $mailfile
does not exist or is not an ordinary file. Finally,
[ ! "$x1" = "$x2" ]
returns true if $x1
is not identical to $x2
and is obviously equivalent to
The operator -a
performs a logical AND of two expressions and returns true only if the two joined expressions are both true. So
[ -f "$mailfile" -a -r "$mailfile" ]
returns true if the file specified by $mailfile
is an ordinary file and is readable by you. An extra space was placed around the -a
operator to aid in the expression's readability and obviously has no effect on its execution.
The command
[ "$count" -ge 0 -a "$count" -lt 10 ]
will be true if the variable count
contains an integer value greater than or equal to zero but less than 10. The -a
operator has lower precedence than the integer comparison operators (and the string and file operators, for that matter), meaning that the preceding expression gets evaluated as
("$count" -ge 0) -a ("$count" -lt 10)
Incidentally, you can use parentheses in a test
expression to alter the order of evaluation; just make sure that the parentheses are quoted because they have a special meaning to the shell. So to translate the preceding example into a test
command, you would write
[ ( "$count" -ge 0 ) -a ( "$count" -lt 10 ) ]
As is typical, spaces must surround the parentheses because test
expects to see them as separate arguments.
The -o
operator is similar to the -a
operator, only it forms a logical OR of two expressions. That is, evaluation of the expression will be true if either the first expression is true or the second expression is true.
[ -n "$mailopt" -o -r $HOME/mailfile ]
This command will be true if the variable mailopt
is not null or if the file $HOME/mailfile
is readable by you.
The -o
operator has lower precedence than the -a
operator, meaning that the expression
"$a" -eq 0 -o "$b" -eq 2 -a "$c" -eq 10
gets evaluated by test
as
"$a" -eq 0 -o ("$b" -eq 2 -a "$c" -eq 10)
Naturally, you can use parentheses to change this order if necessary:
( "$a" -eq 0 -o "$b" -eq 2 ) -a "$c" -eq 10
You will see many uses of the test
command throughout the book. Table A.11 in Appendix A, “Shell Summary,” summarizes all available test
operators.
A construct known as the else
can be added to the if
command, with the general format as shown:
if commandt then command command ... else command command ... fi
Execution of this form of the command starts as before; commandt is executed and its exit status tested. If it's zero, the commands that follow between the then
and the else
are executed, and the commands between the else
and fi
are skipped. Otherwise, the exit status is nonzero and the commands between the then
and else
are skipped and the commands between the else
and fi
are executed. In either case, only one set of commands gets executed: the first set if the exit status is zero, and the second set if it's nonzero.
Let's now write a modified version of on
. Instead of printing nothing if the requested user is not logged on, we'll have the program print a message to that effect. Here is version 3 of the program:
$ cat on
#
# determine if someone is logged on -- version 3
#
user="$1"
if who | grep "^$user " > /dev/null
then
echo "$user is logged on"
else
echo "$user is not logged on"
fi
$
If the user specified as the first argument to on
is logged on, the grep
will succeed and the message $user is logged on
will be displayed; otherwise, the message $user is not logged on
will be displayed.
$ who Who's on? root console Jul 8 10:37 barney tty03 Ju1 8 12:38 fred tty04 Jul 8 13:40 joanne tty07 Jul 8 09:35 tony tty19 Jul 8 08:30 lulu tty23 Jul 8 09:55 $ on pat pat is not logged on $ on tony tony is logged on $
Another nice touch when writing shell programs is to make sure that the correct number of arguments is passed to the program. If an incorrect number is supplied, an error message to that effect can be displayed, together with information on the proper usage of the program.
$ cat on
#
# determine if someone is logged on -- version 4
#
#
# see if the correct number of arguments were supplied
#
if [ "$#" -ne 1 ]
then
echo "Incorrect number of arguments"
echo "Usage: on user"
else
user="$1"
if who | grep "^$user " > /dev/null
then
echo "$user is logged on"
else
echo "$user is not logged on"
fi
fi
$
Compare this program with the previous version and note the changes that were made. An additional if
command was added to test whether the correct number of arguments was supplied. If $#
is not equal to 1, the program prints two messages; otherwise, the commands after the else
clause are executed. These commands are the same as appeared in the last version of on
: They assign $1
to user
and then see whether user
is logged on, printing a message in either case. Note that two fi
s are required because two if
commands are used.
The indentation used goes a long way toward aiding the program's readability. Make sure that you get into the habit of setting and following indentation rules in your programs.
A built-in shell command called exit
enables you to immediately terminate execution of your shell program. The general format of this command is
exit n
where n is the exit status that you want returned. If none is specified, the exit status used is that of the last command executed before the exit
.
Be advised that executing the exit
command directly from your terminal will log you off the system because it will have the effect of terminating execution of your login shell.
exit
is frequently used as a convenient way to terminate execution of a shell program. For example, let's take another look at the rem
program, which removes an entry from the phonebook
file:
$ cat rem
#
# Remove someone from the phone book
#
grep -v "$1" phonebook > /tmp/phonebook
mv /tmp/phonebook phonebook
$
This program has the potential to do unintended things to the phonebook
file. For example, suppose that you type
rem Susan Topple
Here the shell will pass two arguments to rem
. The rem
program will end up removing all Susan
entries, as specified by $1
.
It's always best to take precautions with a potentially destructive program like rem
and to be certain as possible that the action intended by the user is consistent with the action that the program is taking.
One of the first checks that can be made in rem
is for the correct number of arguments, as was done before with the on
program. This time, we'll use the exit
command to terminate the program if the correct number of arguments isn't supplied:
$ cat rem # # Remove someone from the phone book -- version 2 # if [ "$#" -ne 1 ] then echo "Incorrect number of arguments." echo "Usage: rem name" exit 1 fi grep -v "$1" phonebook > /tmp/phonebook mv /tmp/phonebook phonebook $ rem Susan Goldberg Try it out Incorrect number of arguments. Usage: rem name $
The exit
command returns an exit status of 1, to signal “failure,” in case some other program wants to check it. How could you have written the preceding program with an if-else
instead of using the exit
(hint: look at the last version of on
)?
Whether you use the exit
or an if-else
is up to you. Sometimes the exit
is a more convenient way to get out of the program quickly, particularly if it's done early in the program.
As your programs become more complex, you may find yourself needing to write nested if
statements of the following form:
if command1 then command command ... else if command2 then command command ... else ... if commandn then command command ... else command command ... fi ... fi fi
This type of command sequence is useful when you need to make more than just a two-way decision as afforded by the if-else
construct. In this case, a multiway decision is made, with the last else
clause executed if none of the preceding conditions is satisfied.
As an example, suppose that you wanted to write a program called greetings
that would print a friendly “Good morning,” “Good afternoon,” or “Good evening” whenever you logged on to the system. For purposes of the example, consider any time from midnight to noon to be the morning, noon to 6:00 p.m. the afternoon, and 6:00 p.m. to midnight the evening.
To write this program, you have to find out what time it is. date
serves just fine for this purpose. Take another look at the output from this command:
$ date
Wed Aug 29 10:42:01 EDT 2002
$
The format of date
's output is fixed, a fact that you can use to your advantage when writing greetings
because this means that the time will always appear in character positions 12 through 19. Actually, for this program, you really only need the hour displayed in positions 12 and 13. So to get the hour from date
, you can write
Now the task of writing the greetings
program is straightforward:
$ cat greetings
#
# Program to print a greeting
#
hour=$(date | cut -c12-13)
if [ "$hour" -ge 0 -a "$hour" -le 11 ]
then
echo "Good morning"
else
if [ "$hour" -ge 12 -a "$hour" -le 17 ]
then
echo "Good afternoon"
else
echo "Good evening"
fi
fi
$
If hour
is greater than or equal to 0 (midnight) and less than or equal to 11 (up to 11:59:59), “Good morning” is displayed. If hour
is greater than or equal to 12 (noon) and less than or equal to 17 (up to 5:59:59 p.m.), “Good afternoon” is displayed. If neither of the preceding two conditions is satisfied, “Good evening” is displayed.
$ greetings
Good morning
$
As noted, the nested if
command sequence used in greetings
is so common that a special elif
construct is available to more easily express this sequence. The general format of this construct is
if commandl then command command ... elif command2 then command command ... elif commandn then command command ... else command command ... fi
command1, command2, ..., commandn are executed in turn and their exit statuses tested. As soon as one returns an exit status of zero, the commands listed after the then
that follows are executed up to another elif
, else
, or fi
. If none of the commands returns a zero exit status, the commands listed after the optional else
are executed.
You could rewrite the greetings
program using this new format as shown:
$ cat greetings
#
# Program to print a greeting -- version 2
#
hour=$(date | cut -c12-13)
if [ "$hour" -ge 0 -a "$hour" -le 11 ]
then
echo "Good morning"
elif [ "$hour" -ge 12 -a "$hour" -le 17 ]
then
echo "Good afternoon"
else
echo "Good evening"
fi
$
This version is easier to read, and it doesn't have the tendency to disappear off the right margin due to excessive indentation. Incidentally, you should note that date
provides a wide assortment of options. One of these, %H
, can be used to get the hour directly from date:
$ date +%H
10
$
As an exercise, you should change greetings
to make use of this fact.
Another way to add some robustness to the rem
program would be to check the number of entries that matched before doing the removal. If there's more than one match, you could issue a message to the effect and then terminate execution of the program. But how do you determine the number of matching entries? One approach is to do a normal grep
on the phonebook
file and then count the number of matches that come out with wc
. If the number of matches is greater than one, the appropriate message can be issued.
$ cat rem
#
# Remove someone from the phone book -- version 3
#
if [ "$#" -ne 1 ]
then
echo "Incorrect number of arguments."
echo "Usage: rem name"
exit 1
fi
name=$1
#
# Find number of matching entries
#
matches=$(grep "$name" phonebook | wc –l)
#
# If more than one match, issue message, else remove it
#
if [ "$matches" -gt 1 ]
then
echo "More than one match; please qualify further"
elif [ "$matches" -eq 1 ]
then
grep -v "$name" phonebook > /tmp/phonebook
mv /tmp/phonebook phonebook
else
echo "I couldn't find $name in the phone book"
fi
$
The positional parameter $1
is assigned to the variable name
after the number of arguments check is performed to add readability to the program. Subsequently using $name
is a lot clearer than using $1
.
The if
...elif
...else
command first checks to see whether the number of matches is greater than one. If it is, the “More than one match” message is printed. If it's not, a test is made to see whether the number of matches is equal to one. If it is, the entry is removed from the phone book. If it's not, the number of matches must be zero, in which case a message is displayed to alert the user of this fact.
Note that the grep
command is used twice in this program: first to determine the number of matches and then with the -v
option to remove the single matching entry.
Here are some sample runs of the third version of rem
:
$ rem Incorrect number of arguments. Usage: rem name $ rem Susan More than one match; please qualify further $ rem 'Susan Topple' $ rem 'Susan Topple' I couldn't find Susan Topple in the phone book She's history $
Now you have a fairly robust rem
program: It checks for the correct number of arguments, printing the proper usage if the correct number isn't supplied; it also checks to make sure that precisely one entry is removed from the phonebook
file.
The case
command allows you to compare a single value against other values and to execute one or more commands when a match is found. The general format of this command is
case value in pat1) command command ... command;; pat2) command command ... command;; ... patn) command command ... command;; esac
The word value is successively compared against the values pat1, pat2, ..., patn, until a match is found. When a match is found, the commands listed after the matching value, up to the double semicolons, are executed. After the double semicolons are reached, execution of the case
is terminated. If a match is not found, none of the commands listed in the case is executed.
As an example of the use of the case
, the following program called number
takes a single digit and translates it to its English equivalent:
$ cat number
#
# Translate a digit to English
#
if [ "$#" -ne 1 ]
then
echo "Usage: number digit"
exit 1
fi
case "$1"
in
0) echo zero;;
1) echo one;;
2) echo two;;
3) echo three;;
4) echo four;;
5) echo five;;
6) echo six;;
7) echo seven;;
8) echo eight;;
9) echo nine;;
esac
$
Now to test it:
$ number 0 zero $ number 3 three $ number Try no arguments Usage: number digit $ number 17 Try a two-digit number $
The last case shows what happens when you type in more than one digit: $1
doesn't match any of the values listed in the case
, so none of the echo
commands is executed.
The shell lets you use the same special characters for specifying the patterns in a case as you can with filename substitution. That is, ?
can be used to specify any single character; *
can be used to specify zero or more occurrences of any character; and [
...]
can be used to specify any single character enclosed between the brackets.
Because the pattern *
matches anything (just as when it's used for filename substitution it matches all the files in your directory), it's frequently used at the end of the case
as the “catchall” value. That is, if none of the previous values in the case match, this one is guaranteed to match. Here's a second version of the number
program that has such a catchall case.
$ cat number # # Translate a digit to English -- version 2 # if [ "$#" -ne 1 ] then echo "Usage: number digit" exit 1 fi case "$1" in 0) echo zero;; 1) echo one;; 2) echo two;; 3) echo three;; 4) echo four;; 5) echo five;; 6) echo six;; 7) echo seven;; 8) echo eight;; 9) echo nine;; *) echo "Bad argument; please specify a single digit";; esac $ number 9 nine $ number 99 Bad argument; please specify a single digit $
Here's another program called ctype
that prints the type of the single character given as an argument. Character types recognized are digits, uppercase letters, lowercase letters, and special characters (anything not in the first three categories). As an added check, the program makes sure that just a single character is given as the argument.
$ cat ctype # # Classify character given as argument # if [ $# -ne 1 ] then echo Usage: ctype char exit 1 fi # # Ensure that only one character was typed # char="$1" numchars=$(echo "$char" | wc –c) if [ "$numchars" -ne 1 ] then echo Please type a single character exit 1 fi # # Now classify it # case "$char" in [0-9] ) echo digit;; [a-z] ) echo lowercase letter;; [A-Z] ) echo uppercase letter;; * ) echo special character;; esac $
Some sample runs:
Something seems to be amiss. The counting portion of our program doesn't seem to be working properly. This seems like a good point to introduce the shell's -x
option. You can trace the execution of any program by typing sh -x
followed by the name of the program and its arguments. This starts up a new shell to execute the indicated program with the -x
option enabled. In this mode, commands are printed at the terminal as they are executed, preceded by a plus sign. Let's try it out.
$ sh -x ctype a Trace execution + [ 1 -ne 1 ] $# equals 1 + char=a Assignment of $1 to char + echo a + wc –c + numchars= 2 wc returned 2??? + [ 2 -ne 1 ] That's why this test succeeded + echo please type a single character please type a single character + exit 1 $
The trace output indicates that wc
returned 2 when
echo "$char" | wc -c
was executed. But why? There seemed to be only one character in wc
's input. The truth of the matter is that two characters were actually given to wc
: the single character a
and the “invisible” newline character that echo
automatically prints at the end of each line. So the program really should be testing for the number of characters equal to two: the character typed plus the newline added by echo
.
Go back to the ctype
program and replace the if
command that reads
if [ "$numchars" -ne 1 ] then echo Please type a single character exit 1 fi
with
if [ "$numchars" -ne 2 ] then echo Please type a single character exit 1 fi
and try it again.
$ ctype a lowercase letter $ ctype abc Please type a single character $ ctype 9 digit $ ctype K uppercase letter $ ctype : special character $ ctype Usage: ctype char $
Now it seems to work just fine. (What do you think happens if you use ctype *
without enclosing the *
in quotes?)
In Chapter 12, “More on Parameters,” you'll learn how you can turn this trace feature on and off at will from inside your program.
Before leaving the ctype
program, here's a version that avoids the use of wc
and handles everything with the case
:
$ cat ctype
#
# Classify character given as argument -- version 2
#
if [ $# -ne 1 ]
then
echo Usage: ctype char
exit 1
fi
#
# Now classify char, making sure only one was typed
#
char=$1
case "$char"
in
[0-9] ) echo digit;;
[a-z] ) echo lowercase letter;;
[A-Z] ) echo uppercase letter;;
? ) echo special character;;
* ) echo Please type a single character;;
esac
$
The ?
matches any single character. If this pattern is matched, the character is a special character. If this pattern isn't matched, more than one character was typed, so the catchall case is executed to print the message.
The symbol |
has the effect of a logical OR when used between two patterns. That is, the pattern
pat1 | pat2
specifies that either pat1 or pat2 is to be matched. For example,
-l | -list
matches either the value -l
or -list
, and
dmd | 5620 | tty5620
matches either dmd
or 5620
or tty5620
.
The greetings
program that you saw earlier in this chapter can be rewritten to use a case
statement rather than the if-elif
. Here is such a version of the program. This time, we took advantage of the fact that date
with the +%H
option writes a two-digit hour to standard output.
$ cat greetings
#
# Program to print a greeting -- case version
#
hour=$(date +%H)
case "$hour"
in
0? | 1[01] ) echo "Good morning";;
1[2-7] ) echo "Good afternoon";;
* ) echo "Good evening";;
esac
$
The two-digit hour obtained from date
is assigned to the shell variable hour
. Then the case
statement is executed. The value of hour
is compared against the first pattern:
0? | 1[01]
which matches any value that starts with a zero followed by any character (midnight through 9:00 a.m.), or any value that starts with a one and is followed by a zero or one (10:00 or 11:00 a.m.).
The second pattern
1[2-7]
matches a value that starts with a one and is followed by any one of the digits two through seven (noon through 5:00 p.m.).
The last case, the catchall, matches anything else (6:00 p.m. through 11:00 p.m.).
This seems about as good a time as any to talk about the shell's built-in null command. The format of this command is simply
:
and the purpose of it is—you guessed it—to do nothing. So what good is it? Well, in most cases it's used to satisfy the requirement that a command appear, particularly in if
commands. Suppose that you want to make sure that the value stored in the variable system
exists in the file /users/steve/mail/systems
, and if it doesn't, you want to issue an error message and exit from the program. So you start by writing something like
if grep "^$system" /users/steve/mail/systems > /dev/null then
but you don't know what to write after the then
because you want to test for the nonexistence of the system in the file and don't want to do anything special if the grep
succeeds. Unfortunately, the shell requires that you write a command after the then
. Here's where the null command comes to the rescue:
if grep "^$system" /users/steve/mail/systems > /dev/null then : else echo "$system is not a valid system" exit 1 fi
So if the system is valid, nothing is done. If it's not valid, the error message is issued and the program exited.
Remember this simple command when these types of situations arise.
The shell has two special constructs that enable you to execute a command based on whether the preceding command succeeds or fails. In case you think this sounds similar to the if
command, well it is. It's sort of a shorthand form of the if
.
If you write
commandl && command2
anywhere where the shell expects to see a command, commandl will be executed, and if it returns an exit status of zero, command2 will be executed. If commandl returns an exit status of nonzero, command2 gets skipped.
For example, if you write
sort bigdata > /tmp/sortout && mv /tmp/sortout bigdata
then the mv
command will be executed only if the sort
is successful. Note that this is equivalent to writing
if sort bigdata > /tmp/sortout then mv /tmp/sortout bigdata fi
The command
[ -z "$EDITOR" ] && EDITOR=/bin/ed
tests the value of the variable EDITOR
. If it's null, /bin/ed
is assigned to it.
The ||
construct works similarly, except that the second command gets executed only if the exit status of the first is nonzero. So if you write
grep "$name" phonebook || echo "Couldn't find $name"
the echo
command will get executed only if the grep
fails (that is, if it can't find $name
in phonebook
, or if it can't open the file phonebook
). In this case, the equivalent if
command would look like
if grep "$name" phonebook then : else echo "Couldn't find $name" fi
You can write a pipeline on either the left- or right-hand sides of these constructs. On the left, the exit status tested is that of the last command in the pipeline; thus
who | grep "^$name " > /dev/null || echo "$name's not logged on"
causes execution of the echo
if the grep
fails.
The &&
and ||
can also be combined on the same command line:
who | grep "^$name " > /dev/null && echo "$name's not logged on" || echo "$name is logged on"
(Recall that when is used at the end of the line, it signals line continuation to the shell.) The first
echo
gets executed if the grep
succeeds; the second if it fails.
These constructs are also often used in if
commands:
if validsys "$sys" && timeok then sendmail "$user@$sys" < $message fi
If validsys
returns an exit status of zero, timeok
is executed. The exit status from this program is then tested for the if
. If it's zero, then the sendmail
program is executed. If validsys
returns a nonzero exit status, timeok
is not executed, and this is used as the exit status that is tested by the if
. In that case, sendmail
won't be executed.
The use of the &&
operator in the preceding case is like a “logical AND”; both programs must return an exit status of zero for the sendmail
program to be executed. In fact, you could have even written the preceding if
as
validsys "$sys" && timeok && sendmail "$user@$sys" < $message
When the ||
is used in an if
, the effect is like a “logical OR”:
if endofmonth || specialrequest then sendreports fi
If endofmonth
returns a zero exit status, sendreports
is executed; otherwise, specialrequest
is executed and if its exit status is zero, sendreports
is executed. The net effect is that sendreports
is executed if endofmonth
or specialrequest
return an exit status of zero.
In Chapter 9, “'Round and 'Round She Goes,” you'll learn about how to write loops in your programs. However, before proceeding to that chapter, try the exercises that follow.
1: | Write a program called $ valid foo_bar yes $ valid 123 no $ (Hint: Define a regular expression for a valid variable name and then enlist the aid of |
2: | Write a program called $ date Wed Aug 28 19:34:01 EDT 2002 $ t 7:21 pm $ Use the shell's built-in integer arithmetic to convert from 24-hour clock time. Then rewrite the program to use a |
3: | Write a program called mysed '1,10d' text will use |
4: | Write a program called $ isyes yes $ echo $? 0 $ isyes no $ echo $? 1 $ Write the program using an |
5: | Use the |
3.143.5.201