Cover V02, I06
Article
Listing 1
Listing 2
Listing 3
Listing 4
Listing 5
Listing 6

nov93.tar


A Form Letter Facility for E-Mail

Robert Ward and Scott Graham

Whether electronic or paper-bound, forms contribute a useful regularity and structure to office communications. Forms are especially useful to those who must supply detailed information about an unfamiliar subject or event. Thus, in a traditional paper-based office, when the bookkeeper is impressed into phone duty because the phone staff is out with the flu, a well-designed order form can save the day. By itemizing and structuring all the details that must be captured to complete an order, the form improves the bookkeeper's chance of getting the order right. Similarly, when users are faced with reporting a technical problem or requesting a technical service, a well-designed electronic form can help them capture all the details that the system administrator needs.

This article presents a general purpose form-letter program. With this system (built on top of the standard mail program), administrators can create easy-to-use electronic forms to facilitate common user messages -- such as bug reports, resource requests, and account changes.

While the frmlet package is useful to administrators, it is entirely general and user-maintained. Users create and maintain their own form templates. Each new or modified template is automatically checked for minimal correctness, and, if it passes, is automatically added to the list of forms available for use.

To send a form, the user selects a form from a list of available forms (templates), fills in the blanks (using his or her standard editor), and exits. The frmlet application then mails the form to the "owner," whose name is embedded in the form template.

While useful in its own right, frmlet is also a nice illustration of directory and menuing conventions and techniques that are widely used in various UNIX applications.

The Application

The forms package is two top-level scripts designed to be embedded in a menuing system. The frmlet script (Listing 1) manages a session in which a user fills in and mails an existing form. The maintfrm script (Listing 2) manages a maintenance session, during which a user could create, edit, or delete a form template.

The frmlet script takes a directory as an argument. This directory should contain a set of related form templates. Passing the directory as a parameter allows the same script to be used for several different categories of forms. The marketing department, the shipping department, and the customer support department can each have their own "private" set of forms. Widely used forms can be in a "enterprise-wide" directory. Thus the script begins by testing for this parameter and making the selected directory the current directory. (The LOCALCMDS and MAILCMD variables simply isolate certain system-dependent details.)

The frmlet script then calls a utility script, dirmnu (Listing 3), which does all the real work. The dirmnu script dynamically builds a menu, based on what it finds in the current directory, and returns the file name corresponding to the user's selection on stdout. Both top-level scripts invoke dirmnu, which we cover in more detail later.

The next line in frmlet

receiver = `fgrep "Send to:" \
$form_file | ....

exemplifies a common UNIX "trick" -- using grep to extract data marked by some user-created convention. In this instance, we decided that every template would contain the name of the user to whom it would be sent on a line that begins with the words "Send to:" (much like the mail header conventions). Some of our lp administration scripts assume that lines containing an exclamation mark are description lines for use in a menu. In other applications, we've used a leading hash mark (#) to mark description lines. Whatever the convention, the technique is the same: keep the data in the user-maintained file (where it's more apt to be updated properly), and extract it based on some simple pattern.

Note that we include head -1 in the pipe to insure that we get only the first occurrence of "Send to:" and then use sed to remove the mark, leaving just the recipient's name.

The script next performs some integrity checks, and then invokes the mailer. The options in this MAILCMD line work for our system, but might need to be modified to work on yours. The code assumes that the mailer will invoke the editor and can "include" a file in the mailer's buffer without modifying the original file. The options are:

-s <subject>
-i <include file> 

The dirmnu Script

In many ways, dirmnu is the most interesting of the three scripts. This utility script builds a picklist from the file names in a selected directory, and then returns the filename corresponding to the user's selection on stdout. The dirmnu script is invoked with three arguments: a title (which will appear at the top of the picklist), a prompt (which will appear at the bottom of the picklist), and a pattern (which is used to filter out unwanted files from the picklist).

Normally, scripts write user-directed output to stdout. This utility, however, is designed to be "backquoted" in other scripts, and thus must return only its result on stdout (which is implicitly redirected by the backquoting mechanism). Accordingly, all user-oriented output is directed to stderr ( >&2 ). It might be more technically correct to direct the output to /dev/tty. Either stream will show on the terminal.

After echoing the title to the screen and checking for an empty directory (an error condition), dirmnu uses awk and pr to format a list of files in columns. Our first draft used a very simple command in which pr actually did most of the work:

ls $pattern |  awk '(print " " $0)' |  \

pr -t -n\)2 -i20 -4 >&2h

The -t suppresses leading and trailing linefeeds; the -n\)2 puts a two-digit number and a parenthesis in front of each filename; the -i20 option sets tabstops; and the -4 option organizes the output as four columns. We wouldn't have needed awk at all, except that we wanted a space between the number and the filename. The tiny awk script inserts a space in front of each filename.

The more complicated version we ultimately used in Listing 3 computes the number of columns dynamically, based on the length of the longest filename. The script pipes ls's output into an awk script that prints the length of the longest filename. It then uses expr to compute the number of columns (all of our terminals are 80-column devices). The result is a parameter to the display formatting pipeline.

After displaying the numbered picklist, dirmnu uses another little utility script, getnum (Listing 4), to get the user's response. The getnum script takes two numbers and a prompt. The numbers represent the range of valid responses. Like dirmnu, getnum is designed to be backquoted, and sends all interactive output to stderr. This utility will loop, continuously redisplaying the prompt, until the user enters a valid response. If the user makes too many mistakes, the menu will scroll off the top of the display. A stronger design would use curser positioning to always draw the prompt in the same location.

By using an awk script to check the input range, we get string-to-integer conversion, range checking, and type validation all for the price of one.

Finally, dirmnu uses the getnum result in a head/tail pipe to select the line corresponding to the selected file. This usage of head and tail is almost a shell script idiom.

This version of dirmnu uses the filename for the corresponding menu entry. For some applications (and users), filenames are just too cryptic. If you expand your file convention to include a pattern that indicates an embedded file description, you can make dirmnu generate menus consisting of arbitrary descriptions. For example

grep "DESC:" $pattern | sed "s/DESC:/  /" | \

pr -t -n\)2 -i"  "1

should produce a one-column menu where each item is a description line extracted from the corresponding file. The code also depends on another small utility script, askyn (Listing 6), which prompts the user for a response and repeats until the user supplies one.

The Maintenance Script

Though long, maintfrm (Listing 2) is quite straightforward. The option to this script selects one of three modes: create, edit, or delete. When a template is to be edited, it is first copied to a temporary file, and when the modifications are complete and correct, the temporary file is copied over the original template and then removed.

We used a little C utility, tmpname.c (Listing 5), to generate a unique name for the temporary file. You could get a similar result with

TEMPFILE=FORM$$.

The $$ method, however, isn't guaranteed to produce a unique file -- tmpname is. For example, if invoked by process number 101, this script would unintentionally overwrite any template named FORM101. (See Leor Zolman's article in Sys Admin, 1.4 [Nov/Dec 1992] for more information on tmpname.) tmpnam makes use of the standard library function tempnam, so you might wish to examine the man page for that.

Like frmlet, this script uses dirmnu to generate a picklist of existing forms.

Both the create and edit options "syntax check" the new form template before installing it. Each option uses grep to look for the pattern "Send To:". If this pattern is missing, the frmlet script won't know how to address a filled-in form. It notifies the user that a "Send To:" is required and calls askyn (Listing 6) to see if it should continue.

To ensure that the temporary file gets properly deleted, this script traps all significant interrupts.

Summary

While this forms system offers significant benefits to system administrators, it is also general enough to be interesting to users. That utility is in itself an advantage to the administrator -- if your users are already familiar with the forms system, they are more likely to use it when a system-related need arises.

In many ways, this version is still a prototype. We've really only addressed the skeleton of the application -- the user interface issues remain unsolved. How do you make it easy to fill in the blanks? How do you partition the forms into related sets? Where does it fit into the existing menu system?

Even so, we've installed it as is. In classic testimony to the effectiveness of the UNIX utility set, frmlet yields a surprising amount of functionality from a tiny amount of code.

About the Authors

Robert Ward is president of R&D Publications and senior editor of Sys Admin. He is the author of Debugging C, an introduction to scientific debugging, and co-author, with William Smith, of Windows Custom Controls. He has done consulting work in software engineering and data communications and holds a Master's degree in Computer Science from the University of Kansas.

Scott Graham received a BS degree in computer science, computer engineering, and mathematics from Graceland College in Lamoni, IA. He is a graduate student in Computer Science at the University of Kansas and is a Programmer/Analyst for R&D Publications.