next up previous contents
Next: Signal Handling Up: C Shell Programming Previous: C Shell Programming

Testing and Branching

One of the main features of any programming language is decision making. When writing a program there will be a time when the program must examine some data and determine by preprogrammed conditions what to do. Some decisions that might be encountered in a shell script are: if an end of file (EOF) marker is reached, proceed to the next file; if the file is write protected, display an error message to the user; or if a number n is greater than another number m, terminate script. Each of these statements has the form if [some condition] then [do something] which is the general form of a branch statement in the C shell. The shell will examine the condition and decide what to do based on the result of the examination, or test. The actual form of the if statement is as follows:

if [condition_1]
   command list
else if [condition_2]
   command list 
else if [condition_3]
   command list
else if [condition_n]
   command list
   default command list

When a test of a condition returns a TRUE value (non-zero) the commands listed immediately following the condition are executed and the construct is exited with command execution beginning after the endif statement. If a test returns a FALSE value (zero) the next else or else if (if either are present) is executed. The else statement contains the last command list and acts as a default situation - if all else fails, execute these commands. The else statements are optional and may be omitted if a single branch test is all that is required. The minimum if test would then be if [condition] command list endif. The endif is required any time an if statement is used. Every time a command is executed, an integer value is returned depending on the success of the command execution. If the command executes as desired without incident, a zero is returned. For example, the grep command can be used to see if a user is logged onto the system by piping the output of a who command to it, such as

% who | grep jblow
jblow     tty1    Dec 22 2:42
% echo $?

The command demonstrated that jblow was logged onto the system, but more importantly, so did the return value of zero. Had jblow not have been on the system, the who grep command would have returned a non-zero value as the search would have failed. This fact gives a new way of testing command success in shell scripts. The above commands could have been placed in a C shell script like the following:

# usron checks to see if the userid given as an argument
# is logged on to the system
# Usage: usron id_name
who | grep $1 > /dev/null
if (! $status) then 
   echo ``$1 is logged on''
   echo ``$1 is not logged on''

This script may look a bit more complicated than the last example, but there is not much new here. The test checks the return value of the pipeline and returns a string depending on which case is true. The $status holds the return value of the last executed command. It is the same as the $? in the Bourne shell. It is important to notice a subtle, yet important, detail here. The if condition will execute the command list if the expression inside the parentheses has a non-zero value. The $status variable returns a zero if the last command executed properly (or in the case of the grep command, returned a match), and a one otherwise. While this may well seem backwards relative to the if condition evaluation, it can be easily handled by placing a (!) before the test. The $1 refers to the first argument given on the command line after the script name which allows the user to use the script to check on any user without having to alter the script. The only new idea in this script is the redirection of the output of the pipeline to /dev/null. The null device (/dev/null) is really just a place to dump unwanted output. Anything that is sent to the null device vanishes without a trace. It is important to realize that nothing can be recovered from this device so care must be taken when redirecting anything there. If the output of the pipeline had not been sent to the null device, it would have been sent by default to the stdout (the screen) and make an ugly output. When designing and testing a script it is nice to send the output to either to the screen or a file for examination of any problems that might arise.

The above example also illustrates what we consider to be good form in script documentation. Documentation is an important aspect of any programming task, and Unix script programming is no exception. The example shows two important documentation features, a description of the script and a line describing its usage such as arguments and or options. Since this script was relatively short, further documentation is not necessary, but more would likely be included in longer more complicated scripts.

When writing scripts to handle system related tasks, it is often necessary to have the programs make decisions based on file status. For instance, a file might be checked to determine if it is a file before sending it to a printer, or a directory might be checked for set write-access bits. The shell provides a method for testing file attributes. If one of the flags (or operators) in table 4.6 is placed before a filename in an if construct test, a boolean truth value causes the direction of the branch.

Table 4.6:   Operators that test file attributes.

For example, if a.out was executable:

% if (-x a.out) echo ``Yes''

would be the result of a test of executability. Of course tests such as these are much more useful in a script since any of the information attained in this manner from a command line could have been acquired more easily from a long listing (ls -l). The negation operator (!) can be used to negate the truth value of any test if it is placed before the test operator. Logical AND (&&) and OR (||) operators can also be used to make compound condition statements such as

# prok checks to see if filename is a file and user readable
# before attempting to print it
# Usage: prok filename
if (-f $1 && -r $1) then
   lpr -Php_printer $1
   echo ``Error:: $1 cannot be printed''

Comparing string patterns will often be a required task when shell programming in the Unix environment. The grep command is often useful when dealing with strings, but string comparisons can be built into the scripts themselves. A typical string comparison would look something like

if (``APPLE'' == ``ORANGE'') then
   echo Match
   echo No Match

which would result in the string No Match being echoed to the screen. There are four comparators for checking string equivalence in the C shell. The == and =~ check for right and left string equivalence, while the != and !~ check for non-equivalence. While all four can be used on strings, the =~ and !~ comparators match the string on the left against a filename substitution on the right. This makes it possible to use a simple if statement rather than the switch statement for pattern matches.

The C shell goes much further than the Bourne shell in its dealing with arithmetic operations. Arithmetic in the C shell is quite easy to implement. To give an integer value to a variable, the same procedure described in the variable section is used. For example, set X=10, would put the integer value 10 in the variable name X. To square X and place the value in a variable called SQR, the following expression would be used:

% set X=10
% @ SQR=$X * $X
% echo $SQR
% 100

The @ operator tells the C shell that what is about to follow is an arithmetic expression. Care must be taken to use whitespace when writing arithmetic expressions to prevent complaints from the shell. Table 4.7 is a list of the arithmetic operators (including comparisons) provided by the C shell:

Table 4.7:  Comparison operators in the C shell.

When evaluating arithmetic expressions, the C shell uses order of precedence. This means that A*B+C will have different meaning than A*(B+C) and the user will have to know what he wants to ensure that the proper parentheses are put into place.

Like the C programming language, the C shell provides a wide variety of assignment operators to simplify expressions. For example, adding 10 to the variable SUM would normally look like this

@ SUM = $SUM + 10

but it could also be written as follows

@ SUM += 10

which is more to the point although it requires a bit of getting use to for people who are not use to the C programming language. A complete list of arithmetic assignment operators is contained in table 4.8.

Table 4.8:  Arithmetic operators in the C shell.

The bitwise operators above are special operators that allow operations to be carried out on bit patterns (ones and zeros). For example the decimal number 7 can be written as the binary number 0111. The bitwise shift operators do just that, they shift the bit pattern in one direction or another. 12 >> 2 shifts the bit pattern for 12 two places to the right resulting in the decimal number 3. The actual binary shift would look like

 12    >> 2    3
1100          0011

3 << 2 would shift it back to 12 again. Bitwise shift to the right can be looked at as dividing a number by 2, while bitwise shifting it to the left could be viewed as multiplying it by 2.

The bitwise AND operation compares each bit (in terms of its place) in two patterns and results in a 1 where there were 1's in each and a 0 otherwise.

5 & 12 would result in 4       5  0101
                              12  1100
                          5 & 12  0100
8 The bitwise OR operation compares bits in the same fashion as the AND but results in a 0 only if no 1 was in either pattern.

5 | 7 would result in 7        5  0101
                               7  0111
                           5 | 7  0111

The bitwise compliment simply reverses the bit pattern.

^7 would give 8                7  0111
                              ^7  1000

While these would not likely come up often in day to day shell programming, they are worth a mention.

The if:then:else construct can be repeatedly nested to allow as many tests as are required for a particular situation, but this can become very convoluted. A more appropriate construct for dealing with many choices is the switch statement. The switch statement allows many choices to be examined for a particular situation, in a more structured manner than the nested if statement. The format of the switch statement is as follows:

switch (test_pattern)
case pattern1:
case pattern2:
case patternN:

which would check for a match between test_pattern and patterns 1 through N, executing the command list for the first match. If no match is found through the N'th pattern, the command list following the default label will be executed. The reader will notice that there is a breaksw following every command list, with the exception of the list following the default case. The reason for the breaksw is that once a match is made, and execution of the corresponding command list has completed, the command list for each successive case statement would then execute. A breaksw placed at the end of each case statement will then pass execution to after the endsw statement. Since the default case contains the last command list which could be executed within the case construct, no breaksw is required.

The following example would prompt the user prior to deleting a file:

echo ``Do you wish to delete $file ?''
set answer=$<
switch ($answer)
case ([yY]*):
   echo ``Removing $file''
   rm $file
case ([nN]*):
   echo ``$file was not removed''
   echo ``Didn't understand response :: Aborting operation''
   exit 1

This example could have been done relatively easily with nested
\verb+if+ statements, but if there were even one or two more choices
the \verb+if:then:else+ construct begins to lose its appeal.

\subsection{Loop Control}

Along with control branching, loop control gives programs their power.
Branching gives a program the ability to determine the direction
program execution will take and looping allows the program to
repetitiously execute one or more commands, until some condition is or
is not met.
This is after all what gives computers their power.
The C shell provides two tools for controlling program looping, the
while construct and the foreach construct.

A \verb+while+ loop can be best explained in plain English as follows:
while a certain condition is met, a group of commands will be repeatedly
As soon as the condition is not met, the execution ceases.
The actual form of a \verb+while+ statement is:

while (condition)

If the condition is never met, the command list is never executed, while on the other hand if the condition is always met, the execution will never stop. For example, the while loop in:

set A=1 B=10
while ($B < $A)
   echo $A
@  B -= 1

will never execute, while in:

set A=1 B=2
while ($B > $A)
   echo $B
@  B += 1

will execute forever (not really).

The while loop can be used for many things, but one very useful purpose is to handle arguments given on the command line issuing the script. The following script fragment will echo back to the screen the arguments given:

while ($#argv)
  echo argv[1]

The shift statement works to move the elements of the array (argv in this case - the default) one place to the left. The form for the shift statement is:

shift [variable]

The variable $var[2] becomes $var[1], and $var[3] becomes $var[2], etcetera. The variable $var[1] (before shifting) is destroyed (at least unusable). An error will result if any the variable given is unset or has NULL value.

The foreach construct is quite different from the while construct in that now an actual expression is evaluated. The foreach loop execution is determined by the number of elements in a list. The form of a foreach construct is as follows:

foreach variable (list)

Each element of the list is removed from the list and placed in
\verb+variable+ during execution of the command list.
When there remain no elements in list, the execution of command list
This construct can also be used to manipulate arguments given at the
command line that initiates the script.
The following script fragment will, as in the above example, print out
a list of the arguments given:

foreach ARG ($argv)
   echo $ARG

This version is shorter and probably a bit clearer than the first, but each user will have his own preference when it comes to looping constructs for this type of situation. The foreach list can also contain a list of words such as:

foreach color (red green blue yellow)

This is a situation where the foreach construct is the only way to proceed. Likewise, there are situations where only while statements will provide the control required, such as arithmetic expressions resulting from the execution of the command list.

There is really one more loop control mechanism provided by the C shell. The over used quick fix, or goto statement. Anyone who has programmed in BASIC is well aware of the goto statement as well as how quickly it can add bugs to a piece of code. Regardless of its downside, the goto statement is provided for use in the C shell scripting language. The goto statement has the form:

goto LABEL

where LABEL is a string that is placed within the script. The label cannot however reside within a loop or branch construct, and any attempt to do so will result in an error. Almost any code where a goto is used can be rewritten using the control constructs described above. All that is required is a little bit of forethought and imagination. With the tools outlined in this and earlier sections, complex scripts can be constructed, as will be seen shortly.

next up previous contents
Next: Signal Handling Up: C Shell Programming Previous: C Shell Programming

Douglas M Gingrich
Mon Apr 27 15:25:49 MDT 1998