A reusable expect dispatcher I kept around for running the same kind of operation across a list of servers — untar an index, restart a service, patch a config file. The trick is that the script reads the first command-line argument and dispatches to a same-named proc, so one script holds many small ops:
1 2 3 | ./script.exp UntarLuceneindexes ./script.exp stopLuceneServices ./script.exp startLuceneServices |
That’s the $functionName line at the bottom — it literally calls whatever proc name you passed in. Crude, but very handy when you’re iterating on a runbook.
The script below assumes you’ve set up SSH keys so ssh root@host connects without a password prompt. That keeps secrets out of the script and the shell history.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | #!/usr/bin/expect -f # Expect dispatcher. Beware: whitespace matters in Tcl. # Multi-line comments use a set / curly-brace trick: # set comment { # your multi-line comment # } # --------------------------------------------------- # Function definitions # --------------------------------------------------- if {[llength $argv] == 0} { send_user "Usage: script.exp FUNCTION_NAME\n" send_user "Example: script.exp UntarLuceneindexes\n" exit 1 } set functionName [lindex $argv 0] # Servers used by the Lucene-related procs. set aLuceneServers {your_server_1 your_server_2 your_server_3} proc printPass {} { send_user "\n\[PASS\]\n" } proc printFail {} { send_user "\n\[FAIL\]\n" exit 1 } proc simpleTest {} { set aProductionServers {your_server_1 your_server_2 your_server_3 your_server_4 your_server_5 your_server_6} foreach host $aProductionServers { send_user "Processing ... '$host'\n" set timeout 60 spawn ssh root@$host send "hostname\r" expect $host send "uptime\r" expect "load average" send "exit\r" } } proc UntarLuceneindexes {} { global aLuceneServers set sCurrentLuceneTar "index2_20151009.tar.bz2" foreach host $aLuceneServers { send_user "\n================================================\n" send_user "Processing ... '$host'\n" send_user "================================================\n" set timeout 60 spawn ssh root@$host send "hostname\r" expect $host send "su - path\r" send "pwd\r" expect "/home/path" send "ls $sCurrentLuceneTar | wc -l\r" expect "1" send "tar -xjvf $sCurrentLuceneTar &\r" send "sleep 2\r" send "ps aux | grep index2 | grep tar | wc -l\r" send "disown %1\r" expect "1" send_user "Done processing ... '$host'\n" } } proc updateDBRef {} { set apathServers {your_server_6} set newDB "some_db_20150925" foreach host $apathServers { send_user "\n================================================\n" send_user "Processing ... '$host'\n" send_user "================================================\n" set timeout 60 spawn ssh root@$host send "hostname\r" expect $host send "su - path\r" send "pwd\r" expect "/home/path" set propsFile "/home/path/path-current/SomeConfig.properties" send "grep -v 'com.somecompany.setting.server.db.pathdb2.url=' $propsFile > $propsFile.new\r" send "echo 'com.somecompany.setting.server.db.pathdb2.url=jdbc:postgresql://127.0.0.1:5432/$newDB' >> $propsFile.new\r" send "mv $propsFile.new $propsFile\r" send_user "Done processing ... '$host'\n" } } proc stopLuceneServices {} { global aLuceneServers foreach host $aLuceneServers { send_user "\n================================================\n" send_user "Processing ... '$host'\n" send_user "================================================\n" set timeout 60 spawn ssh root@$host send "hostname\r" expect $host send "service lucene stop\r" expect { "stopped PID" { printPass } "lucene is not running" { printPass } timeout { printFail } } send_user "Done processing ... '$host'\n" } } proc startLuceneServices {} { global aLuceneServers foreach host $aLuceneServers { send_user "\n================================================\n" send_user "Processing ... '$host'\n" send_user "================================================\n" set timeout 60 spawn ssh root@$host send "hostname\r" expect $host send "service lucene start\r" expect { "started PID" { printPass } timeout { printFail } } send_user "Done processing ... '$host'\n" } } # --------------------------------------------------- # Run # --------------------------------------------------- set timeout 60 log_file -noappend expect.log ;# default is append; -noappend overwrites $functionName |
Tcl or Expect? The post title says Tcl, but almost every interesting line above — spawn, expect, send, send_user, log_file, the #!/usr/bin/expect -f shebang — comes from Expect, an extension that sits on top of Tcl and lets you drive interactive programs (SSH, telnet, vendor CLIs) by pattern-matching on their output. Pure Tcl is the language; Expect is what makes a script like this one possible. 🤖
Use SSH keys, not expect-and-send-password. The original version of this script had blocks like expect “*assword: “ followed by send “YOUR_SERVER_PASSWORD\r”. That works, but it puts a plaintext password in your script and your shell history. The cleaner answer is SSH keypair auth — the script above assumes that’s already set up, so the password block disappears entirely. Reserve the expect-and-send-password pattern for the cases where it’s truly unavoidable: old network gear, vendor CLIs, devices that don’t accept keys.
You don’t always need Expect. If all you need is to run the same shell command across a list of servers, plain SSH in a loop is simpler and easier to debug:
1 2 3 | for host in your_server_1 your_server_2 your_server_3; do ssh "$host" "service lucene stop" done |
For larger fleets, parallel-ssh (pssh) or ansible -m shell -a “…” mygroup are usually saner. Expect earns its keep when you have to navigate an interactive prompt that doesn’t accept piped commands — confirm dialogs, paged output, vendor consoles. 💡