Automating frequent tasks

Making menus

Starting from our current example, it is not difficult to turn the script into a fully interactive program with menus. We have already seen most of the structures we need: all that is necessary is to put them together in a different order.

The general structure of a batch mode script is as follows:

   Define constants (variables that will not change)
   Define functions (routines to handle specific jobs)
   Set traps
   get command line options with getopts
     use options to set control variables
   for all in $*
The only element of repetition is the loop at the end, which repeats for each file passed to the script as an argument.

A menu driven script behaves differently:

   Initialize variables and define functions
   Repeat (until some ``exit'' state is reached)
     Display a menu
     Get the user's choice
     Do something with the choice (change state or call function)
   On ``exit'' close files and quit
This process, an endless loop, is called a mainloop. The menu is displayed, then a function like getc (described in ``Reading a single character from a file or a terminal'') is used to retrieve a single keystroke. Such a function may either grab the first key the user presses, or let them correct the entry and press <Enter> before accepting input. (There are arguments for and against both strategies. In general, you should always give your users an opportunity to check their input, and correct any mistakes they may have made.)

Depending on the value of the key, an option is selected from a case statement. Each option either sets a variable, or calls a function (called a callback) which does something in the background, ``behind'' the menu. Finally, if the option to quit is selected, the break statement is executed to quit the loop.

Here is part of a menu based script, containing the mainloop:

   282 : done
   283 : if [ $help = "yes" ]
   284 : then
   285 :   _help
   286 :   exit 1
   287 : fi
   288 : if [ $batch = "yes" ]
   289 : then
   290 :   analyze
   291 :   exit 0
   292 : fi
   293 : #
   294 : #---------- enter the mainloop ------------------------
   295 : #
   296 : while :
   297 : do
   298 :         echo $CLS
   299 :         echo "
   300 :
   301 : 	${HILITE}Readability Analysis Program${NORMAL}
   302 :
   303 : 	Type the letter corresponding to your current task:
   304 :
   305 :   	f	Select files to analyze [now ${HILITE}$fname${NORMAL} ]
   306 :   	p	Perform analyses
   307 :   	l	switch ${next_log_state} report logging [now ${HILITE}$log${NORMAL}]
   308 :   	q	quit program
   309 :
   310 :
   311 : 	=======>"
   312 :         getc char
   313 :         case $char in
   314 :         'f')	getloop=1
   315 :             	get_file 	;;
   316 :         'p')	analyze
   317 : 			strike_any_key	;;
   318 :         'l')	toggle_logging	;;
   319 :         'q')	break		;;
   320 :         *)	continue	;;
   321 :         esac
   322 : done
   323 : clear
   324 : exit 0
The first part of this extract, lines 283 to 292, check to see whether help is to be printed, or the script is to be run in batch mode: if the answer to the latter question is yes, a function called analyze is called and the script exits without presenting a menu. Then we see the mainloop, from line 284 to 324. $endloop is initially set to NO, so the test at the top of the loop evaluates to true: therefore the body of the do loop is executed at least once.

Within the loop, a menu is printed and then the script waits for the user to press a key. The character that is read is used to trigger a case statement (lines 312 to 321) that either modifies the state of some variables, or calls a function (like analyze, which does the analysis work, or getfile, which prompts the user for the name of a file to work on, or strike_any_key, which prints a message like ``Press any key to continue'').

Note the use of reverse video in the menu to emphasize important information. In general, you should try to make menu driven interfaces guide the user through to the next step in an intuitive and natural manner. One way of doing this is to highlight the important default information (like the file to be processed), in close proximity to the option that changes it (like the option to select a file to analyze).

Also worth noting is the use of ``toggle'' variables, that switch an additional feature on or off. The variables $log and $next_log_state perform this function for logging. They are switched within a separate function, toggle_logging:

   83 : toggle_logging ()
   84 : {
   85 :   log=$next_log_state
   86 :   case $log in
   87 :     ON)  next_log_state=OFF ;;
   88 :     OFF) next_log_state=ON  ;;
   89 :   esac
   90 : }
log indicates whether output is to be logged to a file; next_log_state is used in a message display that tells the user whether they can switch logging on or off. (By definition, next_log_state and log must be in opposite states at all times.)

It is very easy for a mainloop to become too big to read. For this reason, any task that has more than one step is farmed out to another function. This includes the display of submenus. For example, get_file uses a menu to select a file to check:

   145 : get_file()
   146 : {
   147 :   while :
   148 :   do
   149 :     echo $CLS
   150 :     echo "
   151 :
   152 : 	${HILITE}Select a file${NORMAL}
   153 :
   154 : 	Current file is: [${HILITE} $fname ${NORMAL}]
   155 :
   156 : 	Type the letter corresponding to your current task:
   157 :
   158 :   	[space]	Enter a filename or pattern to use
   159 :   	l	List the current directory
   160 : 	c	Change current directory
   161 :   	q	quit back to main menu
   162 :
   163 :
   164 :     =======>"
   165 :     getc char
   166 :     case $char in
   167 :     ' ')    get_fname 		;;
   168 :     'l')    ls | ${PAGER:-more} ;;
   169 :     'c')    change_dir 		;;
   170 :     'q')    break 		;;
   171 :     *)      			;;
   172 :     esac
   173 :     strike_any_key
   174 :   done
   175 : }
This function contains a couple of features that do not appear in the mainloop. Notably, it calls a routine for changing directory, a routine for getting a filename, and lists the contents of a directory (using the pager indicated by the environment variable PAGER, or more if PAGER is not set).

Next topic: Assigning variables default values
Previous topic: Expanding the example: counting words

© 2003 Caldera International, Inc. All rights reserved.
SCO OpenServer Release 5.0.7 -- 11 February 2003