5. Branching

  1. Testing Conditional Statements
  2. If Statements
  3. Case Statements

5.1 Testing Conditional Statements

Decisions, decisions, decisions.  One of the corner stones of every programming language is the ability to make decisions.  A decision can only be made based upon a proposition.  In other words, if a proposition is true or false, then take appropriate action.  Learning to evaluate a proposition in the shell requires mastery of the test command.  Taking action based upon the proposition's result is discussed in the subsections and chapters that follow.

The built-in command test evaluates logical expressions.  It takes boolean statments as its argument and returns a value of zero or one meaning true and false, respectively.  Looking at the statements below, for example, test can be used to compare the values of two constants.

$ test 100 -eq 100
$ echo $?
0
$ test 100 -eq 50
$ echo $?
1

In the first case, the user checks if 100 is equal to 100.  Note that test does not actually return the value directly to the user.  Instead the user pulls the result from the $? special parameter.  As can be seen, the constants are indeed identical since 0 was returned.  The second case shows test returning a false value.  Here the constants 100 and 50 are not equal so test returns 1.

There is a shortened form of the test command. Enclosing an expression in square brackets, [], also tests the statement.  To use them, place the expression between the brackets. Be certain to include white space between the brackets and the statement.

$ [ ${HOME} = /home/rsayle ]
$ echo $?
0

The white space is mandatory for the shell to properly interpret the expression.  Notice how the shell will misread the test condition as an executable file if the space is missing between the opening bracket and the first operand:

$ [${HOME} = /home/rsayle ]
sh: [/home/rsayle: No such file or directory

Also notice that it does not find the test's termination point without the space between the final operand and the closing bracket:

$ [ ${HOME} = /home/rsayle]
[: missing `]'

Always prefer brackets over the test command; it makes scripts easier to read.

There are a number of operators that allow the formation of logical expressions for testing. Being that the shell itself operates primarily on strings, it follows, naturally, that there exist string operators. The shell can test a string to see if it is empty or non-empty or it can compare two strings for equivalence.
 
 

Table 4.1-1. String Operators
Construct
Meaning
string
string is not null
-n string
string is not null
-z string
string is null
string1 = string2
string1 is identical to string2
string1 != string2
string1 is not identical to string2
 
So here are some farily simple examples that demonstrate these operators.

$ [ "hello" = "world" ]
$ echo $?
1
$ ASTRING="foo"
$ [ ${ASTRING} ]
$ echo $?
0
$ NULLSTRING=""
$ [ ${NULLSTRING} ]
$ echo $?
1
$ [ -n "${NULLSTRING}" ]
$ echo $?
1
$ [ -z "${NULLSTRING}" ]
$ echo $?
0
$ [ -z ${NULLSTRING} ]
test: argument expected

The first test checks the equivalence of the strings hello and world using the = operator.  The result shows that the strings are not the same.  Then, the operator assigns a variable, ASTRING, the value foo.  The user encloses the value in test brackets and retrieves the result.  This construct tests the string for emptiness.  If it has no value or is an empty string, denoted by "", test returns false.  As long as it has some value, the string is considered non-empty, and test returns true.  So testing ASTRING yields 0.  On the other hand, the operator then declares an empty string, NULLSTRING, and checks it.  This time test returns false. Now compare this to using -n which returns true if a string is not empty.  The -n operator gives the same result, false, as shown by the fourth test.  And just for fun, the user follows this by demonstrating the -z operator.  Here, -z reports a true result because NULLSTRING in fact an empty string.

Finally, notice the potential problem of testing strings.  In the last command, the user passes the variable's value unquoted to the operator.  Because the variable really has no value, the shell passes no argument to test.  This results in an error.  It is extremely important to get in the habit of quoting string variables.  Novice scripters often assign values to variables by storing the result of a command.  The variable's value is then dereferenced and parsed.  If the command yields no value, then the variable is null.  Any tests or commands performed on the null value result in an error and program termination.  By quoting the variable during its dereference the shell treats it as an empty string instead of nothing at all, and in most cases, the program does not crash.

Testing strings alone would make for a fairly inflexible system.  The shell does contain facilities for testing other conditions.  In fact, it can perform some routine integer comparisons; namely, the shell supports:

These operators function the same as in any other programming language.  They compare the left operand to the right operand and return a true or false value.  As a quick example, here is a comparison between the variable TEN, whose value is 10, with various other values.

$ TEN=10
$ [ ${TEN} -eq 10 ]
$ echo $?
0
$ [ ${TEN} -gt 10 ]
$ echo $?
1
$ ONEHUNDRED=100
$ [ ${TEN} -le ${ONEHUNDRED} ]
$ echo $?
0
 
In addition to string and integral comparisons, the shell supports a vast array of file operators.  One can test for the existence of a file, if the file is readable, writeable, or even executable, and for the file type.  The full list of file checking capabilities is given in the table below.  It should be noted that having built-in file testing is quite a natural extension of the shell.  After all, the basic unit within UNIX is the file.  Data, directories, hardware interfaces, I/O devices, and even network communications are represented by files.  Because of this, the ability to manipulate files is key to proper system operation.  This book has already discussed some file handling techniques including I/O redirection and the representation of scripts in the system.  It now adds the built-in file testing facilities.
 
 

Table 4.1-2. File Operators
Operator
Returns true if...
-b
file is a block special file
-c
file is a character special file
-d
file is a directory
-f
file is an ordinary file
-g
file has its set group ID (SGID) bit set
-k
file has its sticky bit set
-p
file is a named pipe
-r
file is readable by the current process
-s
file has a non-zero length
-t
file descriptor is open and associated with a terminal
-u
file has its set user id (SUID) bit set
-w
file is writable by the current process
-x
file is executable by the current process
All of the file operators shown are unary.  They only take a single file as an argument.  To test multiple conditions on a file, a scripter must string together a list of expressions using the logical conjuctions, which are discussed below.  Also, to negate an operator, simply precede the expression with an exclamation point.

$ ls -l
total 42
drwxr-xr-x   2 rsayle   users        5120 Apr 11 17:13 complete/
-rwxr-xr-x   1 rsayle   users         313 Apr 11 17:13 cpusers*
-rw-r--r--   1 rsayle   users        1244 Apr 11 17:13 goad-s.txt
-rw-r--r--   1 rsayle   users          66 Apr 11 17:13 helco3.txt
-rw-r--r--   1 rsayle   users         103 Apr 11 17:13 helco3tx.txt
-rw-r--r--   1 rsayle   users        1246 Apr 11 17:13 lawrenc-c.txt
-rw-r--r--   1 rsayle   users        1243 Apr 11 17:13 lien-v.txt
-rw-r--r--   1 rsayle   users        1244 Apr 11 17:13 magro-j.txt
-rw-r--r--   1 rsayle   users        1244 Apr 11 17:13 mergard-j.txt
-rwxr-xr-x   1 rsayle   users        7048 Apr 11 17:13 mkconfig*
-rw-r--r--   1 rsayle   users        1244 Apr 11 17:13 mynes-c.txt
-rw-r--r--   1 rsayle   users        1243 Apr 11 17:13 nair-h.txt
-rw-r--r--   1 rsayle   users        1244 Apr 11 17:13 pricket-j.txt
-rw-r--r--   1 rsayle   users        1241 Apr 11 17:13 riely-c.txt
-rwxr-xr-x   1 rsayle   users        1444 Apr 11 17:13 setamdroute*
-rw-r--r--   1 rsayle   users        1244 Apr 11 17:13 short-j.txt
-rw-r--r--   1 rsayle   users        1244 Apr 11 17:13 spilo-d.txt
-rw-r--r--   1 rsayle   users         334 Apr 11 17:13 users
-rw-r--r--   1 rsayle   users        1243 Apr 11 17:13 zein-a.txt
$ [ -d complete ]
$ echo $?
0
$ [ -d cpusers ]
$ echo $?
1
$ [ -x mkconfig ]
$ echo $?
0
$ [ -f users ]
$ echo $?
0
$ [ ! -c users ]
$ echo $?
0

The previous example demonstrates the usage of the file operators.  It begins with a detailed listing of the current directory.  Comparing this to the tests shows how a few of the operators work.  The first test checks complete to see if it is a directory.  Looking at the file listing and then checking the returned result shows that it is indeed a directory; however, the script cpusers is not a directory as the next test proves.  Next, the example shows that the mkconfig script is executable by using -x.  Then the users file is first shown to be a plain text file with -f.  The example ends with an demonstration of how to take the complement of a file operator.  Notice that by entering ! -c (read "not -c") the test verifies that users is not a character special device.  This is a true statement as both the result and the preceding test on users show.

Now in order to build more meaningful test conditions, it may be necessary to join two or more expressions.  The shell achieves this by providing logical AND, -a, and OR, -o.  Once again, the usual rules of logic taught in math classes around the globe apply.  In the case of -a, both conditions surrounding the conjunction must be true for test to return a true value.  For -o, either expression may be true for test to return true. To use -a or -o, simply place the conjunction between the boolean statements.  Returning to the previous example of comparing files:

$ [ -f setamdroute -a -x setamdroute ]
$ echo $?
0
$ [ -c users -o -d complete ]
$ echo $?
0
$ [ -c users -a -d complete ]
$ echo $?
1

Note that any combination of file, integral, or string comparisons can be joined by -a and -o, but for demonstration, this example sticks to testing files.  So the first test checks that setamdroute is an ordinary text file and that it is executable.  Since both statements are true, the result is true.  The example continues by verifying that either users is a character special file, which it is not, or that complete is a directory.  Since complete is a directory, test returns true.  Finally, the test now changes to an AND expression to show that both conditions must be true; otherwise, test returns false.

As a final discussion of test, a good scripter must know how to force a desired condition.  Namely, set the value of the expression so that the test always returns true or false as desired.  Most UNIX machines have two commands that do exactly this.  They are aptly named true and false.  Whenever a condition should be forced, these commands can be substituted in place of an actual test.  For example, a system administrator might want to monitor a printing queue.  Being a master scripter, said administrator would initiate a while loop that calls the lpq command every second so that he can watch for jobs.  In order to keep the while loop running, he passes the true command to the loop.

$ while true
> do
> lpq -Plp; sleep 1
> done
no entries
no entries
no entries
^C

Another method of forcing a condition is to use the null command. Represented by a colon (:), the null command is a do nothing statement that the always returns zero.  It can be used in place of a test condition like true and false.  It may also be used as an empty statement where the shell expects something, but the user does not necessarily want any processing to occur.  This could be useful, for example, during development of a branch when a user might just want to test the condition before determining the command block that executes.

$ while :
> do
> lpq -Plp; sleep 1
> done
no entries
no entries
no entries
^C

So there is the same print queue monitoring script entered by the now lazy system administrator who figures that saving the extra few key strokes means a shorter trip to carpal-tunnel hell.  Actually, this is a very common technique.  It may not be as readable as the word true, but it gets the job done.  And here is an example of where a scripter might not yet know what command set should be executed after a certain test is satisfied.

if [ "${USER}" = "root" ]; then
  :
else
  echo "`basename ${0}`: you must be root to run this program."
  exit 1
fi

The if statement makes sure that the user is the root user.  The scripter may not know exactly what steps to take once this is verified, but for quick testing, it is left blank with a :.  With this in place, the user can actually run the script to ensure that if the user is not root, then the program exits with an error.


5.2 If Statements

Now that it has been shown how to make decisions, it is time to learn how to act based upon its result.  This is called branching.  The first branching method uses the if-then-elif-else-fi construct.  Read elif as "else if;" it makes more sense.  An if statement reads similarly to classical logic.  If something is true, then do the following, or if another statement is true, then do this instead; otherwise, just do this.  Sentences that can be written in this fashion can be easily translated into an if block.

The if-then-elif-else-fi block has the following syntax:

if [ condition ]; then
  command1
     :
  commandn
elif [ condition ]; then
  command1
     :
  commandn
:
:
elif [ condition ]; then
  command1
     :
  commandn
else
  command1
     :
  commandn
fi

This construct may appear to be long-winded, but it is actually pretty simple to analyze.  The ifs and elifs are pretty much the same.  An if or elif is followed by a test.  Whenever the test evaluates to true, the shell executes the commands that follow and then continues with the rest of the script after the block's end point, the fi.  The else statement is a default action.  Given that neither of the if or elifs pass, the shell performs the commands within the else block, and the script again continues execution after the fi.

Usage of elifs and elses in an if statement are optional.  As a bare minimum, an if block must utilize an if-then-fi construct.  Here is a popular example:

if [ "${USER}" != "root" ]; then
   echo "Sorry, ${USER}, you must be the superuser to run this program."
   exit 1
fi
echo "Executing program."

This simple little subroutine checks if the current operator is the root user.  If not, then it prints a message indicating that the script may only be executed by those with root privileges and promptly terminates.  In the event that the operator is logged in as root, the script moves to the fi and then prints a friendly message indicating that it is continuing execution.

A more advanced example might look like:

if [ -x ${FILE} ]; then
  ${FILE}
elif [ -w ${FILE} ]; then
  vi ${FILE}
else
  echo "${FILE} is neither executable nor writable; don't know what to do!"
  exit 1
fi

Given a file, if the file is executable, the first part of the block causes the shell to run the program.  But if it is not executable, the block continues by testing it for readability.  If it turns out FILE is writable, it loads it into the vi editor.  Should both tests fail, the block writes an error message and terminates the script.

If blocks can be nested.  In other words, it is perfectly legal to build if statements within ifs, elifs, or even else blocks. Nesting, however, should only be used as an absolute necessity.  Nested blocks are difficult to read which, consequently, makes them harder to debug.  Try to rewrite nested if blocks as a series of ifs and elifs that perhaps have more precise test conditions.


5.3 Case Statements

The second form of branching available is the case statement.  A case differs from an if block in that it branches based upon the value of one variable.  An if does not have such restrictions because it uses the test function, and a test can be any string of logical expressions as has been shown.  The best way to illustrate this difference is to examine the syntax:

case (variable) in
   pattern1)
      command1
         :
      commandn
      ;;
   :
   patternn)
      command1
         :
      commandn
      ;;
esac

The case block takes as its argument a variable.  To denote the variable, it must be surrounded by parentheses.  The case compares the variable's value against each pattern.  The pattern may be any legal regular expression.  If the variable's value matches the pattern, then the shell executes the command block immediately following the pattern.  The command block terminates with a pair of double semi-colons.  As soon as it reaches them, the shell continues past the end of the case block as denoted by the esac.

A good use for a case statement is the evaluation of command line arguments:

option="${1}"
case ${option} in
   -f) FILE="${2}"
       ;;
   -d) DIR="${2}"
       ;;
   *)  echo "`basename ${0}`: usage: [ -f filename ] | [ -d directory ]"
       exit 1
       ;;
esac

In this example, the script expects two arguments.  The first is one of two option indicators, -f or -d, that tell the program whether the second argument is a file or a directory.  The case statement switches upon the first argument's value stored in option.  If option equals -f, then it assigns the option's argument, the second positional parameter, to FILE.  Then the scripts continues after the esac.  On the other hand, if option is -d, the shell assigns the second argument's value to DIR and continues execution.  Finally, this case statement has a default action.  In the event the user does not use either the -f or -d options, it detects the error with the wild card, *, which matches any pattern.  The script prints a message instructing the user how to use it properly, and then exits.

Remember that a case switches against patterns.  Programmers who can build regular expressions with ease should have no problem designing effective case blocks.