The next flow control construct we will 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 pipe characters (|). If expression matches one of the patterns, its corresponding statements are executed. If there are several patterns separated by pipe characters, 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." return 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). If the filename
does not end in .o there is an error. We handle this with
the case *
.o ), which has no statements. There is nothing
wrong with a "case" for which the script does nothing.
The final case is *
, which 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." return 1 ;; esac objfiles="$objfiles $objname" done ld $objfiles
The first line in this version of the script initializes the variable objfiles to null. [16] 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.
[16] 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.
We'll return to this example once more in Chapter 6 when we discuss how to handle dash options on the command line. Meanwhile, here is a new task whose initial solution will use case.
You are a system administrator,[17] and you need to set up the system so that users' TERM environment variables reflect correctly what type of terminal they are on. Write some code that does this.
[17] 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 will 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 pipe character 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, i.e., 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 blanks) 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 this way in Chapter 7.