Control Flow

To write interesting scripts you need to know about branching, looping, etc. First, you should realize that every command returns an exit status value to the shell. This value is accessible via the $? shell variable. Normally, however, you do not use $?. Instead you use control flow commands that automatically check $?. Here is the format of the 'if' command.

$ if command
> then
>   # Do this if 'command' returns a zero status (TRUE).
> fi
$

Note that 'fi' is used to terminate an 'if' block. 'Fi' is 'if' spelled backwards.

The basic idea is simple. The command designated by command is run. If that command returns a status value of zero, the commands past the 'then' get executed; otherwise they are skipped. The 'then' keyword plays the role of a command. Since multiple commands can be placed on the command line, another way to format the example above is as

$ if command; then
>   # Do this if 'command' returns a zero status (TRUE).
> fi
$

The syntax of the 'if' command allows for an else. Here are some possibilites.

$ if command1; then
>   # Do this if 'command1' returns a zero status.
> else
>   # Do this if 'command1' returns a nonzero status.
> fi
$

or

$ if command1; then
>   # 'command1' was true.
> elif command2; then
>   # 'command2' was true.
> else
>   # Neither 'command1' nor 'command2' were true.
> fi
$

Many commands return a status code based on what they do. For example, the grep command returns success if it finds at least one match. Grep has an option which suppresses the printing of all matches (silent mode). At first such an option seems silly. However, it allows grep to be used in an 'if' command cleanly. For example

$ if grep -s 'Peter' afile.txt; then
>   # The string 'Peter' was found. Haven't told the user yet.
> fi
$

By far, the program most often used with the 'if' command is a program named test. Test simply makes a test and returns with an appropriate status. Test has many options.

$ test -f afile.txt # TRUE if afile.txt is a file that exists.
$ test -r afile.txt # TRUE if afile.txt is a read only file.
$ test -d afile.txt # TRUE if afile.txt is a directory.
$ test -x afile.txt # TRUE if afile.txt is executable.
$ test -r afile.txt # TRUE if afile.txt is readable.
$ test -w afile.txt # TRUE if afile.txt is writable.

$ test $X -eq $Y
  # TRUE if X and Y are equal (when converted to ints).
$ test $X -ne $Y    # X!= Y
$ test $X -gt $Y    # X > Y
$ test $X -lt $Y    # X < Y
$ test $X -ge $Y    # X >=Y
$ test $X -le $Y    # X <=Y

$ test $X = $Y
  # TRUE if X and Y are equal as strings.
$ test $X != $Y     # X not the same as Y

The difference between strings and integers is very significant. For example

$ FIRST=Hello
$ SECOND=There
$ if test $FIRST -eq $SECOND; then
>  echo They are the same.
> fi
They are the same.
$ if test $FIRST != $SECOND; then
>  echo They are different.
> fi
They are different.
$

The reason why the first test succeeded is because the string "Hello" became the integer zero in the test. Similarly the string "There" became a zero.

Note that spaces are important. The arguments to test are like any other program's arguments. Test is like any other program.

You can use the -o (OR) and the -a (AND) options of test to create more complex tests. For example

$ if test $X -eq $Y -a $A -eq $B; then
>  echo X == Y and A == B
> fi
X == Y and A == B
$

You can use parenthesis to create even more complex tests. However, since parenthesis are special to the shell, you must quote the parenthesis to prevent the shell from handling them. The shell must pass the parenthesis to the test program. For example

$ if test \($X = $Y\) -o \($A != $B\); then
... etc.

Since test is used so much, the shell has a special syntax for invoking it. For example

$ if [ $X -eq $Y ]; then ....

$ if test $X -eq $Y; then ....

Both the commands above are identical. The square bracket syntax is much easier to read. I will use it from now on. Note that there must be a space immediately after the open square bracket. For example

$ X=prog
$ if [$X = $Y]; then ....
[prog: not found
$

The shell attempted to run the command [prog. The square bracket alone signals the special test syntax. The spaces around the trailing ] are optional. For consistency, they are usually included.

The while loop follows many of the same rules and concepts developed above. Here is the basic syntax

$ while command
> do
>   # Do this as long as 'command' returns a TRUE status.
> done
$

Normally the command is an invocation of test with the special syntax. For example

$ while [ $X -lt $Y ]; do ....

The shell also provides an until loop. However, unlike what you would expect, this loop still performs its test before the loop body is entered. For example in

$ until commmand
> do
>   # Do this until 'command' returns a TRUE status.
> done
$

the loop body is skipped if command returns TRUE immediately.

Finally, the shell provides a case statement (similar in concept to C's switch statement). Here is it's syntax

case test-string in
  pattern-1)
    commands;;
  pattern-2)
    commands;;
  pattern-3)
    commands;;
esac

The 'test-string' is an arbitrary string of characters. The shell tries to match each of the patterns to the test-string. When it finds a matching pattern, it executes commands up to the ';;'. The patterns are tested in the order they appear. Furthermore, the shell uses the wildcard matching syntax within the patterns. For example suppose the following appeared in a script:

echo "Enter your response: \c"
read Response junk

case $Response in
  [yY]*)
    echo "You said YES!";;
  [nN]*)
    echo "You said NO!";;
  *)
    echo "I don't know what you're talking about.";;
esac

The script would assume you said "yes" to any response that started with a 'y' (upper or lower case). If your response started with an 'n', the shell would assume "no." Otherwise the shell prints an error message.

In addition, you can join several patterns with a logical OR operator (|). For example:

case $Response in
  yes | Yes)
    echo "You said YES!";;
  *)
    echo "Ok, ok, I won't do it.";;
esac