## 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:

- Lines tty01, tty03, and tty04 are Givalt GL35a's (terminfo name "gl35a").

- Line tty07 is a Tsoris T-2000 ("t2000").

- Line tty08 and the console are Shande 531s ("s531").

- The rest are Vey VT99s ("vt99").

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.

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