2026-05-13
Don Libes wrote expect in 1990 to solve a problem that hasn't gone away: programs that demand a TTY and refuse to read instructions from a pipe. passwd, ssh, telnet, ftp, vendor CLIs from network gear, half the firmware update tools shipped this decade — they all check isatty(stdin) and slam the door on shell scripts. expect opens a pseudo-terminal, runs the program inside it, and lets you script the conversation.
The core vocabulary is four words: spawn starts a process, expect waits for a pattern, send types a reply, interact hands control back to you.
#!/usr/bin/expect -f
set timeout 10
spawn ssh [email protected]
expect {
"yes/no" { send "yes\r"; exp_continue }
"assword:" { send "$env(SWITCH_PW)\r" }
timeout { puts "stuck"; exit 1 }
}
expect "switch#"
send "show running-config\r"
expect "switch#"
send "exit\r"
That expect { ... } block is the killer feature: alternative patterns matched in parallel, each with its own action, and exp_continue loops back without falling out of the block. Try expressing that in sshpass or a heredoc — you can't.
The lazy man's script generator. Ship with the package: autoexpect. Run it, do the interactive thing once by hand, and out pops a working script.exp you can edit.
$ autoexpect -f login.exp ftp legacy.example.com
# ... type your session ...
$ chmod +x login.exp && ./login.exp
Half-automated, half-manual sessions. The interact command is what makes expect different from a one-shot replay tool. Drive the boring login dance, then drop the user at a live prompt:
spawn ssh jumpbox
expect "password:"; send "$env(PW)\r"
expect "$ "
send "sudo -i\r"
expect "password:"; send "$env(PW)\r"
interact ;# you're now driving, live
Pacing slow devices. Cisco/Juniper consoles drop characters if you paste a config too fast. expect has send -s with set send_slow {1 .05} to throttle one character every 50ms — the kind of dirty real-world detail you only learn after bricking something at 2am.
Testing CLIs. A frequently overlooked use: black-box testing your own tools. Spawn the binary, expect a prompt, send a command, assert the response, and exit with a status code. Better than golden-file diffing because you can branch on what the program actually said.
spawn ./mytool --repl
expect "> "
send "compute 2 + 2\r"
expect {
-re "= 4\\b" { puts "ok"; exit 0 }
timeout { puts "hung"; exit 2 }
eof { puts "crashed"; exit 3 }
}
Why not just use ssh keys / API tokens / proper batch mode? You should, when you can. expect earns its keep on the systems where you can't: a 2008-era SAN controller, a UPS web console with a serial fallback, an installer that asks "are you sure? [y/N]" and won't take --yes, an embedded device's U-Boot prompt over picocom. The grizzled wizard's rule: when the vendor refuses to fix their tool, wrap it in a PTY and move on with your life.
It's in every distro's repos (apt install expect, brew install expect), the manual is unusually good, and the language is Tcl — which is itself a tiny, almost forgotten gem you'll have learned by accident after a week.
expect gives you a scriptable human — pattern-match the prompt, type the reply, branch on what happened, and stop fighting tools that refuse to read stdin.
