2023-09-20
expect is a powerful tool for scripting command line interfaces.
Install as follows:
$ sudo apt install expect
It provides a repl that you can invoke with expect:
$ expect
expect1.1>
From there you can spawn a process you’d like to script:
expect1.1> spawn bash
spawn bash
22099
Bash gives us a prompt that ends with “$ “ when it’s ready for commands. So let’s expect that bash has written this output:
expect1.2> expect "$ "
razzi@razzi-lightspeed:~$ expect1.3>
We see the bash prompt, then since expect is satisfied,
it prompts us with expect1.3>.
Write to bash using send.
Put a \n at the end to simulate hitting enter.
expect1.3> send "whoami\n"
expect1.4>
Expecting the next bash prompt shows the input and output:
expect1.4> expect "$ "
whoami
razzi
The output is also stored in a variable $expect_out(buffer),
which you can print with puts:
expect1.5> puts $expect_out(buffer)
whoami
razzi
Wrap up your expect script by sending an exit to bash:
expect1.5> send "exit\n"
And expecting an end-of-file (eof):
expect1.6> expect eof
Finally you can exit expect itself with exit (or control+d):
expect1.7> exit
expect uses the Tcl language,
which is kind of like Python but older.
If you try to use the arrow keys to re-run a previous command, it won’t work and will instead print control codes.
$ expect
expect1.1> ^P^N^[[A^[[B
You can get command history to work by wrapping expect with rlwrap:
$ rlwrap expect
The bash example above is just for demonstration; if you want to script bash, you can write a script and run it with bash script.sh, rather than spawning a bash process and interacting with it.
A more practical example is scripting an ssh session. I’ll script connecting to segfault.net, a disposable root server host provided by The Hacker’s Choice.
Here’s how it looks when you interact with it manually:
$ ssh root@segfault.net
root@segfault.net's password:
As you can see, it prompts for a password, which is segfault. Rather than typing this interactively, let’s script it with expect!
Add the following code to a file segfault.exp:
spawn ssh root@segfault.net
expect "root@segfault.net's password: "
send "segfault\n"
interact
And run it with expect segfault.exp. Like magic!
Of course, it’s not quite magic, especially when it doesn’t work.
One easy way to debug your expect scripts is to add -d when you invoke it:
$ expect -d debugme.exp
You can also add exp_internal 1 to the top of your script:
#!/usr/bin/expect
exp_internal 1
spawn bash
expect "$ "
send exit
When you run it you’ll see output like:
$ expect debugme.exp
spawn bash
parent: waiting for sync byte
parent: telling child to go ahead
parent: now unsynchronized from child
spawn: returns {20802}
expect: does "" (spawn_id exp4) match glob pattern "$ "? no
razzi@razzi-lightspeed:~/hack$
expect: does "\u001b[?2004hrazzi@razzi-lightspeed:~/hack$ " (spawn_id exp4) match glob pattern "$ "? yes
expect: set expect_out(0,string) "$ "
expect: set expect_out(spawn_id) "exp4"
expect: set expect_out(buffer) "\u001b[?2004hrazzi@razzi-lightspeed:~/hack$ "
send: sending "exit" to { exp4 }
which I find especially useful for debugging expect matches and expect_out contents.