## 10.4. A Menu-Based Command Generator

In this section, we look at a general use of the system() and getline functions to implement a menu-based command generator. The object of this program is to give unsophisticated users a simple way to execute long or complex UNIX commands. A menu is used to prompt the user with a description of the task to be performed, allowing the user to choose by number any selection of the menu to execute.

This program is designed as a kind of interpreter that reads from a file the descriptions that appear in the menu and the actual command lines that are executed. That way, multiple menu-command files can be used, and they can be easily modified by awk-less users without changing the program.

The format of a menu-command file contains the menu title as the first line in the file. Subsequent lines contain two fields: the first is the description of the action to be performed and the second is the command line that performs it. An example is shown below:

>     $ cat uucp_commands
>     UUCP Status Menu
>     Look at files in PUBDIR:find /var/spool/uucppublic -print
>     Look at recent status in LOGFILE:tail /var/spool/uucp/LOGFILE
>     Look for lock files:ls /var/spool/uucp/*.LCK

The first step in implementing the menu-based command generator is to read the menu-command file. We read the first line of this file and assign it to a variable named title. The rest of the lines contain two fields and are read into two arrays, one for the menu items and one for the commands to be executed. A while loop is used, along with getline, to read one line at a time from the file.

>     BEGIN { FS = ":"
>     if ((getline < CMDFILE) > 0)
>     	title = $1
>     else
>     	exit 1
>     while ((getline < CMDFILE) > 0) {
>     	# load array
>     	++sizeOfArray
>     	# array of menu items
>     	menu[sizeOfArray] = $1
>     	# array of commands associated with items
>     	command[sizeOfArray] = $2
>     	}
>     ...
>     }

Look carefully at the syntax of the expression tested by the if statement and the while loop.

>     (getline < CMDFILE) > 0

The variable CMDFILE is the name of the menu-command file, which is passed as a command-line parameter. The two angle-bracket symbols have completely different functions. The "<" symbol is interpreted by getline as the input redirection operator. Then the value returned by getline is tested to see if it is greater than (">") 0. It is parenthesized on purpose, in order to make this clear. In other words, "getline < CMDFILE" is evaluated first and then its return value is compared to 0.

This procedure is placed in the BEGIN pattern. However, there is one catch. Because we intended to pass the name of the menu file as a command-line parameter, the variable CMDFILE would not normally be defined and available in the BEGIN pattern. In other words, the following command will not work:

>     awk script CMDFILE="uucp_commands"  -

because CMDFILE variable won't be defined until the first line of input is read.

Fortunately, awk provides the -v option to handle just such a case. Using the -v option makes sure that the variable is set immediately and thus available in the BEGIN pattern.

>     awk -v CMDFILE="uucp_commands"  script -

If your version of awk doesn't have the -v option, you can pass the value of CMDFILE as a shell variable. Create a shell script to execute awk and in it define CMDFILE. Then change the line that reads CMDFILE in the invoke script (see below) as follows:

>     while ((getline < '"$CMDFILE"') > 0 ) {

Once the menu-command file is loaded, the program must display the menu and prompt the user. This is implemented as a function because we need to call it in two places: from the BEGIN pattern to prompt the user initially, and after we have processed the user's response so that another choice can be made. Here's the display_menu() function:

>     function display_menu() {
>     	# clear screen -- comment out if clear does not work
>     	system("clear")
>     	# print title, list of items, exit item, and prompt
>     	print "\t" title
>     	for (i = 1; i <= sizeOfArray; ++i)
>     		printf "\t%d. %s\n", i, menu[i]
>     	printf "\t%d. Exit\n", i
>     	printf("Choose one: ")
>     }

The first thing we do is use the system() function to call a command to clear the screen. (On my system, clear does this; on others it may be cls or some other command. Comment out the line if you cannot find such a command.) Then we print the title and each of the items in a numbered list. The last item is always "Exit." Finally, we prompt the user for a choice.

The program will take standard input so that the user's answer to the prompt will be the first line of input. Our reading of the menu-command file was done within the program and not as part of the input stream. Thus, the main procedure of the program is to respond to the user's choice and execute a command. Here's that part of the program:

>     # Applies the user response to prompt
>     {
>        # test value of user response
>        if ($1 > 0 && $1 <= sizeOfArray) {
>     	# print command that is executed
>     	printf("Executing ... %s\n", command[$1])
>     	# then execute it.
>     	system(command[$1])
>     	printf("<Press RETURN to continue>")
>      	# wait for input before displaying menu again
>     	getline
>        }
>        else
>     	exit
>        # re-display menu
>        display_menu()
>     }

First, we test the range of the user's response. If the response falls outside the range, we simply exit the program. If it is a valid response, then we retrieve the command from the array command, display it, and then execute it using the system() function. The user sees the result of the command on the screen followed by the message "<Press RETURN to continue>." The purpose of this message is to wait for the user to finish before clearing the screen and redisplaying the menu. The getline function causes the program to wait for a response. Note that we don't do anything with the response. The display_menu() function is called at the end of this procedure to redisplay the menu and prompt for another line of input.

Here's the invoke program in full:

>     awk -v CMDFILE="uucp_commands"  '# invoke -- menu-based
>                                       # command generator
>     # first line in CMDFILE is the title of the menu
>     # subsequent lines contain: $1 - Description;
>     # $2 Command to execute
>     BEGIN { FS = ":"
>     # process CMDFILE, reading items into menu array
>       if ((getline < CMDFILE) > 0)
>     	title = $1
>       else
>     	exit 1
>       while ((getline < CMDFILE) > 0) {
>     	# load array
>     	++sizeOfArray
>     	# array of menu items
>     	menu[sizeOfArray] = $1
>     	# array of commands associated with items
>     	command[sizeOfArray] = $2
>       }
>       # call function to display menu items and prompt
>       display_menu()
>     }
>     # Applies the user response to prompt
>     {
>        # test value of user response
>        if ($1 > 0 && $1 <= sizeOfArray) {
>     	# print command that is executed
>     	printf("Executing ... %s\n", command[$1])
>     	# then execute it.
>     	system(command[$1])
>     	printf("<Press RETURN to continue>")
>      	# wait for input before displaying menu again
>     	getline
>        }
>        else
>     	exit
>        # re-display menu
>        display_menu()
>     }

>     function display_menu() {
>     	# clear screen -- if clear does not work, try "cls"
>     	system("clear")
>     	# print title, list of items, exit item, and prompt
>     	print "\t" title
>     	for (i = 1; i <= sizeOfArray; ++i)
>     		printf "\t%d. %s\n", i, menu[i]
>     	printf "\t%d. Exit\n", i
>     	printf("Choose one: ")
>     }' -

When a user runs the program, the following output is displayed:

>     UUCP Status Menu
>             1. Look at files in PUBDIR
>             2. Look at recent status in LOGFILE
>             3. Look for lock files
>             4. Exit
>     Choose one:

The user is prompted to enter the number of a menu selection. Anything other than a number between 1 and 3 exits the menu. For instance, if the user enters "1" to see a list of files in uucp's public directory, then the following result is displayed on the screen:

>     Executing ...find /var/spool/uucppublic -print
>     /var/spool/uucppublic
>     /var/spool/uucppublic/dale
>     /var/spool/uucppublic/HyperBugs
>     <Press RETURN to continue>

When the user presses the RETURN key, the menu is redisplayed on the screen. The user can quit from the program by choosing "4".

This program is really a shell for executing commands. Any sequence of commands (even other awk programs) can be executed by modifying the menu-command file. In other words, the part of the program that might change the most is extracted from the program itself and maintained in a separate file. This allows the menu list to be changed and extended very easily by a nontechnical user.

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