Chapter 15. Interact

In earlier chapters, interact was used in a very simple way. In reality, the interact command simplifies many tasks and opens up a world of new problems that can be solved. In this chapter, I will describe the more common uses for interact. In the next chapter, I will focus on using interact with multiple processes.

The interact Command

In Chapter 3 (p. 82), I introduced the interact command in the context of a script to automate ftp. The script carried out the initial part of the procedure—entering the user name and password—and then returned control to the user by calling interact.

The interact command is much more flexible than that example demonstrated. interact can also:

  • execute actions when patterns are seen from either a user or a spawned process

  • allow the user to take control of a spawned process, and return control to the script for further automated interaction, any number of times

  • suppress parts or all of an interaction

  • connect two or more spawned processes together, pairwise or in other combinations

Many of the things interact does can also be done by expect, but interact can do them more easily and efficiently. In this sense, interact is a higher-level command than expect. In other ways, expect and interact are duals. They do the same thing but have a very different way of looking at the world. As I explain interact, I will frequently bring up expect to compare the similarities and contrast the differences between the two.

In its simplest form, the interact command sets up a connection between the user and the currently spawned process. The user’s terminal is put into raw mode so that the connection is transparent. It really feels like the user is typing to the process.

In its basic form, the interact command connects a user and spawned process.

Figure 15-1. In its basic form, the interact command connects a user and spawned process.

If the spawned process exits, the interact command returns and the next line of the script is executed. In a simple script such as the anonymous ftp script (aftp), the interact command is the last line in the script, so the script simply exits when interact does.

Simple Patterns

Like the expect command, the interact command can execute actions upon detecting patterns. However, interact and expect behave very differently in many respects.

The syntax for specifying patterns and actions is similar to expect. Patterns and actions are listed as pairs of arguments. For example, the following interact command causes the date to be printed if ~d is typed by the user.

interact "~d" {puts [exec date]}

By default, a matched pattern is not sent on to the process. (Later, I will show how to change this behavior.) Thus, in this example, the process never sees the ~d.

Unlike the expect command, interact continues after matching a pattern and executing an action. interact continues shuttling characters back and forth between the user and process. It also continues matching patterns. In the example above, the ~d pattern can be matched again and again. Each time, the action will execute.

As with the expect command, additional pattern-action pairs may be listed. Also, all the arguments may be surrounded by a pair of braces, provided they do not all appear on the same line as the command. The -brace convention from the expect command is also supported. Here are two ways of expressing the same command:

interact "~d" {exec date} "~e" {eproc}


interact {
    "~d"        {exec date}
    "~e"        {eproc}
}

There are all sorts of useful effects that can be accomplished with these patterns. A very simple one is translating characters. For example, one of the problems with the UNIX terminal driver is that you can have only one erase character. Yet, it is often convenient to have both the backspace key and and the delete key generate erase characters—especially if you frequently switch from using one badly-designed keyboard to another. Assuming that you have defined delete as the erase character already (using stty), the following script translates the backspace character to a delete character. This effectively provides you with two erase characters.

interact "" {send "177"}

This technique is often useful when you have to connect (e.g., telnet) to another computer that does not support remapping of keys at all. If you are used to pressing delete but the remote system wants to see backspace, just reverse the above script:

interact "177" {send ""}

Single character translations are not the only possibility. For example, the following interact command maps all the lowercase keys to their Dvorak equivalents. There is no algorithmic mapping so each translation is explicitly spelled out.

interact {
    "q" {send "`"}   "w" {send ","}   "e" {send "."}
    "r" {send "p"}   "t" {send "y"}   "y" {send "f"}
    "u" {send "g"}   "i" {send "c"}   "o" {send "r"}
    "p" {send "l"}   "s" {send "o"}   "d" {send "e"}
    "f" {send "u"}   "g" {send "i"}   "h" {send "d"}
    "j" {send "h"}   "k" {send "t"}   "l" {send "n"}
    ";" {send "s"}   "`" {send "-"}   "z" {send ";"}
    "x" {send "q"}   "c" {send "j"}   "v" {send "k"}
    "b" {send "x"}   "n" {send "b"}   "," {send "w"}
    "." {send "v"}   "/" {send "z"}
}

Patterns can differ in length. For example, you can make abbreviations that automatically expand to their longer forms. Imagine that you are a physiologist writing a report on “glomerular nephritis”. Rather than typing this phrase literally each time, you can define a few keystrokes which will expand into the full phrase.

interact {

    "~gn"            {send "glomerular nephritis"}

    "~scal"            {send "supercalifragalisticexpealidocious"}
    "~adm"            {send "antidisestablishmentarianism"}
    "relief"            {send "rolaids"}
}

The effect of this is very similar to that offered by editors such as vi’s autoabbreviation mode. However, unlike these programs, interact’s abbreviations work inside of any program that is running underneath Expect. If you spawn a shell and then interact with it, any program started from that shell is able to use these abbreviations.

Incidentally, I often start interact patterns with some unusual character such as a tilde (~). It is a good idea to use a special character like this. It protects you from unthinkingly entering a sequence for which interact is watching. Characters or sequences like these are knowns as escapes. Unfortunately, there is no best escape character or sequence. Tilde, for example, is often used in specifying home directories. If ~gn was someone’s home directory, for instance, I would not be able to type it using the interact command above. It would always be intercepted and translated to glomerular nephritis.

One solution is to “double up” the escape to generate the same literal escape. Adding the following line to the example above guarantees that you can always be able to send a tilde followed by anything else by just typing an extra tilde first.

    "~~"        {send "~"}

With this addition, you can send ~gn by entering ~~gn even though ~gn is a pattern itself.

If you cannot give up a single character, sometimes two or even more characters work better. It is also possible to mix escapes. For example, some can start with one tilde, others with two tildes. And it is not necessary that all escapes be the same. For instance, some can start with % or ~, and yet others can have none at all as in the Dvorak script.

Actually, interact does not know anything about escapes. To it, characters are just characters. While I have been using the term “patterns”, interact’s default patterns have no wildcard matching at all and work in the style of the exact strings supported by the -ex flag in the expect command. For example, the following command runs starproc only when an asterisk is typed. No other character matches the "*“.

interact "*" starproc

Backslash is still interpreted directly by Tcl and therefore retains its special properties. For example, matches a return,  matches a backspace, and followed by one to three octal digits matches a character specified by its octal value. A backslash itself can be matched by two backslashes or enclosing it in braces. Here are some examples:

interact "\" proc     ;# match backslash
interact "
" proc     ;# match return
interact "\r" proc    ;# match backslash and r character
interact "\
" proc   ;# match backslash and return

With these interact patterns, there is no feedback analogous to expect_out to record what matched. The reason is that you can always tell what matched. The characters only match themselves, so the patterns automatically define the resulting match.

Much like the expect command, the interact command also supports regular expressions. These do provide feedback to the application; however, regular expressions are generally overkill for reading user input. More importantly, matching complicated user input in raw mode is very hard; users expect to be able to edit their input but interact simply passes the editing keystrokes on to the pattern matcher. For this reason, it usually makes more sense to detect these situations before they happen—by disabling raw mode temporarily and using expect_user. I will provide examples of this later on.

Exact Matching

This style of matching that I have used so far is called exact matching. It is simple to use, but you must take precaution that your patterns do not look like any of interact’s flags. Rather than memorizing them, the simplest way to prevent this from happening is to use the -ex flag. This is especially useful when patterns are stored in a variable.

interact {
    -ex $var action1
    -ex "-ex" action2
}

The first pattern above matches whatever is stored in the variable var. The second pattern matches the string -ex itself. Without the -ex flag, interact would look for action2.

Most of the time, you will not need to use -ex and you should not feel obliged to use it. The -ex flag is only necessary to match patterns that would otherwise be accepted as a keyword (e.g., eof, timeout) or a flag (any argument starting with a hyphen).

Matching Patterns From The Spawned Process

Output from the spawned process can be matched by using the -o flag. (Think of the “o” as standing for the “opposite” or “other direction”.) The following command translates unix into eunuchs, vms into vmess, and dos into dog.

interact {
    -o
    "unix" {send_user "eunuchs"}
    "vms"  {send_user "vmess"}
    "dos"  {send_user "dog"}
}

All the patterns before the -o apply to the user keystrokes. All the patterns after the -o apply to the spawned process. For example, the following command is similar to the previous one except that if the user accidentally types one of the humorous nicknames, it is translated back to the correct name. Notice that the first three actions use send to send to the spawned process. The latter three actions use send_user to send to the user.

interact {
    "eunuchs {send "unix"}
    "vmess   {send "vms"}
    "dog     {send "dos"}
    -o
    "unix" {send_user "eunuchs"}
    "vms"  {send_user "vmess"}
    "dos"  {send_user "dog"}
}

This example is artificial and may not seem to make a convincing case for using -o. In practice, matching output from a spawned process almost always requires using regular expressions, in part because process output can be very verbose.

Earlier I said that regular expressions are hard to use from interact. That is only true when reading keystrokes from users. Spawned processes, on the other hand, never do editing so the associated complexities disappear. Regular expressions are an extremely convenient way of matching output from spawned processes. I will describe regular expressions in the next section.

Regular Expressions

The interact command provides the ability to match regular expressions just like the expect command. The syntax is identical. The pattern is preceded by the -re flag. For example, the following sends an X each time the user presses a tilde followed by any other character.

interact -re "~." {send "X"}

The tilde matches a tilde while the period matches any other character. Other special characters work as before.

When using regular expressions, it is possible to find out what characters were typed. Similar to the expect command, the interact command writes its matches to the array interact_out. If the characters ~abc were entered in the previous example, interact_out(0,string) would be set to ~a. The bc would be sent on to the spawned process and would not be saved anywhere.

As before, the -indices flag causes indices to be saved as well. Using this flag, the complete set of assignments would be:

set interact_out(0,start)  "0"
set interact_out(0,end)    "1"
set interact_out(0,string) "~a"

The number 0 plays the same role that it did in the expect command. Parentheses can be used to pick out submatches. Matching strings are stored in interact_out using the indices 1 through 9.

There is no buffer element in interact_out as there is in expect_out. The reason for this is that the interact command processes unmatched characters as they arrive. So by the time a match occurs, unmatched characters are no longer present. Hence, matches always start with the first character remaining in the output buffer. I will revisit this concept shortly (page 331). For now, just remember that the buffer element of interact_out is never written. In theory, it would always have the same value as the "0,string" element so the buffer element is redundant and omitted for efficiency.

A simple use for interact_out is shown in the next script, written to aid a person who was unable to press two keys at the same time. Uppercase letters were not a problem—the user could press the shift-lock key, then the letter, then the shift-lock again to go back to lower case. But control characters could not be entered in a similar fashion because there was no way to lock the control key down.

I used a two character escape—after entering a / and , the next character would be turned into its control equivalent. This worked out well. I did not want to reserve a single character as an escape mechanism—all the single characters might have to be entered, and it was very intuitive to remember since the sequence /X graphically looks like ^X, a common way of writing “control-X” in text.

spawn $env(SHELL)
interact -re "/\\(.)" {           ;# match /char
    scan $interact_out(1,string) %c i
    send [format %c [expr $i-96]]
}

The first command spawns the user’s requested shell. The second command performs the interaction with the appropriate regular expression. The pattern /\\(.) matches the three character sequences beginning with / and . The four backslashes are required to match a literal backslash because inside double-quotes, Tcl represents a backslash by two backslashes and the regular-expression pattern matcher does the same thing. (See Chapter 4 (p. 91) for a complete explanation of this.)

The remaining commands are executed if there is a match. The variable interact_out(1,string) contains the character itself. Tcl’s scan command converts the character to its integer equivalent in ASCII. Subtracting 96 converts a lowercase value to a control value, and then format converts the integer value back to an ASCII character. Finally, the send command sends it on to the spawned process.

This all looks complicated but it works nicely and executes quickly. The script was a very short solution for the problem. This technique can be applied to many other problems. As an exercise, rewrite the Dvorak script (shown earlier) so that it uses a single action instead of one for each possible character.

As I mentioned earlier, regular expressions are used more often to match output from a spawned process than from a user. A simple but common use is to detect and deal with system inactivity monitors. For example, a VMS system used by a colleague of mine logs users out after 15 minutes of inactivity in the top-level shell (called “DCL”). DCL starts warning after 10 minutes of inactivity. The warnings look like this:

%DCL-W-INACT, session inactive for more than 10 minutes - you will be logged out in
5 minutes.

There is no way of disabling this behavior or message from VMS. To avoid the messages, they use the following interact command:

interact -o -re "%DCL-W-INACT.*
" {
    send "show time
"
}

The regular expression matches the warning message whenever it appears. The matched characters themselves are discarded. The .* near the end allows it to absorb all of the error message without specifying the entire text. Next, a very simple command (show time) is sent which resets the inactivity timer.

The result is that users see timestamps every 10 minutes when they are idle. But they see no irritating messages, and more importantly, they are not logged out automatically.

With one additional expect command in the action (expect -re ".*$prompt"), the timestamp and following prompt could be absorbed as well. This might be useful if you are trying to build a seamless application for users who should not have to know what is going on behind the scenes.

The talk program sends messages to the console window describing the user requesting the connection. A typical message looks like this:

Message from Talk_Daemon@lobotomy at 0:55 ...
talk: connection requested by bimmler@research.
talk: respond with:  talk bimmler@research

This message can be captured using the -console flag described in Chapter 13 (p. 300). The following script uses this flag and the resulting message to automatically start an xterm that runs a talk session back to the original user.

spawn -console $env(SHELL)
interact -o -re "talk: respond with:  talk ([^ 
]*)[ 
]" {
        exec xterm -e talk $interact_out(1,string) &
    }

The pattern is not quite trivial. What is not apparent when looking at the message is that talk may pad the username with spaces. Hence, the pattern has to go to some effort to allow the username to terminate at either a space or return character. Can you think of a better pattern for this?

What Happens To Things That Do Not Match

As characters are entered, they are compared to the patterns. Characters that do not match any of the patterns are sent to the spawned process. This includes characters before and after any matches. Any characters involved in a match are not sent to the spawned process. Consider the following interact command, which sends "hugs and kisses" if it sees XOX.

interact "XOX" {send "hugs and kisses"}

If the user types AXOXB, the spawned program receives "Ahugs and kissesB“. The A does not match so it is sent literally. The XOX is replaced by the phrase "hugs and kisses“. And the trailing B does not match so it is sent literally.

If the user types AXOYB, the spawned program receives AXOYB. The pattern XOX cannot match any part of what was entered, so it is all sent on.

If the user types XOXO, the spawned program receives "hugs and kissesO“. The first XOX matches so it is translated and sent on, but the O that remains cannot possibly match XOX so it is sent literally.

If the user types XOXOX, the spawned program receives "hugs and kissesO" following the logic of the previous case. The trailing X can match the pattern XOX if another OX is entered so the interact command waits for more characters to be entered before deciding whether to send on the X or not. Suppose the user instead enters Z. In this case, the X can no longer match so interact sends the string XZ to the spawned process. What would have happened if the user entered another X rather than a Z? The new X would be eligible to match but the previous could no longer match so the previous X would be sent to the spawned process. The user will have typed XOXOXX and the spawned process will have received "hugs and kissesOX“. The last X remains with interact waiting for more characters.

To summarize, the interact command buffers input until it can decide whether characters can or cannot match. If they cannot match, they are removed from the buffer and sent to the spawned process. If they match, their associated action is executed and the characters are removed from the buffer. And if they might match if more characters arrive, nothing at all happens—the characters simply remain buffered. This buffering behavior allows interact to do “the right thing” no matter how slowly or quickly characters are entered.

More Detail On Matching

If two or more patterns can match the same output, only one action will be executed. The action corresponds to the pattern that appears first in the command. This is just the way the expect command works. Here is an example that has both an exact pattern and a regular expression. When abc is entered, action1 is executed while action2 is executed if axc is entered. If the patterns were reversed, the exact pattern would never match since the regular expression would always match first.

interact {
    "abc" action1
    -re "a.c" action2
}

Pattern matching works differently in one respect between expect and interact. The expect command attempts to match a pattern beginning at every place in the input. Only after failing to match anywhere will expect try another pattern. In contrast, if interact fails to match a pattern starting from a particular place in the input, it then tries the next pattern beginning at the same place in the input.

The difference is only noticeable in a situation where two or more patterns can match in the input. This does not normally occur in user input but can be common when making substitutions in spawned process output, such as when using -o. Earlier I showed an example of -o. Here it is again:

interact {
    -o
    "unix" {send_user "eunuchs"}
    "vms"  {send_user "vmess"}
    "dos"  {send_user "dog"}
}

Now consider what happens when a spawned process produces the following output:

This software runs on both vms and unix.

Both unix and vms appear in the output so both can potentially match. Although unix is the first pattern listed, it appears later in the output so it does not match at first. Instead, vms matches and its action is executed. The input from "This software" up to "vms" is removed from the output and interact continues running. It then matches unix and the corresponding action is executed. The resulting output looks like this:

This software runs on both vmess and eunuchs.

With the expect matching algorithm, vms would not have been matched. Reversing the order of the patterns would change this behavior—allowing expect to match vms. But then, expect would misbehave if the order of vms and unix were reversed in the output.

There is no way to make expect work precisely as interact does and vice versa—which is partly why there are two different commands. Generally though, expect and interact are used for very different types of interaction, and their algorithms are well suited to their purpose. The expect command is primarily used to look for responses or prompts where it is important not to be distracted by things in the middle that may otherwise resemble patterns. In contrast, the interact command is not normally used for recognizing prompts but instead makes substitutions wherever they appear.

Echoing

Normally, the interact command depends on the spawned process to echo characters. For example, when you have spawned a shell and are interacting with it through the interact command, all the printable characters that you type are echoed by the shell. Nonprintable characters are not usually echoed back but instead invoke special actions such as erasing previous characters or generating signals. As with echoing, this special processing of nonprintables is also handled by the shell. The interact command does no special processing. It just shuttles characters back and forth.

The interact command works the same way when matching patterns. That is, no special action is taken to echo characters. However, because the characters are being buffered and not sent on to the spawned process, you will not see them appear on your screen. This is similar to the way the tip program works. tip does not echo its escape character unless additional characters are entered that force the escape not to match any known sequence.

For patterns that match short character sequences, the lack of echoing is rarely a problem. In most cases, users do not need to see a single character being echoed before they enter the next one. But patterns that are long enough (however you choose to define this) can use some sort of feedback. A simple strategy is to echo the typed characters.

By preceeding a pattern with the -echo flag, interact echoes characters that match a pattern. Partial matches are also echoed. This echoing is most useful with actions that cause a local effect, such as modifying the behavior of a script. In this case, the user might not otherwise get any feedback.

The following script behaves similarly to the UNIX script command, which records all input and output in a file called typescript. However, this script also allows the user to turn recording on and off by typing ~r (to record) or ~s (to stop recording). This can be used to avoid recording things such as vi or emacs editor interactions in the middle of a longer shell session. The script starts with recording disabled.[54]

spawn $env(SHELL)
catch {exec rm typescript}
interact {
    "~s" {log_file}
    "~r" {log_file typescript}
}

As is, the script gives no feedback when the user types ~s or ~r, nor when the actions are executed. Adding -echo allows the escapes to echo.

interact {
    -echo "~s" {log_file}
    -echo "~r" {log_file typescript}
}

Once patterns are completely matched, you can add any kind of additional feedback you like. For example:

interact {
    -echo "~s" {
        send_user "
 stopped recording
"
        log_file
    }
    -echo "~r" {
        send_user "
 recording
"
        log_file typescript
    }
}

The at the beginning of each send_user puts the message on another line, but you can take advantage of what is already on the screen to accomplish interesting effects. For example, you can incorporate the echoed keystrokes into new messages by writing the send_user commands as follows:

send_user "topped recording
"  ;# prefaced by "s"
send_user "ecording
"          ;# prefaced by "r"

This kind of thing can be very cute but generally leads to torturous code, so I only recommend it in very simple cases.

Avoiding Echoing

A problem with -echo is that a user may start entering a pattern that echoes but then go on to enter other characters that do not match the remainder of the pattern. The buffered characters plus the new ones will be sent to the spawned process. Assuming that the spawned process echoes them, if a pattern using -echo was causing them to echo already, the user will see characters echoed twice.

For example, in the recording script above, suppose the user enters ~q. The tilde matches and is echoed by Expect. The q does not match, so the tilde and q are sent to the spawned process and are likely echoed by the spawned process. Unfortunately, there is no trivial way to “unecho” a character without getting involved in knowing how to erase characters on the screen as well as what was on the screen in the first place. So the user ends up seeing "~~q“—one tilde too many.

There is no perfect solution for this problem except to avoid it to begin with (perhaps by not using -echo at all). But the next best possibility is to choose patterns that are less likely to match at any point unless the user is definitely entering a pattern.

Giving Feedback Without -echo

In practice I rarely use -echo. If the user really needs feedback, I give it immediately after recognizing that a pattern is being entered. There is no special support for this. Instead, simply use the escape prefix itself as the sole pattern and then pass control to another procedure that carries out the rest of the recognition. Here is an example from a script which uses ~~ to start commands.

interact "~~" cmd

In the cmd procedure, the user can now be directly prompted for commands:

proc cmd {} {
    send_user "command (g, p, ? for more): "
    expect_user {
        "g"        get_cmd
        "p"        put_cmd
        "?"        help_cmd
        "~"        {send "~~"}
        . . . and so on
    }
}

The user now clearly knows what they are dealing with because the prompting is very explicit. There is no way for the user to be confused about echoing because the two-tilde sequence always invokes the cmd procedure, which in turn entirely suspends the interact until the procedure returns.

Note that if the user really intended to enter two tildes and not have them interpreted as a command, another tilde has to be entered. This is a fairly standard and customary inconvenience with interactions that suspend interact commands. This style is identical to that used by the UNIX telnet command when its escape is entered.

Telling The User About New Features

In the examples so far, the escape sequences were layered on top of the spawned process. In each case, the user must be informed of these new commands in order to take advantage of them. Indeed, the more obvious the underlying spawned process is, the more likely the user is to forget that there are extra commands available.

Printing out documentation while the program is running can be very helpful and is easy to do. The previous script showed one such example—printing out a list of choices when an escape prefix is entered. Here are some others.

The following extract is from a script that layers several commands on top of the traditional ftp client to enable it to perform recursive ftp. (Called rftp, this script comes with Expect as an example.) These new commands are entered as ~g (get recursively), ~p (put recursively), and ~l (list recursively).

puts "Once logged in, cd to the directory to be transferred and
                                press:
"
puts "~p to put the current directory from the local to the remote
                                host
"
puts "~g to get the current directory from the remote host to the
                                local host
"
puts "~l to list the current directory from the remote host
"
if {[llength $argv] == 1} {
    spawn ftp
} else {
    spawn ftp [lindex $argv 1]
}

When the script starts up, it prints the messages above, which describe the new commands. Then the script drops into an interact so that ftp can enter its usual dialogue. The tilde escape works well, because ftp does not use tilde for anything anyway.

Below is the complete version of the script to emulate the UNIX script command. A couple of things have been added to make it more like the original, but most important is the message that tells the user about the new commands. This message prints out immediately when the script starts. The first shell prompt then follows on a new line.

log_file typescript
spawn -noecho $env(SHELL)
send_user "recording to file typescript
"
send_user "~s to stop recording, ~r to record
"
interact "~s" {log_file} "~r" {log_file typescript}
send_user "recording to file typescript complete
"

Here is how it looks to the user when run:

28% newscript
recording to file typescript
~s to stop recording, ~r to record
1%

Sending Characters While Pattern Matching

By default, characters are not sent to the spawned process until either a match is made or no match is possible. Characters that may potentially match are buffered. It is occasionally useful to disable this buffering.

The buffering is disabled using the -nobuffer flag. Using this flag, all characters are sent to the spawned process whether or not they match, do not match, or might match in the future.

For example, a site had a modem that was available to all users. The site administrators wanted to monitor the phone numbers being dialed. Using tip or some other interactive program, there was no way of recording the numbers. They used the following fragment in a script that ran on top of tip.

proc lognumber {} {
    interact -nobuffer -re "(.*)
" return
    puts $log "[exec date]: dialed $interact_out(1,string)"
}
interact -nobuffer "
atd" lognumber

The interact command (see last line) passes all characters to the spawned process. If the user presses return followed by atd, the lognumber procedure is invoked. The return forces the command to be entered at the beginning of a line—just one more safeguard against detecting the pattern at the wrong time.

Unlike the example on page 335, lognumber records everything until another return is pressed. The characters between the first atd and the next are the phone number. Because of the -nobuffer on the second interact command, the phone number is sent to the spawned process and echoed normally. The user cannot tell that the number is being recorded.

When the final return is pressed, the pattern matches and the return action is executed. The return action simply forces the interact to return to its caller (more on this in the next section), and the next command, puts, executes. The puts records the date and the phone number in a log file.

The log file will eventually look something like this:

Wed Aug 25 21:16:28 EDT 1993: dialed 2021234567
Wed Aug 25 23:00:43 EDT 1993: dialed 3013594830

The phone numbers in the log file will also contain things like backspaces or whatever erase characters the user might press to delete characters and fix errors. You could, of course, write a procedure to interpret these and strip them out after they are matched but before they are put in the log.

interact -nobuffer -re "(.*)
" return
set number [cleanup $interact_out(1,string)]
puts $log "[exec date]: dialed $number

A much easier solution is just to change to cooked mode temporarily and call expect to read the number. However, this approach also has a drawback in this example—users could conceivably press atd in some other context besides dialing a modem. In that case, you may well get a “number” that turns out not to be a number at all. In this example, the likelihood is pretty low— atd is a very unusual character sequence. In the general case, more heuristics could be added but there is no sure-fire way to know. For example, the user could be writing a manual on how to dial a modem. Obviously, making this look like a number was being dialed could be crucial to the task!

The continue And break Actions

Like the expect command, it is possible to have the interact command cause its caller to break or continue. Each of these can be used as an action. For example, in the following loop, if a user presses "+“, interact returns and the while loop breaks.

while {1} {
    interact "+" break
}

The continue command works similarly. In the following loop, if a user presses "+“, the interact returns and the loop breaks. If the "-" is pressed, the interact returns, and the while loop continues.

while {1} {
    interact "+" break "-" continue
}

The return Action

By default, the return command does not behave in a way analogous to break or continue. The return command does not cause the caller to return. Instead, when used as an action, return causes interact itself to return.

For example, consider the following commands:

interact "X" return
puts "interact done"

If the user presses X, the interact command returns and the puts command executes.

This behavior is very useful when you have a problem that is almost automated but has sections that require attention. The script will do most of the work, but you can grab control when necessary. You can interact and then return control to the script.

For example, fsck, the file system checker, looks for discrepancies in the file system. fsck describes the problems it encounters and interactively asks whether it should fix the problem. fsck waits for a y or n as an answer. The following script automatically answers one type of question yes and another type no. Everything else causes control to be turned over to the user via the interact command.

for {} 1 {} {
    expect {
        eof                    break
        "UNREF FILE*CLEAR\?"  {send "y
"}
        "BAD INODE*FIX\?"     {send "n
"}
        "\? "                 {interact + return}
    }
}

When the user is through interacting, a plus causes interact to return, after which the script resumes control and continues answering questions.

In this example, control is completely turned over to the user. This is especially useful when the situation is very complicated and the user might need to explore things a lot. In more controlled situations—for example, when you do not trust the user or when the user does not understand what is happening behind the scene—the script should stick with expect_user and send_user commands.

It is also possible to have interact cause a return in its caller. This is more like what the expect command does with a return action. This behavior is useful if you call interact from within a procedure. The caller will return if interact executes the inter_return command. For example, the procedure below returns if a plus is pressed. If a minus is pressed, the interact command ends, but the procedure continues executing the next command after interact.

proc x {} {
    interact {
        "+" {inter_return}
        "-" {return}
    }
    send ". . ."
    expect ". . ."

In Chapter 11 (p. 250), I presented a script that used expect commands to handle answerback at login. Answerback requires communications to flow in both directions, and this should make you think of interact. The earlier script required nine lines to solve the problem. Using interact, it is possible to handle answerback using only one line:

interact -o -nobuffer $prompt return

This command permits the spawned process to interact with the terminal. When the prompt appears, return is executed which causes interact itself to return. The -nobuffer flag allows the prompt to appear in the output even though it has been matched.

This same idea can be used to convert the rlogin script in Chapter 5 (p. 122) into a telnet script. Remember that the script provided an interactive rlogin session with the same directory as the one on the local host. The script assumed that rlogin does not prompt for either account or password. In comparison, telnet is guaranteed to prompt. The solution is to substitute the code above in place of the wait for the prompt done by an expect command.

Here is the resulting script using telnet:

#!/usr/local/bin/expect --
# telnet-cwd - telnet but with same directory

eval spawn telnet $argv
if [info exists env(EXPECT_PROMPT)] {
    set prompt $env(EXPECT_PROMPT)
} else {
    set prompt "(%|#|\$) $"          ;# default prompt
}
interact -o -nobuffer -re $prompt return
send "cd [pwd]
"
interact

The Default Action

The interpreter command lets you interactively give commands to Expect and is described further in Chapter 9 (p. 225). The interpreter command is very useful when debugging scripts or avoiding having to hardcode commands beyond some point where the interactions are stable enough. Just call interpreter with no arguments.

interpreter

The last action in an interact may be omitted in which case it defaults to interpreter. The following two commands are equivalent:

interact X
interact X interpreter

The first form is simply a shorthand. There is no other inherent value to omitting the action. Note that this shorthand can only be used with the final pattern—with any other pattern, the subsequent pattern would be misinterpreted as the action to the previous pattern. Consider the following two commands. If X is pressed, the first command invokes interpreter while the second mistakenly invokes pattern.

interact X interpreter pattern action
interact X pattern action               ;# OOPS!

It is common to write scripts that end by leaving the user in an interact command, interacting with a process in raw mode. The interpreter command provides a way to get back out to Expect before the script terminates. The environment can then be explored or manipulated while the script is still running.

While I just said that omitting the action is only a shorthand, it is intended to be a compellingly convenient shorthand. Indeed, it should be so compelling that you should never use interact without some sort of escape unless you have intentionally considered all the implications. When prototyping, get used to writing "interact X" (or whatever your favorite escape character is) rather than just interact. It will save you in lots of situations such as when the spawned process hangs.

Detecting End-Of-File

Normally, a spawned process generates an eof when it exits. This can be detected by using the special pattern eof. When an eof occurs, the following action is executed. The syntax is identical to expect. For example, the following command prints out a message when an eof occurs. The return causes interact to return to its caller.

interact eof {
    puts "detected end-of-file"
    return
}

If an eof occurs but no eof pattern is given, the return action is executed by default. So you could write the fragment above more simply as just:

interact
puts "detected end-of-file"

You usually want to end an action for the eof pattern with return. It rarely makes sense to continue the interaction if the spawned process has exited. However, the next chapter has examples where it is meaningful to continue the interaction.

When interact detects an eof, it automatically closes the connection to the spawned process so that there is no need to call close explicitly. This is identical to the behavior of expect when it detects an eof. One tiny difference between expect and interact is that interact can never detect an eof from the user. Because interact puts the terminal into raw mode, the user has no way of generating an end-of-file. In expect, a ^D closes the input, but in interact a ^D is just sent on to the spawned process.

Matching A Null Character

The interact command can match a null character using the null pattern. This pattern works the same way as in the expect command. See Chapter 6 (p. 155) for more information.

Timing Out

The special timeout pattern matches after a given amount of time. Unlike the expect command, the timeout pattern does not depend on the timeout variable. Instead, the timeout is given after the keyword itself. The timeout is specified in seconds. The action follows the number of seconds. For example, the action in the interact command below is executed if the user presses no keys for more than an hour.

interact timeout 3600 {
    send_user "Idle for 1 hour - you are being logged out"
    return
}

In this example, the output of the spawned process is not timed. This is useful in preventing irrelevant system messages (”System going down at midnight.“) from making the session seem active.

The interact command uses explicitly specified timeouts so that you can use different timers simultaneously. For instance, the following fragment times out the user after 10 seconds and the spawned process after 600 seconds.

interact {
    timeout 10 {
        send_user "Keep typing -- we pay you by the character!"
    }
    -o
    timeout 600 {
        send_user "It's been ten minutes with no response.
                   I recommend you go to lunch!"
    }
}

On many systems, the following script can be used to fake out shells that automatically exit after periods of inactivity. The script works by detecting the inactivity and sending a space and immediately removing it.

spawn $env(SHELL)
interact timeout 3000 {send " 177"}

As shown here, the script waits for a little less than an hour under the assumption that the spawned program’s timeout is exactly an hour. You can modify or parameterize the timeout. Some programs will need different keystroke fakes. For example, editors such as emacs and vi are more appropriately faked by sending ^G and escape respectively. In contrast, ftp would need a simple but real command such as "binary " to force the client to actually send something to the server since it is the ftp server that times out, not the client.

The following code achieves an effects similar to the fragment in Chapter 6 (p. 146). Both watch for output containing a pattern where each character is not separated from the next by more than $timeout seconds.

set status bad
interact {
    -o
    timeout $timeout inter_return
    $pattern {set status ok}
}

This code works differently but ultimately does much the same thing. The -o flag makes the subsequent patterns apply to the output of the spawned process. If too much time occurs between characters, the inter_return action causes the interact to complete. If $pattern appears in the output, a status variable is set and the interact command continues waiting for more characters to arrive (or the timeout to occur). The only significant difference in the code is that the buffering is done internally by interact rather than explicitly in the user-supplied actions.

A timeout value of −1 is equivalent to infinity. The associated action can never be executed. This is useful when the need for a timeout is not known in advance. Rather than rewriting the interact command dynamically, the timeout can be suppressed just by an appropriate variable assignment.

Compare:

interact timeout $timeout $action

with:

if {$need_timeout > 0} {
    interact timeout 100 $action
} else {
    interact
}

More On Terminal Modes (Or The -reset Flag)

The interact command puts the terminal into raw mode so that all characters can pass uninterpreted to the spawned process. When a pattern matches, actions are executed in raw mode as well. Usually this works well. Most actions do not depend on the terminal mode. For example, the following commands are all terminal-mode independent:

set a [expr 8*$a]
send "k
"
send_user "hello
"

You may be surprised that the last command works correctly since it includes a newline. Normally, newlines become linefeeds in raw mode. However, the send_user command automatically translates newlines to carriage-return linefeed sequences when the terminal is in raw mode. (See Chapter 8 (p. 197) if you want to disable this.) Thus, you can write such commands and not worry about the mode.

Some commands are mode dependent. Here are three example commands, each of which is mode dependent.

system cat file
exec kill -STOP 0
expect -re "(.*)
"

The first command (”system cat ...“) executes a program that writes directly to the standard input and output. The program assumes the terminal is in cooked mode and will misbehave if it is not. A common symptom of this misbehavior is displayed in the following output:

this is line one
                this is line two
                                this is line three
                                                  and so on

The next command (exec kill ...) suspends the Expect process, placing the user back in the original shell. Shells that do not force cooked mode will behave incorrectly, leaving processes to run in raw mode when they expect cooked mode.

The last command (expect ...) is intended to read a single line in cooked mode. (This is similar to "gets stdin“.) Not all expect commands require cooked mode. In fact, the example on page 335 was specifically intended to work in raw mode. However, when accepting long strings via expect_user, it is helpful to be in cooked mode to allow the user to edit the strings.

The expect command above checks for the newline character explicitly. This could be changed to look for either the linefeed or return character, but the cooked mode editing would still be lacking. The only way to obtain that is to go to cooked mode.

The simplest solution to fix all of these is to use the -reset flag when defining the action. For example, to define that ^Z (ASCII 32 in octal) suspends the Expect process, you can write the following:

interact -reset "32" {exec kill -STOP 0}

The other actions could be written similarly. When the actions return, the mode is reset in the other direction—back to raw.

The -reset flag does not specifically force the terminal into cooked mode. Rather, it forces the terminal into whatever mode was set just before the interact command was executed. This allows you to set up your own (however bizarre) definition of a “normal” mode and flip between it and raw mode inside of interact and actions.

Unfortunately, this ability to set the “normal” mode means that -reset may not have the desired effect inside of an interact command that was in turn invoked from another interact. If you want cooked mode in such cases, you have to explicitly set it using stty in the action. Fortunately, these kind of situation is extremely uncommon.

If you do explicitly set the terminal mode inside an action without using -reset, reset the mode before returning to the interact. If you do not reset it, interact will continue running with your new terminal modes.

In Chapter 8 (p. 206), I mentioned that characters can be lost when terminal parameters are modified. This problem carries over to using -reset as well, since -reset potentially modifies terminal parameters. Fortunately, users do not type very quickly so this is rarely a problem; conceivably, however, this problem could arise if interact were used as a noninteractive filter. If you need absolute assurance that characters will not be lost, do not use the -reset flag.

Example—Preventing Bad Commands

The following fragment demonstrates how -reset could be used to prevent a user from entering certain commands. For example, suppose you want to provide an interface to another program that allows two commands that you would rather people not enter. You want to allow any others, just not these two.

Consider the following reasonable-looking but incorrect approach:

set badcmds "badcmd1|badcmd2"         ;# WRONG
interact -re "$badcmds.*
" {         ;# WRONG
    put "command not allowed
"       ;# WRONG
}                                     ;# WRONG

But as with the earlier modem monitor script, users can enter command-line editing characters to force the pattern to fail. For example, "ba<backspace>adcmd1" does not match the interact pattern and will be accepted by the spawned process.

One solution is to recognize the program prompt and shift into cooked mode while the user enters a command. Any command not in badcmds will be allowed. This algorithm could also be reversed to allow only a set of good commands.

set badcmds "badcmd1|badcmd2"
interact {
    -o -reset -nobuffer -re $prompt {
        expect_user {
            -re "^$badcmds.*
" {
                puts "Command not allowed
"
            }
            -re "(.*)
" {
                send "$expect_out(1,string)
"
            }
        }
    }
}

This script can also be written as an expect loop with an interact inside of it—similar to the one above but “inside out”. How you structure the script is not that important. What is important is that you use cooked mode to read the results of potentially edited input. In general, if the spawned process is using cooked mode, then your script should too. Conversely, if the spawned process is using raw mode, your script should also.

There are some programs that provide much more sophisticated interfaces than what can be supported in cooked mode. In general, trying to follow what they are doing can require a lot of work. This is unfortunate but understandable—they are internally complicated and any script that attempts to track them will be also.

Exercises

  1. Modify the Dvorak keyboard script (page 325) to so that it uses a single send command. Use an array to provide the mapping between input and output. Can you tell any difference in speed between the two implementations?

  2. On page 330, I showed a script that responded to an inactivity message. However, some systems do not print a warning message. Rewrite the script so that it does not require a warning message. Experiment with resetting the timer by sending a null or a ^Q or a space-delete sequence.

  3. Modify the script script (on page 334) so that it strips out carriage returns. Does "stty -ocrnl" help? How about "tr -d '15'“?

  4. Write a script to boot and simultaneously allow interaction with an auxiliary processor such as a vxWorks board. Make the script respond appropriately if the board hangs for more than one minute or if the board is power-cycled behind Expect’s back.

  5. Write a script that allows you to repeat a command in the shell (or any program) by pressing a single function-key. Write a second version that suspends this behavior when the shell is not prompting.

  6. Write a script that provides “hotkey” service in the style of a DOS TSR, which will temporarily bring a background process to the foreground. Allow triggering either by keypresses or specific times.



[54] The catch around the exec catches systems on which rm complains if no file is present. POSIX standardizes the -f flag to prevent such a diagnostic, but some systems complain anyway. Use catch to be maximally portable.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.4.181