expect: Automating Anything That Demands a Human Sit at the Keyboard

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.

Key Takeaway: When a program insists on a human at the terminal, 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.

All newsletters