Chapter 17

Shell Scripts - case and for

Introduction

In this chapter, we look at shell's simplest way of selecting between alternative actions and at its simplest way of repeating commands.

case

In chapter ??, we were embarrassed by bu not being able to behave well if not given exactly one argument. We need to make bu select between two courses of actions. Using shell's case statement will allow us to do that but first let's see the general shape of case. The man pages for sh say the format is like this

case word in [pattern[ | pattern] ... ) list ;; ] ... esac

However, when used in real scripts it is usually spread over several lines and indented - like this:

case word in
     pattern )
          list
          ;;
     pattern )
          list
          ;;
     pattern )
          list
          ;;
esac

Here, the optional | pattern has been omitted and the optional pattern ) list ;; has been used three times. The word must be replaced with the value of a variable or parameter. The list must be replaced with a sequence of Unix commands. The only fixed part of the layout is that the case, esac and the patterns must be the first thing on their lines. If esac is puzzling you, it is case backwards!

The way case works is that shell takes the word and compares it with the patterns. If a match is found, the commands in the list after the pattern are executed as far as the ;;. Shell then skips to the line after esac.

Here then is an improved bu using the case command:

$ more bu
case $# in
     0)   echo usage: bu file
          ;;
     1)   file=$1
          cp $file $file.bu
          echo $file backed-up in $file.bu
          ;;
     2)   echo usage: bu file
          ;;
esac
$

In this version of bu, $#, the number of parameters, has been used as the word in the case statement. Each of the three patterns is simply a single digit. The first list has been replaced by one Unix command, the second list has been replaced by three Unix commands, and the last list has been replaced by one Unix command. Shell checks the word against the patterns to select which list of commands to execute.

This version will not work with three or more parameters because the case statement does not include the patterns: 3, 4, and so on. To correct it, we need to use pattern matching like shell uses with file names. The main facilities are * and ?. * matches any string of characters and ? matches any single character. You need to read the manual carefully for all the options.

Using * allows us improve bu:

$ more bu
case $# in
     1)   file=$1
          cp $file $file.bu
          echo $file backed-up in $file.bu
          ;;
     *)   echo usage: bu file
          ;;
esac
$

Be careful to put the * pattern last, otherwise shell will never find the earlier patterns because * matches any string including 1!

For

Our bu command is still not as useful as it could be. Why can't we back-up more than one file at once? To do that we need to know shell's for statement. According to the man pages for sh, it has this format:

for name [ in word ... ] do list done

This example shows the usual layout:

for green in Carson Kelly Lambert
do   echo Ms $green is a well-known green.
     echo
done

As you can see, name has been replaced with the variable green; word ... has been replaced with a list of actual words; and list has been replaced with two echo commands. When shell executes a for it puts the first word in the list of words into the named variable and performs the Unix commands between do and done. Then it repeats the commands with the second and subsequent words from the list. Shell insists that for, do and done are the first words on their lines.

Here is the output from the previous example:

$ greens
Ms Carson is a well-known green.

Ms Kelly is a well-known green.

Ms Lambert is a well-known green.

$

The above form of for won't quite do for bu. If you look carefully at the format of for, you will see that in word ... is optional. If we don't supply any words, shell will use the parameters one by one. We can use that to step through as many files as we like. Making the changes, we get:

$ more bu
case $# in
     0)   echo usage: bu file ...
          ;;
     *)   for file
          do   cp $file $file.bu
               echo $file backed-up in $file.bu
          done
          ;;
esac

Now, the only thing bu has to check for is zero parameters; any other number causes the for statement to be executed.

A very important idea has sneaked in here so let's spell it out: There is no problem in using a for inside a case or vice versa. This applies to all of shell's selection and repetition facilities. Of course, proper indentation helps the reader follow what is happening.

If we create another valuable file, we can use the new, improved bu on it.

$ date > priceless
$ bu precious priceless
precious backed-up in precious.bu
priceless backed-up in priceless.bu
$ ls pr*
precious
precious.bu
priceless
priceless.bu
$

Our bu script goes from strength to strength!

Any number of parameters

Another point to notice about the previous example is that it will deal with any number of parameters. Because we don't try to refer to them by number, there is no problem handling parameters ten, eleven and so on, or even parameters 100, 101 and upwards!

What a powerful combination

The for and case statements are very powerful and simple; they rely on lists of words and on pattern matching, instead of relying on condition tests. They allow us to do things we would normally expect to use the (more complicated) if or while statements for. The following example demonstrates their power:

$ more picky
for file
do   case $file in
          *.txt)
               echo "can't do .txt files"
               exit
               ;;
          fred.*)
               echo "can't do files called fred.*"
               exit
               ;;
          *'*'*)
               echo "can't handle asterisks in file names"
               exit
               ;;
     esac
done
echo all OK
$

The exit statement is new to us; it causes the shell to stop running the script, without executing any more statements.

The picky script displays an error message and stops if it finds a filename it can't handle in its arguments. For example:

$ picky tom.gif dick.jpeg harry.html
all OK
$ picky tom.gif dick.jpeg fred.any harry.html
can't do files called fred.*
$

This is very straightforward code to do such complex checking.

QUESTIONS

For each of the following, you have to write a shell script. Name them `q12.1' ...

  1. Write a shell script to append one file to another. The arguments are both file names. If only one name is given, the script should append from the standard input to the named file. If two names are given, the first named file should be appended to the second. Hint: cat copies its input to its output.

    Answer

    $ more q12.1
    case $# in
         1) cat >> $1 ;;
         2) cat $1 >> $2 ;;
    esac
    $ > file1
    $ > file2
    $ q12.1 file1
    a line of text for file1
    and another
    ^D
    $ q12.1 file2
    a line for file2
    ^D
    $ q12.1 file1 file2
    $ more file2
    a line for file2
    a line of text for file1
    and another
    $
    

    Hide

  2. Extend the script from question one so that it displays a usage message if given anything other than one or two arguments.

    Answer

    $ more q12.2
    case $# in
         1) cat >> $1 ;;
         2) cat $1 >> $2 ;;
         *) echo 'Usage: q12.2 [from] to'
    esac
    $ q12.2
    Usage: q12.2 [from] to
    $
    

    Hide

  3. Write a script to do an ls of the directories `Tom', `Jerry' and `Bonzo' with appropriate headings.

    Answer

    $ more q12.3
    for directory in Tom Jerry Bonzo
    do   cd $directory
         echo This is an ls of $directory:
         ls
         cd ..
    done
    $ mkdir Tom Jerry Bonzo
    $ q12.3
    This is an ls of Tom:
    This is an ls of Jerry:
    This is an ls of Bonzo:
    $
    

    Hide

  4. Construct a shell script to display its arguments one per line.

    Answer

    $ more q12.4
    for parameter
    do   echo $parameter
    done
    $ q12.4 f1 f2 f3
    f1
    f2
    f3
    $
    

    Hide

ANSWERS

In each of the answers the script is displayed using more; then it is executed by typing its name.

  1. $ more q12.1
    case $# in
         1) cat >> $1 ;;
         2) cat $1 >> $2 ;;
    esac
    $ > file1
    $ > file2
    $ q12.1 file1
    a line of text for file1
    and another
    ^D
    $ q12.1 file2
    a line for file2
    ^D
    $ q12.1 file1 file2
    $ more file2
    a line for file2
    a line of text for file1
    and another
    $
    
  2. $ more q12.2
    case $# in
         1) cat >> $1 ;;
         2) cat $1 >> $2 ;;
         *) echo 'Usage: q12.2 [from] to'
    esac
    $ q12.2
    Usage: q12.2 [from] to
    $
    
  3. $ more q12.3
    for directory in Tom Jerry Bonzo
    do   cd $directory
         echo This is an ls of $directory:
         ls
         cd ..
    done
    $ mkdir Tom Jerry Bonzo
    $ q12.3
    This is an ls of Tom:
    This is an ls of Jerry:
    This is an ls of Bonzo:
    $
    
  4. $ more q12.4
    for parameter
    do   echo $parameter
    done
    $ q12.4 f1 f2 f3
    f1
    f2
    f3
    $