razzi.abuissa.net

Razzi's guide to expect

2023-09-20

expect is a powerful tool for scripting command line interfaces.

It provides a repl that you can invoke with expect:

$ expect
expect1.1>

basic usage

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

expect uses the Tcl language, which is kind of like Python but older.

accessing expect history interactively

If you try to use the arrow keys to re-run a previous command, it won’t do that and will instead print control codes.

$ expect
expect1.1> ^P^N^[[A^[[B

But you can get this by wrapping expect with rlwrap:

$ rlwrap expect

Using expect with ssh

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.

A more practical example is scripting an ssh session.

The Hacker’s Choice offers disposable root servers at segfault.net.

$ 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!

debugging expect

Of course, it’s not quite magic, expecially 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.