Cover V05, I09
Article
Figure 1
Figure 2
Listing 1

sep96.tar


Taking Shell Scripts Beyond Simple Batch Functionality

Emmett Dulaney

Shell scripts can be as simple or as complex as you care to make them. In their simplest form, they are nothing more than batch files - combining one or more single-line commands into one short, executable file. In their more complex form, they perform logic operations and manipulations in the interest of time saving and automation.

The key to increasing the functionality of your shell scripts while maintaining manageability is to know what external utilities and built-in shell commands are at your disposal. This article examines several that you must know if you want to move beyond beginning scripting. They are, in order of discussion:

read            built-in
case            built-in
break           built-in
exit            built-in
tr              external
expr            external
typeset         built-in, ksh only

Using these commands within scripts removes the static limitations of always performing the same, mundane tasks that must exist in their absence.

Accepting User Input

There are essentially three ways to introduce variables to scripts. The first is to place them on the command line and look for them using variations of $1-$9. The second is to read them from a static file into which you place the variables that remain consistent, and the third is to enter them into the script, as it is running, from the keyboard.

Whenever a read command is encountered in a script, processing stops and input is read into a named variable(s) until the newline character is entered. White space (be it a space or a tab) is used as the delimiter between variables, and excess words are stored in the last variable. The syntax is simply:

read variable

For example, if the script contains this set of lines:

clear
read name
echo $name

the screen will clear, processing will stop and wait until a value is read in and the newline character entered, then it will echo the value of what was read in, and go on. An echo command is used to prompt the user, as in:

clear
echo "Enter your name:c" read name

echo $name

More than one read command can be used to bring in additional variables, or they can be simultaneously obtained by the same read command. For example:

clear
echo "Enter your name:c" read fname lname

echo "Pleased to meet you $fname. Are you related to
$lname?"

In this scenario, if the keyboard operator enters "Kristin Evan," then the value of the variable fname becomes "Kristin," and the value of the lname variable becomes "Evan." The white space placed between the two at time of entry serves as the delimiter signifying the end of fname and the beginning of lname. It is extremely important to note what was mentioned before; the last variable read takes on all remaining characters.

Therefore, if the keyboard operator enters at the prompt "Kristin Karen Evan," then fname remains "Kristin," but lname becomes "Karen Evan." In all cases, using only one variable on the line for read has the effect of assigning the entire line to the variable. Compare input and act, accordingly, with case.

Multiple Choices

The case command is intended for use when there are a number of possible operations that can take place based upon the value of a variable. A perfect example would be a menu - choices are displayed, the user picks one, and the operations carried out are those limited to that choice.

The syntax for the case command is shown below:

case {value} in
pat1)
command
command
;;
pat2)
command
command
;;
esac

Notice the esac at the end of the options list. This signifies the end of the case list in the same manner fi signifies the end of if statements. The purpose of the double semicolons (;;) is to end the case command at that point. Thus, if pat1 is entered, the lines specified for it are executed, and the case command exited. Without the double semicolons, the processing would continue until a pair were met, or the end of the case list. This provides a means by which multiple-line commands can be entered for each choice. If no patterns match the value, then no commands are executed.

The following is a simple example:

echo "\n\nEnter a letter from A - E:"
read lett
case $lett in
A)  echo "ASCII value is 65";;
B)  echo "ASCII value is 66";;
C)  echo "ASCII value is 67";;
D)  echo "ASCII value is 68";;
E)  echo "ASCII value is 69";;
esac

If the keyboard operator types in "A," the response they get is that the equivalent ascii value is 65. If they type in "C," they learn that it is 67. If they type in "F," then it does not match any of the choices, and nothing is echoed back to them.

It is important to understand that case will execute the first line matching the selection, and no others. Thus, if you had something like:

echo "\n\nEnter a letter from A - E:"
read lett
case $lett in
A)  echo "ASCII value is 65";;
B)  echo "ASCII value is 66";;
C)  echo "ASCII value is 67";;
D)  echo "ASCII value is 68";;
E)  echo "ASCII value is 69";;
A)  echo "You picked this
again?;;
esac

the last "A" choice would never execute. Once a match is found for the variable, and the double semicolons are encountered, the routine drops to esac and leaves. Therefore, you must place a bottom choice in the list that matches anything that made it this far without triggering execution of a command. This provides a means of dealing with exceptions, as in:

echo "\n\nEnter a letter from A - E:"
read lett
case $lett in
A)  echo "ASCII value is 65";;
B)  echo "ASCII value is 66";;
C)  echo "ASCII value is 67";;
D)  echo "ASCII value is 68";;
E)  echo "ASCII value is 69";;
*)  echo "You are beyond the
realm of
choices\07\07";;
esac

In this routine, anything not matching "A" through "E" causes an error message and two beeps of the terminal.

Patterns can utilize metacharacters, *, ?, {...}, and the pipe (|) symbol to signify one or another pattern. For example:

echo "\n\nEnter a letter from A - E:"
read lett
case $lett in
A)  echo "ASCII value is 65";;
B)  echo "ASCII value is 66";;
C)  echo "ASCII value is 67";;
D)  echo "ASCII value is 68";;
E)  echo "ASCII value is 69";;
Q|q) exit ;;
[1-9])echo "Only letters may be entered";;
*)  echo "You are beyond the realm of
choices\07\07";;
esac

Now, if any number is entered (as opposed to a letter), a standard message is returned. The routine can be exited using either "Q" or "q."

The exit command, within a case routine, exits the entire shell script. Therefore, if there are lines meant to run after the case routine, do not use exit, because they never will. An alternative to exit is break. When you use break as a choice, the execution leaves the entire case routine (drops beneath esac), and the rest of the script is carried out.

Using tr

The tr utility translates one character set to another as it copies standard input to standard output. The syntax is:

tr {first set} {second set}

For example:

tr "[a-z]" "[A-Z]"

will convert all lowercase characters from a to z into their corresponding uppercase character. In all cases, the to-string must be the same number of characters as the from-string, otherwise unexpected (or unwanted) results will occur. The tr command can also be used to change the value of an existing variable, using the following syntax:

x=`echo $1 | tr "[a-z]" "[A-Z]"`

There are a handful of parameters that can be used with the utility:

-c Restricts output to the characters specified in from-string and appends characters contained in to-string.
-d Deletes characters specified in from-string.
-s Strips repeated characters generated in the output by those specified in to-string.
[c1-cn] Specifies a range of ASCII characters, from c1 to cn.
[c*n] Specifies that character c repeats n times. The n may be zero or left blank, which assumes a huge number of the character c.

expr

Under normal operation, variables are always treated as string text and cannot be added together, increased, decreased, or otherwise arithmetically manipulated within the shell. Following is an example of the result of treating string variables numerically:

$ i=7
$ i=i+5
$ echo i
i
$ echo $i
i+5
$

One of the primary purposes of the expr utility is to interpret variables from strings. Numbers are separated by blanks, and strings should be quoted. Minus signs can be used to denote negative numbers, and all integers are treated as 32-bit, 2's complement numbers.

There are a great many possibilities present with expr, but the most useful are those pertaining to arithmetic. Operators that can be used in operations are shown in Figure 1. If expr has a problem during operation, it will return an exit value to the shell of:

0 if the expression is neither null nor 0. 1 if the expression is null or 0. 2 for invalid expressions.

Figure 2 shows five examples of operation.

A simple, incrementing script can be written in a file in the following fashion:

while true
do
i=`expr $i + 1`
echo $i
done

Using while true in this manner creates an endless loop because the test can never be untrue. expr will continue to increment the value of i and display it on the screen until you break out of it. To limit the number of times this routine runs, without breaking out of it, change the test condition to something that exists for only a short period, such as:

while [`date  '+%M'` + 02]

which will only run for one minute (naturally replace "02" with the current minute).

Using typeset Instead of expr

The typeset utility exists in the Korn shell only. If you are using Bourne, you must stick with expr to mathematically manipulate variables. The Korn shell, via the typeset utility, allows you to define, or set, a variable as possessing definition or a type, which makes it stand out. You can define a variable as always being one of the following:

uppercase -u lowercase -l left justified -L right justified -R read only -r exported -x an integer -i

It is the integer type (-i) of most interest for using typeset in place of expr. A subcommand, if you will, of typeset is let. let is used to change values, as in the following example:

typeset -i x y
x=$1
y=1
until [ "$y" < "$x" ]
do
print $y
let y="$y+1"
done

This routine establishes two variables as being integers, x and y. Next, the value of x is defined as a variable passed on the command line, and y is set to 1. Until y is one greater than x, the routine will print numbers from 1 to x. Therefore, if 7 is entered at the command line, the numbers 1 through 7 are printed. If 600 is entered on the command, the numbers 1 through 600 are printed.

An Example

Listing 1 shows an example of several of the utilities discussed here (the line numbers were added to aid explanation only). This is a standard menu utility, simply called MENU. Lines 2 through 14 clear the screen and print the menu choices. Line 14 also mentions that the user can press "Q" to quit the utility.

Line 15 accepts the input while line 16 translates any lowercase characters into uppercase - this will come in useful later when offering the choice to quit or obtain help. Line 17 echoes a portion of the date and the number of the menu choice made to a logfile that can be used to keep track of transactions.

Line 18 begins the case routine, which runs through line 73. Lines 19 through 21 comprise the first choice, and lines 22 through 37 are the second choice. Within the second choice, notice the additional prompt for input and lines 24 and 26.

Lines 38 through 47 are the third choice - with another read statement embedded within. The fourth selection begins on line 48 and runs through line 58. Notice the additional case routine embedded within it that begins on line 52.

Line 60 begins the "Q" choice allowing the user to quit. Notice the two exit commands within the choice. The first exits from the routine itself. The second kills the shell and knocks the user back to a login. Thus, this routine is limited to only being run by a special login defined in a user profile. When not running this routine, the user must log in by another identity.

Line 65 is where a help facility could be installed, and line 69 catches all invalid choices. Line 73 ends the case statement, and line 74 calls the routine all over again, restarting the menu on the screen.

If this script (shown in Listing 1) were running in a Korn shell environment, the Help and Quit functions could be assigned to menu choices 5 and 6, respectively. The variable read in could then be typeset as an integer and a test done to see whether the value was more than six or less than one. If so, an error message indicating the invalid choice would be delivered prior to getting to the case routine, and the user would be returned to the menu.

Conclusion

The utilities discussed can add power and performance to your shell scripts in a manner that cannot be equaled without them. Familiarizing yourself with them and the options they offer will allow you to write very good script routines.

About the Author

Emmett Dulaney is a Publishing Manager for New Riders Publishing and the author of several books on Unix and TCP/IP. He can be reached on Compuserve at 74507,3713.