I often need to scp local content to a server and run the same commands on the server, and I learned that exepect scripts can automate this, so I'm writing it down. Let computers handle simple, repetitive work as much as possible: automate processes and tasks to get more done.

When you need to interact with the console, you'll find that TCL-Expect is an incredibly useful tool that can rescue you from repetitive, tedious tasks. Work you once had to do by hand can now be done easily with an expect script. These scripts are a lot like shell scripts, but they use the .tcl extension and are invoked with #!.

Setting Up the Script Environment

The first step is similar to writing a bash script: tell the script which environment to run commands in. For expect, we usually call it like this:

1
2
3
4
5
6
7
8
9
#!/usr/bin/expect
It's best to add `interact` at the end of the script so that when the script finishes, you stay in the terminal instead of exiting, like this:
#!/usr/bin/expect
'''code goes here'''
interact

Puts & Output

shell scripts usually print with echo, while expect uses puts, which is a lot like Ruby.

puts "I am performing a output command"

This prints I am performing a output command, just like echo.

By default, the script prints the commands you're running. That's useful for testing, but not always needed in production or day-to-day use. To show only the puts text and hide other commands, use:

log_user 0

Or you can force it to show commands and their responses with:

log_user 1

Variables

Defining a variable is simple. Just use set {name}{value}, for example:

1
2
3
set a "apple"
set b "banana"
set c "cantalope"

Reference a variable with $ plus its name; $a means apple.

Arguments

To set variables by passing arguments, do this:

1
2
set user [lindex $argv 0]
set password [lindex $argv 1]

Then pass the arguments when you run the script
./mycript.tcl youruser yourpassword

Spawn & Send

You can use spawn to start a new process, such as ssh or scp

1
2
3
set user "myuser"
set server "myserver.com"
spawn ssh "$user\@server"

This example starts an ssh process with the username and server. It's essentially the same as typing ssh myuser@myserver.com in the console.

The send command lets you send input to the console, such as a password:

1
2
3
4
5
6
7
8
set password [lindex $argv 0]
set app [lindex $argv 1]
spawn sudo "apt-get install $app"
expect "assword"
send "$password\r"

This example lets you pass in your sudo password and the name of the app to install with apt, then waits for the password prompt and sends it. No, "assword" is not a misspelling. Let's look at the expect statement.

Expect

expect is where the magic happens. Let's start with the basics: suppose you run a command, wait for the console prompt, and then keep going...

1
2
3
4
5
spawn apt-get "update"
expect "$"
# Move on to the next thing...

You spawn an apt-get process, then wait for $, the prompt.
The expect statement matches the nearest occurrence, so the prompt in this example could be something like root@server $~. When expect finds $, it knows it can continue.

Now let's look at something a bit more complex, with more than one expect. Let's revisit ssh

1
2
3
4
5
6
7
8
9
10
11
12
spawn ssh "$user\@server"
expect {
"assword" {
send "$password\r"
}
"yes/no" {
send "yes\r"
}
}
# NOW we can move on...

In this example, there are two possible outcomes. In the first, you're prompted for a password before connecting to the server. The loose match assword covers both cases and submits the $password variable.
If you're connecting to a remote server for the first time, you'll be asked yes/no. The script submits yes for you, then waits for the password prompt and handles it.

This also works for different prompts; you can handle different prompt styles, like this.

1
2
3
4
expect {
"> " { }
"$ " { }
}

This can be built-upon more to really close the error-gap: (I wasn't sure how to translate this gracefully, so I've kept the original sentence)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
expect {
"> " { }
"$ " { }
"assword: " {
send "$password\n"
expect {
"> " { }
"$ " { }
}
}
"(yes/no)? " {
send "yes\n"
expect {
"> " { }
"$ " { }
}
}
default {
send_user "Login failed\n"
exit
}
}

Conditionals

if statements are simple. This example says it all:

1
2
3
4
5
if { $a == "soup" } {
puts "You have soup!"
else {
puts "No soup for you!"
}

Looping

There are several ways to loop. I prefer the while approach

1
2
3
4
5
set count 10;
while {$count > 0 } {
puts "$count\n"
set count [expr $count-1];
}

This statement counts down from 10 and prints each number

Functions

Functions let you reuse code, like this:

1
2
3
proc do_something { a b c } {
puts "You should buy some $a, $b, and $c!\n"
}

You can call it like this

1
2
set running [do_something "apples" "bananas" "cantalopes"]

The output looks like this:
You should buy some apples, bananas, and cantalopes!"
Here's my small script for reference:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# !/usr/bin/expect -f
# Sync the local directory to the server, log in, delete the tkb directory, and rename site to tkb
set timeout -1
set user "username"
set server "server_ip"
spawn /usr/bin/scp -r site $user@$server:/usr/share/nginx/html
expect eof
spawn ssh $user@$server
expect "*"
send "cd /usr/share/nginx/html\r"
send "rm -rf tkb\r"
send "mv site tkb\r"
interact

Conclusion

Those are the most basic concepts of expect scripts. A series of actions that once had to be done by hand can now be turned into a script with expect.
Translated from Basic principles of using tcl-expect scripts
For more details, see expect