Book HomeBook TitleSearch this book

5.3. case

The next flow control construct to cover is case. While the case statement in Pascal and the similar switch statement in C can be used to test simple values like integers and characters, the Korn shell's case construct lets you test strings against patterns that can contain wildcard characters. Like its conventional language counterparts, case lets you express a series of if-then-else type statements in a concise way.

The syntax of case is as follows:

case expression in
    pattern1 )
        statements ;;
    pattern2 )
        statements ;;
    ...
esac

Any of the patterns can actually be several patterns separated by "OR bar" characters (|, which is the same as the pipe symbol, but in this context means "or"). If expression matches one of the patterns, its corresponding statements are executed. If there are several patterns separated by OR bars, the expression can match any of them in order for the associated statements to be run. The patterns are checked in order until a match is found; if none is found, nothing happens.

This rather ungainly syntax should become clearer with an example. An obvious choice is to revisit our solution to Task 4-2, the front-end for the C compiler. Earlier in this chapter, we wrote some code that processed input files according to their suffixes (.c, .s, or .o for C, assembly, or object code, respectively).

We can improve upon this solution in two ways. First, we can use for to allow multiple files to be processed at one time; second, we can use case to streamline the code:

for filename in "$@"; do
    case $filename in
        *.c )
            objname=${filename%.c}.o
            ccom "$filename" "$objname" ;;
        *.s )
            objname=${filename%.s}.o
            as "$filename" "$objname" ;;
        *.o ) ;;
        *   )
            print "error: $filename is not a source or object file."
            exit 1 ;;
    esac
done

The case construct in this code handles four cases. The first two are similar to the if and first elif cases in the code earlier in this chapter; they call the compiler or the assembler if the filename ends in .c or .s, respectively.

After that, the code is a bit different. Recall that if the filename ends in .o nothing is to be done (on the assumption that the relevant files will be linked later). We handle this with the case *.o ), which has no statements. There is nothing wrong with a "case" for which the script does nothing.

If the filename does not end in .o, there is an error. This is dealt with in the final case, which is *. This is a catchall for whatever didn't match the other cases. (In fact, a * case is analogous to a default case in C and an otherwise case in some Pascal-derived languages.)

The surrounding for loop processes all command-line arguments properly. This leads to a further enhancement: now that we know how to process all arguments, we should be able to write the code that passes all of the object files to the linker (the program ld) at the end. We can do this by building up a string of object file names, separated by spaces, and hand that off to the linker when we've processed all of the input files. We initialize the string to null and append an object file name each time one is created, i.e., during each iteration of the for loop. The code for this is simple, requiring only minor additions:

objfiles=""
for filename in "$@"; do
    case $filename in
        *.c )
            objname=${filename%.c}.o
            ccom "$filename" "$objname" ;;
        *.s )
            objname=${filename%.s}.o
            as "$filename" "$objname" ;;
        *.o )
            objname=$filename ;;
        *   )
            print "error: $filename is not a source or object file."
            exit 1 ;;
    esac
    objfiles+=" $objname"
done
ld $objfiles

The first line in this version of the script initializes the variable objfiles to null.[77] We added a line of code in the *.o case to set objname equal to $filename, because we already know it's an object file. Thus, the value of objname is set in every case -- except for the error case, in which the routine prints a message and bails out.

[77] This isn't strictly necessary, because all variables are assumed to be null if not explicitly initialized (unless the nounset option is turned on). It just makes the code easier to read.

The last line of code in the for loop body appends a space and the latest $objname to objfiles. Calling this script with the same arguments as in Figure 5-1 would result in $objfiles being equal to " a.o b.o c.o d.o" when the for loop finishes (the leading space doesn't matter). This list of object filenames is given to ld as a single argument, but the shell divides it up into multiple file names properly.

Task 5-4 is a new task whose initial solution uses case.

Task 5-4

You are a system administrator,[78] and you need to set up the system so that users' TERM environment variables correctly reflect what type of terminal they are on. Write some code that does this.

[78] Our condolences.

The code for the solution to this task should go into the file /etc/profile, which is the master startup file that is run for each user before his or her .profile.

For the time being, we assume that you have a traditional mainframe-style setup, in which terminals are hard-wired to the computer. This means that you can determine which (physical) terminal is being used by the line (or tty) it is on. This is typically a name like /dev/ttyNN, where NN is the line number. You can find your tty with the command tty(1), which prints it on the standard output.

Let's assume that your system has ten lines plus a system console line (/dev/console), with the following terminals:

Here is the code that does the job:

case $(tty) in
    /dev/tty0[134]            ) TERM=gl35a ;;
    /dev/tty07                ) TERM=t2000 ;;
    /dev/tty08 | /dev/console ) TERM=s531  ;;
    *                         ) TERM=vt99  ;;
esac

The value that case checks is the result of command substitution. Otherwise, the only thing new about this code is the OR bar after /dev/tty08. This means that /dev/tty08 and /dev/console are alternate patterns for the case that sets TERM to "s531".

Note that it is not possible to put alternate patterns on separate lines unless you use backslash continuation characters at the end of all but the last line. In other words, the line:

/dev/tty08 | /dev/console ) TERM=s531  ;;

could be changed to the slightly more readable:

/dev/tty08 | \
    /dev/console   ) TERM=s531  ;;

The backslash must be at the end of the line. If you omit it, or if there are characters (even spaces) following it, the shell complains with a syntax error message.

This problem is actually better solved using a file that contains a table of lines and terminal types. We'll see how to do it that way in Chapter 7.

When a case appeared inside the $(...) command-substitution construct, ksh88 had a problem: the ) that demarcates each pattern from the code to execute terminated the $(...). To get around this, it was necessary to supply a leading ( in front of the pattern:

result=$(case $input in
         ( dave ) print Dave! ;;      Open paren required in ksh88
         ( bob  ) print Bob! ;;
         esac)

ksh93 still accepts this syntax, but it no longer requires it.

5.3.1. Merging Cases

Sometimes, when writing a case-style construct, there are instances where one case is a subset of what should be done for another. The C language handles this by letting one case in a switch "fall through" into the code for another. A little-known fact is that the Korn shell (but not the Bourne shell) has a similar facility.

For example, let's suppose that our C compiler generates only assembly code, and that it's up to our front-end script to turn the assembly code into object code. In this case, we want to fall through from the *.c case into the *.s case. This is done using ;& to terminate the body of the case that does the falling through:

objfiles=""
for filename in "$@"; do
    case $filename in
        *.c )
            asmname=${filename%.c}.s
            ccom "$filename" "$asmname"
            filename=$asmname ;&    # fall through!
        *.s )
            objname=${filename%.s}.o
            as "$filename" "$objname" ;;
        *.o )
            objname=$filename ;;
        *   )
            print "error: $filename is not a source or object file."
            exit 1 ;;
    esac
    objfiles+=" $objname"
done
ld $objfiles

Before falling through, the *c case has to reset the value of filename so that the *.s case works correctly. It is usually a very good idea to add a comment indicating that the "fall through" is on purpose, although it is more obvious in shell than in C. We'll return to this example once more in Chapter 6 when we discuss how to handle dash options on the command line.



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.