2.14. Conditional Execution

Sometimes we need to run a particular command or sequence of commands only if a certain condition is true.

For example, if program B processes the output of program A, we probably won't want to run B at all unless A finished successfully.

2.14.1. Command Exit Status

Conditional execution in Unix shell scripts often utilizes the exit status of the most recent command.

All Unix programs return an exit status. It is not possible to write a Unix program that does not return an exit status. Even if the programmer neglects to explicitly return a value, the program will return a default value.

By convention, programs return an exit status of 0 if they determine that they completed their task successfully and a variety of non-zero error codes if they failed. There are some standard error codes defined in the C header file sysexits.h. You can learn about them by running man sysexits.

We can check the exit status of the most recent command by examining the shell variable $? in Bourne shell family shells or $status in C shell family shells.

bash> ls
myprog.c
bash> echo $?
0
bash> ls -z
ls: illegal option -- z
usage: ls [-ABCFGHILPRSTUWZabcdfghiklmnopqrstuwx1] [-D format] [file ...]
bash> echo $?
1
bash> 
            
tcsh> ls
myprog.c
tcsh> echo $status
0
tcsh> ls -z
ls: illegal option -- z
usage: ls [-ABCFGHILPRSTUWZabcdfghiklmnopqrstuwx1] [-D format] [file ...]
tcsh> echo $status
1
tcsh> 
            

Practice Break

Run several commands correctly and incorrectly and check the $? or $status variable after each one.

2.14.2. If-then-else Statements

All Unix shells have an if-then-else construct implemented as internal commands. The Bourne shell family of shells all use the same basic syntax. The C shell family of shells also use a common syntax, which is somewhat different from the Bourne shell family.

Bourne Shell Family

The general syntax of a Bourne shell family if statement is shown below. Note that there can be an unlimited number of elifs, but we will use only one for this example.

#!/bin/sh

if command1
then
    command
    command
    ...
elif command2
then
    command
    command
    ...
else
    command
    command
    ...
fi
                

Note

The 'if' and the 'then' are actually two separate commands, so they must either be on separate lines as shown above, or separated by an operator such as ';', which can be used instead of a newline to separate Unix commands.

cd; ls
if command; then
                

The if command executes command1 and checks the exit status when it completes.

If the exit status of command1 is 0 (indicating success), then all the commands before the elif are executed, and everything after the elif is skipped.

If the exit status is non-zero, then nothing above the elif is executed. The elif command then executes command2 and checks its exit status.

If the exit status of command2 is 0, then the commands between the elif and the else are executed and everything after the else is skipped.

If the exit status of command2 is non-zero, then everything above the else is skipped and everything between the else and the fi is executed.

Note

In Bourne shell if statements, an exit status of zero effectively means 'true' and non-zero means 'false', which is the opposite of C and similar languages.

In most programming languages, we use some sort of Boolean expression (usually a comparison, also known as a relation), not a command, as the condition for an if statement.

This is generally true in Bourne shell scripts as well, but the capability is provided in an interesting way. We'll illustrate by showing an example and then explaining how is works.

Suppose we have a shell variable and we want to check whether it contains the string "blue". We could use the following if statement to test:

#!/bin/sh

printf "Enter the name of a color: "
read color

if [ "$color" = "blue" ]; then
    printf "You entered blue.\n"
elif [ "$color" = "red" ]; then
    printf "You entered red.\n"
else
    printf "You did not enter blue or red.\n"
fi

The interesting thing about this code is that the square brackets are not Bourne shell syntax. As stated above, the Bourne shell if statement simply executes a command and checks the exit status. This is always the case.

The '[' in the condition above is actually an external command! In fact, is simply another name for the test command. The files /bin/test and /bin/[ are actually the same program file:

FreeBSD tocino bacon ~ 401: ls -l /bin/test /bin/[
-r-xr-xr-x  2 root  wheel  8516 Apr  9  2012 /bin/[*
-r-xr-xr-x  2 root  wheel  8516 Apr  9  2012 /bin/test*
                

We could have also written the following:

if test "$color" = "blue"; then
                

Hence, "$color", =, "blue", and ] are all separate arguments to the [ command, and must be separated by white space. If the command is invoked as '[', then the last argument must be ']'. If invoked as 'test', then the ']' is not allowed.

The test command can be used to perform comparisons (relational operations) on variables and constants, as well as a wide variety of tests on files. For comparisons, test takes three arguments: the first and third are string values and the second is a relational operator.

# Compare a variable to a string constant
test "$name" = 'Bob'
                
# Compare the output of a program directly to a string constant
test `myprog` = 42
                

For file tests, test takes two arguments: The first is a flag indicating which test to perform and the second is the path name of the file or directory.

# See if output file exists and is readable to the user
# running test
test -r output.txt
                

The exit status of test is 0 (success) if the test is deemed to be true and a non-zero value if it is false.

shell-prompt: test 1 = 1
shell-prompt: echo $?
0
shell-prompt: test 1 = 2
shell-prompt: echo $?
1
                

The relational operators supported by test are shown in Table 2.5, “Test Command Relational Operators”.

Table 2.5. Test Command Relational Operators

OperatorRelation
=Lexical equality (string comparison)
-eqInteger equality
!=Lexical inequality (string comparison)
-neInteger inequality
<Lexical less-than (10 < 9)
-ltInteger less-than (9 -lt 10)
-leInteger less-than or equal
>Lexical greater-than
-gtInteger greater-than
-geInteger greater-than or equal

Caution

Note that some operators, such as < and >, have special meaning to the shell, so they must be escaped or quoted.

test 10 > 9     # Redirects output to a file called '9'.
                # The only argument sent to the test command is '10'.
                # The test command issues a usage message since it requires
                # more arguments.
test 10 \> 9    # Compares 10 to 9.
test 10 '>' 9   # Compares 10 to 9.
                

Caution

It is a common error to use '==' with the test command, but the correct comparison operator is '='.

Common file tests are shown in Table 2.6, “Test command file operations”. To learn about additional file tests, run "man test".

Table 2.6. Test command file operations

FlagTest
-eExists
-rIs readable
-wIs writable
-xIs executable
-dIs a directory
-fIs a regular file
-LIs a symbolic link
-sExists and is not empty
-zExists and is empty

Caution

Variable references in a [ or test command should usually be enclosed in soft quotes. If the value of the variable contains white space, such as "navy blue", and the variable is not enclosed in quotes, then "navy" and "blue" will be considered two separate arguments to the [ command, and the [ command will fail. When [ sees "navy" as the first argument, it expects to see a relational operator as the second argument, but instead finds "blue", which is invalid.

Furthermore, if there is a chance that a variable used in a comparison is empty, then we must attach a common string to the arguments on both sides of the operator. It can be almost any character, but '0' is popular and easy to read.

name=""
if [ "$name" = "Bob" ]; then    # Error, expands to: if [ = Bob; then
if [ 0"$name" = 0"Bob" ]; then  # OK, expands to: if [ 0 = 0Bob ]; then
                

Relational operators are provided by the test command, not by the shell. Hence, to find out the details, we would run "man test" or "man [", not "man sh" or "man bash".

See Section 2.14.1, “Command Exit Status” for information about using the test command.

Practice Break

Run the following commands in sequence and run 'echo $?' after every test or [ command under bash and 'echo $status' after every test or [ command under tcsh.

bash
test 1 = 1
test 1=2
test 1 = 2
[ 1 = 1
[ 1 = 1 ]
[ 1 = '1' ]
[1=1]
[ 2 < 10 ]
[ 2 = 3 ]
[ 2 \< 10 ]
[ 2 '<' 10 ]
[ 2 -lt 10 ]
[ $name = 'Bill' ]
[ 0$name = 0'Bill' ]
name='Bob'
[ $name = 'Bill' ]
[ $name = Bill ]
[ $name = Bob ]
exit
tcsh
[ $name = 'Bill' ]
[ 0$name = 0'Bill' ]
set name='Bob'
[ $name = 'Bill' ]
which [
exit
                    

C shell Family

Unlike the Bourne shell family of shells, the C shell family implements its own operators, so there is generally no need for the test or [ command (although you can use it in C shell scripts if you really want to).

The C shell if statement requires () around the condition, and the condition is always a Boolean expression, just like in C and similar languages. As in C, and unlike Bourne shell, a value of zero is considered false and non-zero is true.

#!/bin/csh -ef

printf "Enter the name of a color: "
set color = "$<"

if ( "$color" == "blue" ) then
    printf "You entered blue.\n"
else if ( "$color" == "red" ) then
    printf "You entered red.\n"
else
    printf "You did not enter blue or red.\n"
endif

The C shell relational operators are shown in Table 2.7, “C Shell Relational Operators”.

Table 2.7. C Shell Relational Operators

OperatorRelation
<Integer less-than
>Integer greater-than
<=Integer less-then or equal
>=Integer greater-than or equal
==String equality
!=String inequality
=~String matches glob pattern
!~String does not match glob pattern

Note

C shell does not directly support string comparisons except for equality and inequality. To see if a string is lexically less-than or greater than another, use the test or [ command with <, <=, >, or >=.

Conditions in a C shell if statement do not have to be relations, however. We can check the exit status of a command in a C shell if statement using the {} operator:

#!/bin/csh

if ( { command } ) then
    command
    ...
endif
                

The {} essentially inverts the exit status of the command. If command returns 0, the the value of { command } is 1, which means "true" to the if statement. If command returns a non-zero status, then the value of { command } is zero.

C shell if statements also need soft quotes around strings that contain white space. However, unlike the test command, it can handle empty strings, so we don't need to add an arbitrary prefix like '0' if the string may be empty.

if [ 0"$name" = 0"Bob" ]; then
                
if ( "$name" == "Bob" ) then
                

Practice Break

Type in the following commands in sequence:

tcsh
if ( $first_name == Bob ) then
    printf 'Hi, Bob!\n'
endif
set first_name=Bob
if ( $first_name == Bob ) then
    printf 'Hi, Bob!\n'
endif
exit
                    

2.14.3. Conditional Operators

The shell's conditional operators allow us to alter the exit status of a command or utilize the exit status of each in a sequence of commands. They include the Boolean operators AND (&&), OR (||), and NOT (!).

shell-prompt: command1 && command2
shell-prompt: command1 || command2
shell-prompt: ! command
            

Table 2.8. Shell Conditional Operators

OperatorMeaningExit status
! commandNOT0 if command failed, 1 if it succeeded
command1 && command2AND0 if both commands succeeded
command1 || command2OR0 if either command succeeded

Note that in the case of the && operator, command2 will not be executed if command 1 fails (exits with non-zero status), since it could not change the exit status. Once any command in an && sequence fails, the exit status of the whole sequence will be 1, so no more commands will be executed.

Likewise in the case of a || operator, once any command succeeds (exits with zero status), the remaining commands will not be executed.

This fact is often used to conditionally execute a command only if another command is successful:

pre-processing && main-processing && post-processing
            

When using the test or [ commands, multiple tests can be performed using either the shell's conditional operators or the test command's Boolean operators:

if [ 0"$first_name" = 0"Bob" ] && [ 0"$last_name" = 0"Newhart" ]; then
if test 0"$first_name" = 0"Bob" && test 0"$last_name" = 0"Newhart"; then
            
if [ 0"$first_name" = 0"Bob" -a 0"$last_name" = 0"Newhart" ]; then
if test 0"$first_name" = 0"Bob" -a 0"$last_name" = 0"Newhart"; then
            

The latter is probably more efficient, since it only executes a single [ command, but efficiency in shell scripts is basically a lost cause, so it's best to aim for readability instead. If you want speed, use a compiled language.

Conditional operators can also be used in a C shell if statement. Parenthesis are recommended around each relation for readability.

if ( ("$first_name" == "Bob") && ("$last_name" == "Newhart") ) then
            

Practice Break

Run the following commands in sequence and run 'echo $?' after every command under bash and 'echo $status' after every command under tcsh.

bash
ls -z
ls -z && echo Done
ls -a && echo Done
ls -z || echo Done
ls -a || echo Done
first_name=Bob
last_name=Newhart
if [ 0"$first_name" = 0"Bob" ] && [ 0"$last_name" = 0"Newhart" ]
then
    printf 'Hi, Bob!\n'
fi
if [ 0"$first_name" = 0"Bob" -a 0"$last_name" = 0"Newhart" ]
then
    printf 'Hi, Bob!\n'
fi
exit
tcsh
ls -z
ls -z && echo Done
ls -a && echo Done
ls -z || echo Done
ls -a || echo Done
if ( $first_name == Bob && $last_name == Newhart ) then
    printf 'Hi, Bob!\n'
endif
set first_name=Bob
set last_name=Nelson
if ( $first_name == Bob && $last_name == Newhart ) then
    printf 'Hi, Bob!\n'
endif
set last_name=Newhart
if ( $first_name == Bob && $last_name == Newhart ) then
    printf 'Hi, Bob!\n'
endif
exit
                

2.14.4. Case and Switch Statements

If you need to compare a single variable to many different values, you could use a long string of elifs or else ifs:

#!/bin/sh

printf "Enter a color name: "
read color

if [ "$color" = "red" ] || \
   [ "$color" = "orange" ]; then
    printf "Long wavelength\n"
elif [ "$color" = "yello" ] || \
     [ "$color" = "green" ] || \
     [ "$color" = "blue" ]; then
    printf "Medium wavelength\n"
elif [ "$color" = "indigo" ] || \
     [ "$color" = "violet" ]; then
    printf "Short wavelength\n"
else
    printf "Invalid color name: $color\n"
fi

Like most languages, however, Unix shells offer a cleaner solution.

Bourne shell has the case statement:

#!/bin/sh

printf "Enter a color name: "
read color

case $color in
    red|orange)
        printf "Long wavelength\n"
        ;;
    yellow|green|blue)
        printf "Medium wavelength\n"
        ;;
    indigo|violet)
        printf "Short wavelength\n"
        ;;
    *)
        printf "Invalid color name: $color\n"
        ;;
esac

C shell has a switch statement that looks almost exactly like the switch statement in C, C++, and Java:

#!/bin/csh -ef

printf "Enter a color name: "
set color = "$<"

switch($color)
case    red:
case    orange:
    printf "Long wavelength\n"
    breaksw
case    yellow:
case    green:
case    blue:
    printf "Medium wavelength\n"
    breaksw
case    indigo:
case    violet:
    printf "Short wavelength\n"
    breaksw
default:
    printf "Invalid color name: $color\n"
endsw

Note

The ;; and breaksw statements cause a jump to the first statement after the entire case or switch. The ;; is required after every value in the case statement. The breaksw is optional in the switch statement. If omitted, the script will simply continue on and execute the statements for the next case value.

2.14.5. Self-test

  1. What is an "exit status"? What conventions to Unix programs follow regarding the exit status?
  2. How can we find out the exit status of the previous command in Bourne shell? In C shell?
  3. Write a Bourne shell script that uses an if statement to run 'ls -l > output.txt' and view the output using 'more' only of the ls command succeeded.
  4. Repeat the previous problem using C shell.
  5. Write a Bourne shell script that inputs a first name and outputs a different message depending on whether the name is 'Bob'.

    shell-prompt: ./script
    What is your name? Bob
    Hey, Bob!
    shell-prompt: ./script
    What is your name? Bill
    Hey, you're not Bob!
                    
  6. Repeat the previous problem using C shell.
  7. Write a Bourne and/or C shell script that inputs a person's age and indicates whether they get free peanuts. Peanuts are provided to senior citizens.

    shell-prompt: ./script
    How old are you? 42
    Sorry, no peanuts for you.
    shell-prompt: ./script
    How old are you? 72
    Have some free peanuts, wise sir!
                    
  8. Why is it necessary to separate the tokens between [ and ] with white space? What will happen if we don't?
  9. What will happen if a value being compared using test or [ contains white space? How to we remedy this?
  10. What will happen if a value being compared using test or [ is an empty string? How to we remedy this?
  11. Why do the < and > operators need to be escaped (\<, \>) or quoted when used with the test command?
  12. What is the == operator used for with the test command?
  13. How do we check the exit status of a command in a C shell if statement?
  14. Write a Bourne and/or C shell script that inputs a person's age and indicates whether they get free peanuts. Peanuts are provided to senior citizens and children between the ages of 3 and 12.

    shell-prompt: ./script
    How old are you? 42
    Sorry, no peanuts for you.
    shell-prompt: ./script
    How old are you? 72
    Have some free peanuts, wise sir!
                    
  15. Write a Bourne shell script that uses conditional operators to run 'ls -l > output.txt' and view the output using 'more' only of the ls command succeeded.
  16. Write a shell script that checks the output of uname using a case or switch statement and reports whether the operating system is supported. Assume supported operating systems include Cygwin, Darwin, FreeBSD, and Linux.
    shell-prompt: ./script
    FreeBSD is supported.
                    
    shell-prompt: ./script
    AIX is not supported.