Chapter 8. Decisions, Decisions

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.

Exit Status

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 $? Variable

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.

The test Command

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.

String Operators

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.

test $name = julio with name null.

Figure 8.1. test $name = julio with name null.

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).

test “$name” = julio with name null.

Figure 8.2. test “$name” = julio with name null.

Other operators can be used to test character strings. These operators are summarized in Table 8.1.

Table 8.1. test String Operators

Operator

Returns TRUE (exit status of 0) if

string1 = string2

string1 is identical to string2.

string1 != string2

string1 is not identical to string2.

string

string is not null.

-n string

string is not null (and string must be seen by test).

-z string

string is null (and string must be seen by test).

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 mondayincluding 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.

So the command

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.

An Alternative Format for test

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.

Integer Operators

test has an assortment of operators for performing integer comparisons. Table 8.2 summarizes these operators.

Table 8.2. test Integer Operators

Operator

Returns TRUE (exit status of 0) if

int1 -eq int2

int1 is equal to int2.

int1 -ge int2

int1 is greater than or equal to int2.

int1 -gt int2

int1 is greater than int2.

int1 -le int2

int1 is less than or equal to int2.

int1 -lt int2

int1 is less than int2.

int1 -ne int2

int1 is not equal to int2.

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.

File Operators

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.

Table 8.3. Commonly Used test File Operators

Operator

Returns TRUE (exit status of 0) if

-d file

file is a directory.

-e file

file exists.

-f file

file is an ordinary file.

-r file

file is readable by the process.

-s file

file has nonzero length.

-w file

file is writable by the process.

-x file

file is executable.

-L file

file is a symbolic link.

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).

The command

[ -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 Logical Negation Operator !

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

[ "$x1" != "$x2" ]

The Logical AND Operator –a

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)

as you would expect.

Parentheses

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 Logical OR Operator –o

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.

The else Construct

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 fis 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.

$ on                                 No arguments
Incorrect number of arguments
Usage:  on user
$ on priscilla                       One argument
priscilla is not logged on
$ on jo anne                         Two arguments
Incorrect number of arguments
Usage:  on user
$

The exit Command

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.

A Second Look at the rem Program

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.

The elif Construct

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

$ date | cut -c12-13
10
$

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.

Yet Another Version of rem

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

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.

Special Pattern Matching Characters

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:

$ ctype a
Please type a single character
$ ctype 7
Please type a single character
$

The -x Option for Debugging Programs

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.

$ ctype u
lowercase letter
$ ctype '>'
special character
$ ctype xx
Please type a single character
$

Back to the case

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.).

$ date
Wed Aug 28 15:45:12 EDT 2002
$ greetings
Good afternoon
$

The Null Command :

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 && and || Constructs

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.

Exercises

1:

Write a program called valid that prints “yes” if its argument is a valid shell variable name and “no” otherwise:

$ valid foo_bar
yes
$ valid 123
no
$

(Hint: Define a regular expression for a valid variable name and then enlist the aid of grep or sed.)

2:

Write a program called t that displays the time of day in a.m. or p.m. notation rather than in 24-hour clock time. Here's an example showing t run at night:

$ 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 case command instead. Rewrite it again to perform arithmetic with the expr command.

3:

Write a program called mysed that applies the sed script given as the first argument against the file given as the second. If the sed succeeds (that is, exit status of zero), replace the original file with the modified one. So

mysed '1,10d' text

will use sed to delete the first 10 lines from text, and, if successful, will replace text with the modified file.

4:

Write a program called isyes that returns an exit status of 0 if its argument is “yes,” and 1 otherwise. For purposes of this exercise, consider y, yes, Yes, YES, and Y all to be valid “yes” arguments:

$ isyes yes
$ echo $?
0
$ isyes no
$ echo $?
1
$

Write the program using an if command and then rewrite it using a case command. This program can be useful when reading yes/no responses from the terminal (which you'll learn about in Chapter 10, “Reading and Printing Data”).

5:

Use the date and who commands to write a program called conntime that prints the number of hours and minutes that a user has been logged on to the system (assume that this is less than 24 hours).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.65.61