Edwin Kofler's profile picture

Edwin Kofler

Terminal Automation with Expect

Edwin Kofler

2022-0-6

Expect is THE tool for automating tasks in the terminal. It is extremely helpful if you need to automate sending input to a program directly on the command line.

This guide is targeted towards developers that wish to automate sending and receiving input from a program, without having to read long manual pages or fragmented StackOverflow answers. I’m going to skimp on technical details relating to Expect and the concepts around it; for that, there are plenty of resources online

Here is a result of what using Expect might look like. Note that I am not touching the keyboard after invoking the program ./automate.tcl!

asciicast

Installation

Of course, program installation is a prerequisite for program usage

If you’re using Linux, the installation is more-or-less distro-dependent. If you don’t wish to install with a GUI, I have provided the commands below

# For Debian, Ubuntu, PopOS, Linux Mint, etc.
sudo apt install expect
# For Arch Linux, Manjaro, Endeavour, Artix, etc.
sudo pacman -S expect
# For Fedora, CentOS Streams, RHEL, Rocky, Alma, etc.
sudo dnf install expect
# For OpenSUSE, etc.
sudo zypper install expect

If you’re using MacOS, the de facto package manager is brew

brew install expect

For Windows users, you’re out of luck, as per usual. There are some ports for Windows, but I cannot vouch for their reliability or popularity. I’ll also add that the way “terminals” (consoles) work on Windows is fundamentally different compared to Unix systems.

Hello World (tcl)

Expect programs are written in a language called tcl (pronounced tickle). Before I show anything related to Expect, I would like to show you a tcl Hello World

First, create a hello.tcl file…

#!/usr/bin/tclsh
puts "Hello, World!"

Then, run it

$ tclsh ./hello.tcl
Hello, World!

Because the shebang is at the top, we can also execute it like so:

$ chmod +x ./hello.tcl # remember to mark the file as executable
$ ./hello.tcl
Hello, World!

Tcl is a simple, yet powerful scripting language. Think of it like Python, but faster and a bit more antique. If you want to learn more about Tcl, I would recommend reading the Tcl Wiki

Hello World (expect)

Expect is essentially a library for Tcl that adds extra functions which enables us to supply input and check the output of a program. Using Expect is very similar. Below, we use an Expect function called sleep, which sort of pauses the program for the amount of seconds specified:

#!/usr/bin/expect -f
puts "Hello, World!"
sleep 1
puts "Hello again! ^w^"

To run, invoke expect with the -f flag

$ expect -f ./script.tcl
Hello, World!
Hello again! ^w^

Just like last time, you can also execute the script directly

$ chmod +x ./script.tcl # remember to mark the file as executable
$ ./script.tcl
Hello, World!
Hello again! ^w^

Actual Automation

Now that we have a basic understanding of Expect, it is time to actually use it for its primary purpose: automation of interactive terminal programs.

First, we’ll start off with a program that we want to automate: a simple name printer. I’ve supplied versions in Python and C++, but it doesn’t matter at all which language you use

#!/usr/bin/env python3
print("What is your name?")
name = input()

print(f"Hewwo {name}! Nice to meet you~ ^w^")
#include <iostream>

int main() {
  std::cout << "What is your name?" << std::endl;
  std::string name;
  std::getline(std::cin, name);

  std::cout << "Hewwo " + name + "! Nice to meet you~ ^w^" << std::endl;
}

Now, run the program, paying actual attention to what is outputed to the screen, and when the user needs to enter in information

$ python3 test.py
What is your name?
Edwin
Hewwo Edwin! Nice to meet you~ ^w^

So, the user executes the program, and expects the program to prompt for a name. Once the program prints the prompt, the user sends their name. This can easily be codified as such:

#!/usr/bin/expect -f
spawn "python3" "test.py"

expect "What is your name?"
send "Edwin\r"

expect eof

If you are curious, the expect eof essentially makes Expect continue running until the ./test.py program finishes executing. Otherwise, Expect would have quit prematurely. And, the \r denotes a return character, like pressing Return on your keyboard (\n works as well)

Cool! Let’s run it! As you can see, I’ve called the file automate.tcl

$ expect -f ./automate.tcl
spawn python3 test.py
What is your name?
Edwin
Hewwo Edwin! Nice to meet you~ ^w^

Woah~! We didn’t even have to do anything and the user input was supplied automatically!

Handing control back to the user

90% of the time, you’re either going to be using the expect or send functions. There are times where you would want to do other things, though, such as giving control back to the user, so they can input data themselves. Let’s try that.

First, we’ll modify the program to ask another question:

#!/usr/bin/env python3
print("What is your name?")
name = input()
print(f"Hewwo {name}! Nice to meet you~ ^w^")

print("What is your favorite animal?")
animal = input()
print(f"Woah~, the {animal} is a pretty cool animal!")

Let’s run the same Expect script, just to see what happens

$ expect -f automate.tcl
spawn python3 test.py
What is your name?
Edwin
Hewwo Edwin! Nice to meet you~ ^w^
What is your favorite animal?

It hangs! This is because at this point, Expect has reached the expect eof line. Here, Expect is just sort of waiting until the program exits (try to type a response and press enter, nothing will happen). Instead of waiting, we want Expect to enable the user to interact with the program

To do this, just replace the expect eof with interact

#!/usr/bin/expect -f
spawn "python3" "test.py"

expect "What is your name?"
send "Edwin\r"

interact

The output looks the same, but if you type and press enter, the Python program actually receives the input and prints out a response! Here is my output:

$ expect -f automate.tcl
spawn python3 test.py
What is your name?
Edwin
Hewwo Edwin! Nice to meet you~ ^w^
What is your favorite animal?
Fox
Woah~, the Fox is a pretty cool animal!

This is extremely useful if you want to automate the beginning of a program, but also want to manually input text near the end.

Miscellaneous tips

There are a few things that you may want to be aware of

  1. Using asterisks!

If you are expecting the string What is your name?, you don’t have to write it out in full like so:

expect "What is your name?"

Instead, use a glob pattern. The asterisks means “zero or more characters”

expect "*your name?*"
  1. Use -- if want to input something that starts with a hyphen

Sort of like invoking command line arguments programs ls, mkdir, adding -- will prevent any ambiguity. In other words, send will interpret -cool as a string to send, rather than an option that modifies its behavior

# ❌ INCORRECT
send "-cool"

# ✅ CORRECT
send -- "-cool"
  1. Pass -h to send if you want to make it seem like a human typed it (slower typing, with variant intermidant delays)
set send_human {.1 .3 1 .05 2}

send -h "It looks like a human is typing this"

Note that you only have to set the send_human variable once, best placed at the top of the file. For more information about the 5 numbers, here is a snippet from the man page

The first two elements are average interarrival time of characters in seconds. The first is used by default. The second is used at word endings, to simulate the subtle pauses that occasionally occur at such transitions. The third parameter is a measure of variability where .1 is quite variable, 1 is reasonably variable, and 10 is quite invariable. The extremes are 0 to infinity. The last two parameters are, respectively, a minimum and maximum interarrival time. The minimum and maximum are used last and “clip” the final time. The ultimate average can be quite different from the given average if the minimum and maximum clip enough values

  1. Type with a slight pause. Sometimes, it may be useful to type something, and wait for the underling program to catch up (ex. Slow languages like Java). You can use a Tcl procedure for this
#!/usr/bin/expect -f
proc send_with_delay {str} {
  send $str
  sleep 0.3
}

# Use it like so...
send_with_delay "Fox\r"

Wrap

Expect has many more features, but those are the ones you’ll be using the most often! If this helped you or if you have any feedback, let me know! Thank youu~ and have a fantastic rest-of-the-day!