Portability


A Portable VMS-Style Input Line Routine

Robert Bybee


Robert Bybee is Senior Electrical Engineer at Scientific Games, Atlanta Georgia, where he is involved with hardware and software design of point-of-sale terminals used in state lottery systems. He was previously with Chromatics, a manufacturer of color graphic computer systems, and Telecorp Systems, which builds voice response and telephone call processing computers. He has a BSEE from the University of Virginia and is currently seeking an MBA at Georgia State University. Robert has been programming computers for 23 years, with 10 years of C experience, and has been designing hardware for over 15 years. He can be contacted at 5011 Brougham Court, Stone Mountain GA 30087.

When a program needs to read a line of text input, the programmer often resorts to using a standard C library function, such as gets(). Depending on the operating system, this function will permit only minimal input editing, perhaps limited to the backspace key.

The DEC VAX-VMS operating system, however, provides far more sophisticated facilities to edit command-line input. The MS-DOS world has produced similar facilities, such as the public-domain DOSEDIT, Peter Norton's NDE, and the DOS-KEY program in MS-DOS 5.0. Of course, if you're writing code for embedded systems or non-DOS machines, you may not be able to use any of these.

The get_str() routine presented here duplicates the functions found in these systems. It provides a way to gather commands or input strings from the user, emulating most of the VMS keystrokes. You can change the code easily to respond to other keystrokes if you don't like VMS-style editing.

Command Line Recall

get_str()'s most useful feature is its ability to scroll through previously entered commands, edit them, and transmit them again. This functionality comes in handy when you make a typing error and must retype almost the same command. Moreover, when running test programs, I find myself retyping the same series of commands, time and time again. With the touch of a few cursor keys, get_str() redisplays these earlier commands for you to enter. The number of lines in its "history buffer" is a compile-time decision.

When entering a command, or after recalling a previous command, you often need more than a simple backspace key to edit the command. get_str() supports left and right cursor motion, moving to the start and end of the line, and both insert (push-right) and overtype modes of text entry.

Display Dependencies

I wrote get_str() to support a "least common denominator" output device. The routine will run on a PC display screen, a VT100 terminal connected to a serial port, or nearly any other type of terminal or display, provided the display can backspace (move the cursor left) without erasing the character that the cursor lands on. get_str() makes no other assumptions about the control-codes accepted by its output device.

Keyboard Dependencies

The next problem is the type of keyboard device to support. It is possible to avoid all keyboard dependencies by forcing the operator to type CTRL-characters for all editing operations. However, people accustomed to using the cursor keys will find such a solution unacceptable.

Most terminals have at least a backspace key and four cursor arrow keys. VMS lets the left and right arrow keys move the cursor in the current line. It uses the up and down arrows to move forward and backward through previous commands. And it uses the DELETE key on a VT100 to delete one character to the left of the cursor. Under VMS, all other command-line editing functions are performed using control keys. The key assignments are:

CTRL-A  switches between insert
        and overtype mode
CTRL-H  moves to the beginning
        of the line
CTRL-E  moves to the end of the line
CTRL-R  recalls the last line entered
CTRL-X  erases the line
get_str() duplicates all of these, except CTRL-H. The backspace key on a VT100 terminal and PC keyboard both generate this character. There are probably historical reasons why VMS uses this key to move the cursor to the start of the line, and uses DELETE to erase one character.

All too often, I find myself hitting the BACKSPACE key instead of DELETE when I want to delete a character under VMS. I can't fix VMS, but I can fix get_str(). In this code, CTRL-B moves the cursor to the beginning of the line, and both DELETE and BACKSPACE erase one character. If you want true VMS emulation in this regard, change the value CTRL ('B') to BACKSPACE_KEY (line 100), and remove the reference to BACKSPACE_KEY on line 79.

Code Description

I wrote the code in this article to compile under either Turbo C 2.0 or Borland C++ 2.0. If you port the code to a different compiler or environment, you need to change only the sys_getchar() and sys_putchar() functions. To support a different keyboard, the get_char_esc() routine would also require attention.

Listing 1 contains the get_str() function and supporting routines. Lines 22-25 define four constants, UP_ARROW, DOWN_ARROW, RIGHT_ARROW, and LEFT_ARROW, which pass the notion of a cursor movement between get_chr_esc() and get_str(). I defined these values to be the same as the PC keyboard scan codes for the four arrow keys, but they could really be any four non-ASCII constants if you don't need to read input from a PC keyboard.

Lines 30-33 declare the size of the history list, called lastlines, which stores previously entered commands. The first and last entries in this list must be blank, so declaring MAX_RECALL as 22 allows for the two blank strings plus 20 prior commands. The RECSUB macro generates a subscript into the list and handles modulo arithmetic so that the subscript is always between 0 and MAX_RECALL minus one.

RECALL_LEN defines the longest line you can type into the get_str() routine. You can make it as long as you like. You can also expand MAX_RECALL to permit more than 20 commands in your buffer. All it costs is memory.

The VMS command interpreter does not treat its history list as a circular buffer. You can't scroll around in one direction indefinitely. You can use the up-arrow until you reach the oldest command in the list, then you get a blank line and the up-arrow stops working. The down-arrow takes you one step toward the most recently entered command, and it too stops working at the end of the list.

The get_str() routine begins on line 60. You pass it the address of the buffer in which to store the input line, and the length of that buffer. On line 73, get_str() goes into a "forever" loop, where it gathers up characters and performs the editing functions. It breaks out of the loop when RETURN is pressed.

After calling the routine get_chr_esc(), which gets a character from the user, get-str() enters a large if statement that handles all of the cursor keys and control-characters. If the character isn't anything special, the function stores it in the buffer.

The other functions in Listing 1 are fairly simple. cursor_right() moves the cursor one position to the right by sending out the character that is already under the cursor. This operation moves the cursor without relying on any terminal-dependent control or escape codes.

cursor_left ()moves the cursor one position to the left. It depends upon the terminal responding to CTRL-H as a backspace. In a string in C, the character '\b' is a backspace, CTRL-H, hex value 08. Note that this value does not erase the character to the left of the cursor. To do that, as in the function clear_line(), a three-character string is sent out: backspace, space, backspace.

get_str() calls the final function in Listing 1, get_char_esc(), to get a character. This function in turn calls sys_getchar() to get a character from the user, and passes most characters through unchanged. If, however, get_char_esc() detects an escape (ESC), it looks at the next two characters to see if they represent a VT100 terminal's arrow keys. A VT100 puts out the following escape codes when you press a cursor key:

up    ESC [ A
down  ESC [ B
right ESC [ C
left  ESC [ D
get_char_esc() translates these three-character sequences into a single integer, so get_str() can process it easily. For simplicity, these integers are the same four integer values the BIOS produces when a PC's arrow keys are struck. If you plan to use get_str() with a terminal whose arrow keys don't produce the VT100 escape sequences above, you must modify get_char_exc().

sys getchar And sys_putchar

get_str() uses these two functions for all of its input and output. I isolated them in this fashion for portability, since these functions were voted "most likely to change" when the boss says, "By the way, we're going to OS/2 tomorrow."

On lines 37-47 of Listing 2, sys_putchar() sends one character to the display. Its only frill is that it expands a line-feed character, '\n', to a carriage-return and line-feed pair. Depending on how you use get_str(), you may want to prevent this translation.

sys_getchar(), found on lines 50-63, waits for a character from the user. In the MS-DOS environment, the function calls the Turbo C bioskey() function to wait for the next keystroke. bioskey() returns a 16-bit integer, whose high byte is the PC keyboard scan code, and the low byte is the ASCII value of that character (if any), or 00 if it's a non-ASCII key. sys_getchar() checks that the key is ASCII, and if so, strips off the scan code before returning it.

If you need to do background processing while waiting for the next character, you could modify sys_getchar() to call an idle function until a key is ready.

Listing 2 also contains a simple main() routine for testing the code. It reads an input line using get_str() and prints the results in quotes so you can see what was received. To leave the program, type quit.

Storing More Commands

VMS stores the most recently entered command into the bottom of the history buffer, so that a single up-arrow keystroke will recall it. If this command matches the previously entered command, get_str() doesn't store it, since doing so would only waste space in the buffer.

As the program stands, if you enter two or more commands repeatedly,

EDIT XX
RUN XX
these two commands will eventually occupy the entire history list. You might consider improving the code here: before entering a command into the list, remove the command if it already exists anywhere in the list. While this would allow the list to hold more unique commands, it would be the non-VMS thing to do.

Limiting The Search

Under VMS and in get_str(), striking an up-arrow or down-arrow immediately replaces the input line with the previous (or next) entry from the history list. If you accidentally hit an up-arrow while typing a command, everything you've typed is lost.

Some MS-DOS keyboard enhancers have a different philosophy. If you type one or two characters and then press the up-arrow, you will scroll through a subset of the history list. Your subset is limited to those entries beginning with the characters you have already typed. If you wanted to recall a command that began with the letter D, you would simply type "D" and a few up-arrows until the command appeared.

This feature resembles the "command completion" functionality built into some flavors of UNIX. On those systems, you can type a partial filename or command, press ESC, and the system will fill in the remainder of the name if it can.

You could modify get_str() to include this functionality. The code that handles the up and down arrow keys, lines 120-142 of Listing 1, would skip any entries that didn't match the partial input line. You would also keep a copy of that partial line, otherwise the next up-arrow keystroke would overwrite it.

Something as simple as an input-line reader can really enhance the usefulness of your programs. At the time I wrote this code, I was working for a die-hard VAX person, who was very pleased to see a VMS-like front-end attached to our embedded-system diagnostics. If your users are familiar with VMS, the get_str() routine will warm their hearts, too.