Expect is, in its own words, "a tool for automating interactive applications such as telnet, ftp, passwd, fsck, rlogin, tip, etc." Don Libes wrote it at the National Institute of Standards and Technology, and the manual page for version 5 is still the reference; the last release of the original repository is 5.45.4, from February 2018, and the page now points to a maintained fork under the tcltk-depot organisation that supports 8.6 and Tcl 9. Everything below quotes that manual page, read on 2026-10-05, because the mistakes are all answered there.
is an extension of Tcl. That matters more than it sounds: the quoting rules are Tcl's, the variables are Tcl's, and half the surprises in an Expect script are Tcl surprises wearing an Expect hat.
The four verbs
spawn program argsstarts a program and makes it the current process. From then on its output is whatexpectreads and its input is whatsendwrites. By defaultspawnechoes the command line;spawn -noechokeeps it quiet.expect pattern body ..."waits until one of the patterns matches the output of a spawned process, a specified time period has passed, or an end-of-file is seen." Patterns are glob patterns by default, the same language as Tcl'sstring match(*,?,[chars],\x);-remakes one a regular expression,-exan exact string,-nocasefolds case. Five keywords stand for events rather than text:eof,timeout,full_buffer,nullanddefault(timeout or end-of-file).send stringtypes the string at the program. Variants write elsewhere:send_userto stdout,send_errorto stderr,send_ttyto the terminal.-ssends slowly and-h"(somewhat) like a human actually typing".interact"gives control of the current process to the user, so that keystrokes are sent to the current process, and the stdout and stderr of the current process are returned."
A login script is these four in a row: spawn ssh, expect the password prompt, send the password, expect the shell prompt, send a command, expect the prompt again, read what came back from expect_out(buffer), send exit, expect eof. The Expect script explainer lays that dialogue out as a table, each expect beside the send that answers it, which is the quickest way to see where a script's picture of the conversation and the program's actual conversation part ways.
Sentence one: ten seconds, or forever
"The default timeout period is 10 seconds but may be set, for example to 30, by the command "set timeout 30". An infinite timeout may be designated by the value -1."
Two mistakes live in that sentence. The first is not setting a timeout at all and letting a slow login or a long show tech run into ten seconds. The second is the reflex fix, set timeout -1, which turns every wait into a wait with no end: a box that never prints the prompt holds the script, and whatever scheduled it, forever. Set a number that fits the slowest step, and handle the timeout (next sentence). A third, quieter mistake is placing set timeout after the first expect; that one already ran on the default. And inside a procedure, "variables written are always in the local scope (unless a "global" command has been issued)", so a set timeout in a proc without global timeout changes nothing outside it.
Sentence two: the implicit null action
"If no timeout keyword is used, an implicit null action is executed upon timeout."
This is the sentence behind most "it worked yesterday" reports. An expect "assword:" with no timeout pattern does not fail when the prompt never comes; it returns, silently, after the timeout, and the next line runs. The script then sends the password to a program that is showing a host-key question, sends show version to a program still asking for the password, and reads an expect_out(buffer) that holds the wrong text. Every expect that waits for something a program might not say wants a timeout arm that reports and exits:
expect {
-nocase "password:" { send -- "$env(SSHPASS)\r" }
timeout { send_user "no password prompt from $host\n"; exit 1 }
eof { send_user "connection closed by $host\n"; exit 1 }
}
Sentence three: the pattern that does not wait
"The pattern "" (and -re ".") will flush the output buffer without reading any more output from the process."
expect "*" looks like "wait for anything". It is the opposite: it matches whatever is already in the buffer, immediately, even nothing, and moves on. The same manual explains why short prompts misfire: patterns are "unanchored", and $ to mark the end of a string is encouraged "if you can exactly describe the characters at the end of a string". A pattern of ">" matches the first > anywhere in the output, which on a switch with a banner is usually not the prompt. -re {[>#] ?$} names the prompt characters and anchors them to the end of what has arrived.
Sentence four: the return character
"Characters are sent immediately although programs with line-buffered input will not read the characters until a return character is sent. A return character is denoted "\r"."
send "show version" types the words and stops. The program, reading a line at a time, waits for the return that never comes, and the script's next expect waits for a prompt that never comes either, until the timeout and the implicit null action pick up the story. Every send to a program that reads lines ends in \r. Tcl adds a twist of its own: between braces "No substitutions are performed on the characters between the braces except for backslash-newline substitutions", so send {show version\r} sends a backslash and the letter r, and send {$env(PW)\r} sends the eight characters of the variable's name. Double quotes are the right delimiter for a send with a \r or a $ in it.
Sentence five: the connection closes when you leave
"Upon exiting, all connections to spawned processes are closed. Closure will be detected as an EOF by spawned processes."
A script whose last line is send "show running-config\r" has asked a question and left the room. Expect closes the connection as the script ends, the program sees end-of-file, and the answer the script existed to collect is never read. The ending of a well-formed script is an expect for the prompt after the last command (to read its output), then send "exit\r" and expect eof to let the program finish; or interact, if the point was to log in and hand over.
The beginning has a sentence too: "It is a good idea to precede the first send to a process by an expect. expect will wait for the process to start, while send cannot." A send on the line after spawn, before any expect, can arrive before the program is listening, and "you run the risk of having your data ignored".
Tcl's rules, at the pattern boundary
An expect with several patterns is usually written as one braced block, one pattern and body per line. The manual allows it and adds the detail people miss: "all the arguments may be "braced" into one so as to avoid terminating each line with a backslash. In this one case, the usual Tcl substitutions will occur despite the braces." So inside that block, a quoted pattern is still subject to Tcl's substitutions. -re "[Pp]assword: $" asks Tcl to run a command named Pp before Expect ever sees the pattern, and the script dies with "invalid command name". The fix is Tcl's: brace the pattern, -re {[Pp]assword: $}, and nothing inside is touched. The same applies to a $ that is meant as a regular-expression anchor rather than a variable: braces keep it literal.
Secrets, echo, and the end of the process
Three habits round out the manual's advice. First, a password belongs outside the script: in an argument (lindex $argv 2), in the environment ($env(SSHPASS)), or typed by the user with the idiom the manual itself gives, stty -echo, send_user "Password: ", expect_user -re "(.*)\n", set password $expect_out(1,string), stty echo. Note the last step: echo is turned back on as soon as the password has been read. Second, log_user 0 hides the dialogue from stdout; it is the right tool for a quiet script, as long as log_user 1 or a puts of the captured output gives the operator something to read when things go wrong. Third, "close does not call wait since there is no guarantee that closing a process connection will cause it to exit"; a script that closes the connection explicitly should wait afterwards so the process is reaped.
One more, from ssh
Not in the Expect manual but in nearly every Expect script: OpenSSH's StrictHostKeyChecking defaults to ask, and with that setting "new host keys will be added to the user known host files only after the user has confirmed that is what they really want to do" (ssh_config(5)). The first run against a new host therefore shows a (yes/no) question where the script expects a password prompt, and the implicit null action does the rest. Either answer the question in an expect arm (-re "\\(yes/no.*\\)\\?" { send "yes\r"; exp_continue }) or pass -o StrictHostKeyChecking=accept-new, which "will automatically add new host keys to the user's known_hosts file, but will not permit connections to hosts with changed host keys".
The explainer
The Expect script explainer reads a script with the site's Tcl 8.4 parser and never runs it. It explains each command in plain words, walks into the bodies of expect, if, loops, switch, catch and proc, rebuilds the dialogue, and reports eighteen findings that map onto the sentences above: the default or infinite timeout, the missing timeout arm, the flushing pattern and the one-character prompt, the send without \r and the braced send, the script that ends on a send or starts with one, the quoted pattern Tcl substitutes, the host-key question, the secret in clear text (masked in the output), log_user 0 and stty -echo left in place, close without wait, exp_continue outside an expect, and set timeout placed late or inside a procedure. Each finding names its line and quotes the manual, so the fix comes with its reason.
Sources
- expect(1), the Expect manual page (Don Libes, NIST; Expect version 5) (read 2026-10-05)
- Expect at core.tcl-lang.org: description, version 5.45.4, maintenance notice (read 2026-10-05)
- Tcl 8.4 manual: Tcl, the rules of the language (read 2026-10-05)
- Tcl 8.4 manual: string match (read 2026-10-05)
- OpenSSH ssh_config(5): StrictHostKeyChecking (read 2026-10-05)