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