Linux使用Expect做自动化交互

背景

在Linux进行ssh多次登录,sudo输入切换用户,打开screen/tmux时需要多次输入重复命令,思考如何解决重复性输入问题,经过探索找到了Expect脚本,其主要是用TCL语法(在网络仿真模拟中有用到过)。本文主要真对上述场景遇到的问题和基本语法做介绍,另外读者可以通过给出的资料文档进一步探索。

简介

维基百科: Expect
SourceForge: The Expect Home Page

TCL语法

由于Expect是建立在TCL语言基础上的一个工具,首先看一些TCL常见语法。

变量

一般用于保存hostname/username/password

1
2
3
4
5
6
7
8
9
# 定义一个变量
set username "leo"
set hostname "domain.local"

# 获取命令行参数(与bash不同的是第一个参数index为0)
## 获取第一个参数
set hostname [lindex $argv 0]
## 获取第二个参数
set passwrod [lindex $argv 1]

数组

一般用于保存服务器列表,显示并供用户选择

1
2
3
4
5
6
7
# 定义一个数组
set host_list(0) {host1 127.0.0.1 root}
set host_list(1) {host2 192.168.1.1 admin}
set host_list(2) {host3 8.8.8.8 dns}

# 获取数组长度
set arrlen [array size host_list]

获取用户输入

一般用于接收用户输入选择,如密码、服务器等

1
2
3
4
5
6
7
8
9
10
# 关闭终端回显,用于保证输入密码安全
system stty -echo
# 发送给用户,提示输入密码
send_user "please input password:"
# 获取用户输入
expect_user -re "(.*)\n"
# 将用户输入存入变量
set password $expect_out(1, string)
# 打开终端回显
system stty echo

if条件分支

用于逻辑判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 判断变量是否相等,相等返回0,否则返回1
[string compare $host "123"]

# 获取第一个参数
set host [lindex $argv 0]
if ![string compare $host ""] { #注意化括号前必须有一个空格,具体参考TCL语言规范
# 如果host变量为空字符串
}

expect_user -re "(.*)\n"
set choose $expect_out(1,string)
if {[string compare [string toupper $choose] "N"] == 0} {
# 如果choose变量为"N"
exit
} elseif {[scan $choose {%[0-9]} choose] == 0} {
# 如果choose不是数字,scan用户匹配,详情参考TCL语法
exit
} elseif { $choose < 0 || $choose >= $len } {
# 输入非法,退出
exit
}

for循环

1
2
3
4
5
set len [array size host_list]
for {set index 0} {$index < $len} {incr index} {
puts "$index -> $host_list($index)"
}
# incr为自增关键字,puts用于输出给用户,类似send_user

Expect应用介绍

接受窗口改变信号

如果是用expect登录后,是用screen或tmux,在用户终端大小调整情况下,因为窗口大小改变信号为同步到远程服务器会导致内容错乱。

1
2
3
4
5
trap {
set rows [stty rows]
set cols [stty columns]
stty rows $rows columns $cols < $spawn_out(slave,name)
} WINCH

ssh自动登录

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
# 设置timeout时间为10秒
set timeout 10

# 执行ssh命令,登录远程服务器
spawn ssh $username@$hostname
# 匹配服务器返回的信息
expect {
"yes/no" { send "yes\r"; exp_continue }
"passsword" { send "$password\r"; exp_continue }
"$" { send "hostname\r" } # 登录后发送hostname,用于debug
"Permission denied" { send_user "Permission denied (publickey,password)."; exit }
incorrent { puts "Invalid account or password!"; exit }
timeout { puts "Connection to $hostname timeout!"; exit }
eof { puts "Connection to $hostname failed: $expect_out(buffer)"; exit }
}

# 检查是否登录成功
expect {
"\\\$" { send "whoami\r" }
}

# 登录screen
sleep 0.2
send "script /dev/null\r"
expect "Script started"
sleep 0.2
send "screen -ls\r"
expect {
-re ".*Detached" { send "screen -r $username\r" }
-re ".*Attached" { send "screen -x $username\r" }
-re "/var/run/screen" { send "screen -dmS $username; screen -r $username\r" }
}

# 登录后的环境直接交给用户
interact

exp_continue可以理解为for/while的continue,Expect可以看作重新执行expect匹配