Linux 下自动应答工具 Expect 使用指南

Posted by Mike on 2021-02-05

Expect 是用于自动化交互式应用程序

1. 软件介绍

现代的 Shell 对程序提供了最小限度的控制(程序的开始/停止/关闭等),而把交互的特性留给了用户。这意味着有些程序,你不能非交互的运行,比如说 passwd 命令。有一些程序可以非交互的运行,但在很大程度上丧失了灵活性,比如说 fsck 命令。这表明 Unix 的工具构造逻辑开始出现问题。Expect 恰恰填补了其中的一些裂痕,解决了在 Unix 环境中长期存在着的一些问题。

Expect 使用 Tcl 作为语言核心,不管程序是交互和还是非交互的,Expect 都能运用。Tcl 实际上是一个子程序库,这些子程序库可以嵌入到程序里从而提供语言服务。 最终的语言有点象一个典型的 Shell 语言。里面有给变量赋值的 set 命令,控制程序执行的 if, for, continue 等命令,还能进行普通的数学和字符串操作。

Expect 是在 Tcl 基础上创建起来的并且还提供了一些 Tcl 所没有的命令:

  • spawn命令激活一个 Unix 程序来进行交互式的运行
  • send命令向进程发送字符串
  • expect命令等待进程的某些字符串且支持正规表达式并能同时等待多个字符串
1
2
# 命令格式
expect patlist1 action1 patlist2 action2.....

该命令一直等到当前进程的输出和以上的某一个模式相匹配,或者等到时间超过一个特定的时间长度,或者等到遇到了文件的结束为止。每一个 patlist 都由一个模式或者模式的表(lists)组成。如果有一个模式匹配成功,相应的 action 就被执行,执行的结果从 expect 返回。

被精确匹配的字符串(或者当超时发生时,已经读取但未进行匹配的字符串)被存贮在变量 expect_match 里面。如果 patlisteof 或者 timeout 的情况,则发生文件结束或者超时时才执行相应的 action 动作。一般超时的默认值是 10 秒,但可以用类似 "set timeout 30" 之类的命令把超时时值设定为 30 秒。

1
2
3
4
5
6
# 下面的一个程序段是从一个有关登录的脚本里面摘取的
# abort是在脚本的别处定义的过程,而其他的action使用类似与C语言的Tcl原语
expect "*welcome*" break
"*busy*" {print busy;continue}
"*failed*" abort
timeout abort

模式是通常的 C Shell 风格的正规表达式,模式必须匹配当前进程的从上一个 expect 或者 interact 开始的所有输出(所以统配符*****使用的非常的普遍)。但是,一旦输出超过 2000 个字节,前面的字符就会被忘记,这可以通过设定 match_max 的值来改变。

字符可以使用反斜杠来单独的引用,反斜杠也被用于对语句的延续,如果不加反斜杠的话,语句到一行的结尾处就结束了。这和 Tcl 也是一致的。Tcl 在发现有开的单引号或者开的双引号时都会继续扫描。而且,分号可以用于在一行中分割多个语句。这乍听起来有点让人困惑,但是,这是解释性语言的风格,但是,这确实是 Tcl 的不太漂亮的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spawn passwd [lindex $argv 1]

expect eof {exit 1}
timeout {exit 2}
"*No such user.*" {exit 3}
"*New password:"
send "[index $argv 2]\r"
expect eof {exit 4}
timeout {exit 2}
"*Password too long*" {exit 5}
"*Password too short*" {exit 5}
"*Retype new password:"
send "[index $argv 3] "
expect timeout {exit 2}
"*Mismatch*" {exit 6}
"*Password unchanged*" {exit 7}
" "
expect timeout {exit 2}
"*" {exit 6}
eof

这个脚本退出时用一个数字来表示所发生的情况。0 表示 passwd 程序正常运行,1 表示非预期的死亡,2 表示锁定,等等。使用数字是为了简单起见。expect 返回字符串和返回数字是一样简单的,即使是派生程序自身产生的消息也是一样的。实际上,典型的做法是把整个交互的过程存到一个文件里面,只有当程序的运行和预期一样的时候才把这个文件删除。否则这个 log 被留待以后进一步的检查。

这个 passwd 检查脚本被设计成由别的脚本来驱动。这第二个脚本从一个文件里面读取参数和预期的结果。对于每一个输入参数集,它调用第一个脚本并且把结果和预期的结果相比较。(因为这个任务是非交互的,一个普通的老式 shell 就可以用来解释第二个脚本)。比如说,一个 passwd 的数据文件很有可能就象下面一样。

1
2
3
4
5
6
passwd.exp    3    bogus    -        -
passwd.exp 0 fred abledabl abledabl
passwd.exp 5 fred abcdefghijklm -
passwd.exp 5 fred abc -
passwd.exp 6 fred foobar bar
passwd.exp 4 fred ^C -

第一个域的名字是要被运行的回归脚本。第二个域是需要和结果相匹配的退出值。第三个域就是用户名。第四个域和第五个域就是提示时应该输入的密码。减号仅 仅表示那里有一个域,这个域其实绝对不会用到。在第一个行中,bogus 表示用户名是非法的,因此 passwd 会响应说:没有此用户。expect 在退出时会返回 33 恰好就是第二个域。在最后一行中,^C 就是被切实的送给程序来验证程序是否恰当的退出。

2. 工具安装

源代码和下载地址都是由 Linux 软件基金会维护的(sourceforge)

1
2
3
4
5
6
# 因为Expect需要Tcl编程语言的支持
$ sudo yum install -y gcc
$ sudo yum install -y tcl tclx tcl-devel

# centos
$ sudo yum install expect
1
2
3
4
5
6
7
# 因为Expect需要Tcl编程语言的支持
$ sudo apt install -y gcc
$ sudo apt install tcl


# ubuntu
$ sudo apt install expect
1
2
3
4
5
6
7
8
9
10
11
# 下载源代码包
# 官网主页地址: http://sourceforge.net/projects/expect/
wget "https://sourceforge.net/projects/expect/files/Expect/5.45.4/expect5.45.4.tar.gz/download"

# 源代码编译
$ sudo ./configure \
--with-tcl=/usr/lib \
--with-tclinclude=/usr/include/tcl-private/generic

# 源代码安装
$ sudo make && make install

3. 基础知识

主要介绍常见的 4 个命令的使用方式

  • 我们知道,send 命令用于发送信息到进程中,expect 命令则是根据进程反馈的信息进行对应逻辑的交互的。而 spawn 命令后的 sendexpect 命令其实都是和使用 spawn 命令打开的进程进行交互的。
  • 需要说明的是 interact 命令其实用的不多,一般情况下使用 spawnsendexpect 命令就可以很好的完成任务了。但在一些特殊场合下,使用 interact 命令还是能够发挥很好作用的。interact 命令主要用于退出自动化进入人工交互。比如我们使用 spawnsendexpect 命令完成了 ftp 登陆主机,执行下载文件任务,但是我们希望在文件下载结束以后,仍然可以停留在 ftp 命令行状态,以便手动的执行后续命令,此时使用 interact 命令就可以很好的完成这个任务。
编号 命令 作用
1 send send 命令接收一个字符串并将该参数发送到进程中
2 expect expect 通常用来等待进程的反馈再发送对应的交互命令
3 spawn spawn 命令用来启动新的进程
4 interact 允许退出自动化进入人工交互

4. 控制结构

介绍 TCL 语言的控制结构

  • [1] if else
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/expect

set timeout 10
set alias_host [lindex $argv 0]
set b1_password ASJZOMxlgM^9
set b2_password a0yDuePSLUGM

if {$argc!=1} {
echo "请输入想要远程连接的服务器: [b1|b2]"
exit 1
}

if {$alias_host=="b1"} {
spawn ssh escape@192.168.100.100 -p 22
expect "*password*" {send "$b1_password\r"}
interact
} elseif {$alias_host=="b2"} {
spawn ssh escape@192.168.100.101 -p 22
expect "*password*" {send "$b2_password\r"}
interact
} else {
send "请输入想要远程连接的服务器: [b1|b2]"
}
  • [2] switch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/expect

set timeout 10
set alias_host [lindex $argv 0]
set b1_password ASJZOMxlgM^9
set b2_password a0yDuePSLUGM

switch -glob -- $file1 {
b1 {
spawn ssh escape@192.168.100.100 -p 22
expect "*password*" {send "$b1_password\r"}
interact
}
b2 {
spawn ssh escape@192.168.100.101 -p 22
expect "*password*" {send "$b2_password\r"}
interact
}
  • [3] while
1
2
3
4
5
6
7
8
9
10
#!/usr/bin/expect

set test 0
while {$test<10} {
set test [expr {$test + 1}]
if {$test > 7}
break
if "$test < 3"
continue
}
  • [4] catch
1
2
3
4
5
6
7
8
#!/usr/bin/expect

proc Error {} {
error "This is a error for test"
}

catch Error test
puts $test

5. 简单使用

下面是一些简单的示例代码,主要帮助我们理解 expect 的使用。

  • [1] 系统指定修改用户密码
1
2
3
4
5
6
7
[escape@linuxworld ~]$ passwd
Changing password for user escape.
Changing password for escape.
(current) UNIX password:
New password:
Retype new password:
passwd: all authentication tokens updated successfully.
1
2
3
4
5
6
7
8
9
10
# password.exp
#!/usr/bin/expect -d

set timeout 30

spawn passwd [lindex $argv 1]
set password [lindex $argv 2]
expect "*New password:*" {send "$password\r"}
expect "*Retype new password:*" {send "$password\r"}
expect eof
  • [2] 登陆远程服务器并停留在远程服务器上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# login.exp
#!/usr/bin/expect

set timeout 30 # 设置超时时间
set host "100.200.200.200"
set username "root"
set password "123456"

# 给ssh运行进程加个壳用来传递交互指令
spawn ssh $username@$host
# 判断上次输出结果里是否包含指定的字符串
expect {
# exp_continue表示继续执行下一步
"*yes/no" {send "yes\r";exp_continue}
# 匹配即可发送密码到对应进程中
"*password*" {send "$password\r"}
}
# 执行完成后保持交互状态
interact
  • [3] 传输参数执行登并停留在远程服务器上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# login.exp
#!/usr/tcl/bin/expect

# $argc表示参数个数
if {$argc < 3} {
puts "Usage:cmd <host>:<username> -p <port>"
exit 1
}

# 获取第几个参数的内容
set timeout 30
set host [lindex $argv 0]
set username [lindex $argv 1]
set password [lindex $argv 2]
set port [lindex $argv 3]

spawn ssh $username@$host -p $port
expect "*password*" {send "$password\r"}
interact
  • [4] 在 shell 脚本中使用 expect
1
2
3
4
5
6
7
8
# [1] 直接添加expect脚本文件

#!/bin/bash
read -p "please input you user:" -t30 remote_user
read -p "please input you ip:" -t30 remote_ip
read -p "please input you port:" -t30 remote_port
echo "ssh $remote_user:$remote_ip -p $remote_port"
./login.exp $remote_user $remote_ip $remote_port
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# [2] 在shell脚本直接写入expect命令

#!/bin/bash
read -p "please input you user:" -t30 remote_user
read -p "please input you ip:" -t30 remote_ip
read -p "please input you port:" -t30 remote_port
echo "ssh $remote_user:$remote_ip -p $remote_port"

expect -d <<EOF
spawn ssh $remote_ip
expect {
"*yes/no" {send "yes\r";exp_continue}
"*password:" {send "Xuansiwei123!\r"}
}
exit
expect eof;
EOF

6. 高级示例

弄懂下面的高级玩法,就可以应对日常的工作使用了。

  • [1] 自动 telnet 会话
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
#!/usr/bin/expect -f

set ip [lindex $argv 0 ] # 接收第1个参数,作为IP
set userid [lindex $argv 1 ] # 接收第2个参数,作为userid
set mypassword [lindex $argv 2 ] # 接收第3个参数,作为密码
set mycommand [lindex $argv 3 ] # 接收第4个参数,作为命令
set timeout 10 # 设置超时时间

# 向远程服务器请求打开一个telnet会话,并等待服务器询问用户名
spawn telnet $ip
expect "username:"
# 输入用户名,并等待服务器询问密码
send "$userid\r"
expect "password:"
# 输入密码,并等待键入需要运行的命令
send "$mypassword\r"
expect "%"
# 输入预先定好的密码,等待运行结果
send "$mycommand\r"
expect "%"
# 将运行结果存入到变量中,显示出来或者写到磁盘中
set results $expect_out(buffer)
# 退出telnet会话,等待服务器的退出提示EOF
send "exit\r"
expect eof
  • [2] 自动建立 FTP 会话
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
#!/usr/bin/expect -f

set ip [lindex $argv 0 ] # 接收第1个参数,作为IP
set userid [lindex $argv 1 ] # 接收第2个参数,作为Userid
set mypassword [lindex $argv 2 ] # 接收第3个参数,作为密码
set timeout 10 # 设置超时时间

# 向远程服务器请求打开一个FTP会话,并等待服务器询问用户名
spawn ftp $ip
expect "username:"
# 输入用户名,并等待服务器询问密码
send "$userid\r"
expect "password:"
# 输入密码,并等待FTP提示符的出现
send "$mypassword\r"
expect "ftp>"
# 切换到二进制模式,并等待FTP提示符的出现
send "bin\r"
expect "ftp>"
# 关闭ftp的提示符
send "prompt\r"
expect "ftp>"
# 下载所有文件
send "mget *\r"
expect "ftp>"
# 退出此次ftp会话,并等待服务器的退出提示EOF
send "bye\r"
expect eof
  • [3] 自动登录 ssh
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/expect -f

set ip [lindex $argv 0 ] # 接收第1个参数,作为IP
set username [lindex $argv 1 ] # 接收第2个参数,作为username
set mypassword [lindex $argv 2 ] # 接收第3个参数,作为密码
set timeout 10 # 设置超时时间

spawn ssh $username@$ip # 发送ssh请求
expect { # 返回信息匹配
"*yes/no" { send "yes\r"; exp_continue} # 第一次ssh连接会提示yes/no,继续
"*password:" { send "$mypassword\r" } # 出现密码提示,发送密码
}
interact # 交互模式,用户会停留在远程服务器上面
  • [4] 自动登录 ssh 执行命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/expect

set IP [lindex $argv 0]
set USER [lindex $argv 1]
set PASSWD [lindex $argv 2]
set CMD [lindex $argv 3]

spawn ssh $USER@$IP $CMD
expect {
"(yes/no)?" {
send "yes\r"
expect "password:"
send "$PASSWD\r"
}
"password:" {send "$PASSWD\r"}
"* to host" {exit 1}
}
expect eof
  • [5] 批量登录 ssh 服务器执行操作范例 => for 循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/expect

for {set i 10} {$i <= 12} {incr i} {
set timeout 30
set ssh_user [lindex $argv 0]
spawn ssh -i .ssh/$ssh_user abc$i.com

expect_before "no)?" {
send "yes\r" }
sleep 1
expect "password*"
send "hello\r"
expect "*#"
send "echo hello expect! > /tmp/expect.txt\r"
expect "*#"
send "echo\r"
}

exit
  • [6] 批量登录 ssh 并执行命令 => foreach 语法
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
#!/usr/bin/expect

if {$argc!=2} {
send_user "usage: ./expect ssh_user password\n"
exit
}

foreach i {11 12} {
set timeout 30
set ssh_user [lindex $argv 0]
set password [lindex $argv 1]
spawn ssh -i .ssh/$ssh_user root@xxx.yy.com
expect_before "no)?" {
send "yes\r" }
sleep 1

expect "Enter passphrase for key*"
send "password\r"
expect "*#"
send "echo hello expect! > /tmp/expect.txt\r"
expect "*#"
send "echo\r"
}

exit
  • [7] 批量 ssh 执行命令 => 用 shell 调用 tclsh 方式、多进程同时执行
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
#!/bin/sh

exec tclsh $0 "$@"
package require Expect
set username [lindex $argv 0]
set password [lindex $argv 1]
set argv [lrange $argv 2 end]
set prompt "(%|#|\\$) $"

foreach ip $argv {
spawn ssh -t $username@$ip sh
lappend ids $spawn_id
}

expect_before -i ids eof {
set index [lsearch $ids $expect_out(spawn_id)]
set ids [lreplace $ids $index $index]
if [llength $ids] exp_continue
}
expect -i ids "(yes/no)\\?" {
send -i $expect_out(spawn_id) yes\r
exp_continue
} -i ids "Enter passphrase for key" {
send -i $expect_out(spawn_id) \r
exp_continue
} -i ids "assword:" {
send -i $expect_out(spawn_id) $password\r
exp_continue
} -i ids -re $prompt {
set spawn_id $expect_out(spawn_id)
send "echo hello; exit\r"
exp_continue
} timeout {
exit 1
}
  • [8] 使用 ssh 自动登录 expect 脚本 => ssh.expect
1
2
3
4
5
6
7
8
9
10
11
12
The authenticity of host '192.168.xxx.xxx (192.168.xxx.xxx)' can't be established.
RSA key fingerprint is 25:e8:4c:89:a3:b2:06:ee:de:66:c7:7e:1b:fa:1c:c5.
Are you sure you want to continue connecting (yes/no)?

Warning: Permanently added '192.168.xxx.xxx' (RSA) to the list of known hosts.
Enter passphrase for key '/data/key/my_dsa':

Last login: Sun Jan 26 13:39:37 2019 from 192.168.xxx.xxx
[root@master003 ~]#
root@192.168.xxx.xxx's password:

Last login: Thu Jan 23 17:50:43 2019 from 192.168.xxx.xxx
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
#!/usr/bin/expect -f

if {$argc < 4} {
send_user "Usage:\n $argv0 IPaddr User Passwd Port Passphrase\n"
puts stderr "argv error!\n"
sleep 1
exit 1
}

set timeout 30
set ip [lindex $argv 0 ]
set user [lindex $argv 1 ]
set passwd [lindex $argv 2 ]
set port [lindex $argv 3 ]
set passphrase [lindex $argv 4 ]

if {$port == ""} {
set port 22
}

spawn ssh $user@$ip -p $port

expect_before "(yes/no)\\?" {
send "yes\r"}

expect \
"Enter passphrase for key*" {
send "$passphrase\r"
exp_continue
} " password:?" {
send "$passwd\r"
exp_continue
} "*\[#\\\$]" {
interact
} "* to host" {
send_user "Connect faild!"
exit 2
} timeout {
send_user "Connect timeout!"
exit 2
} eof {
send_user "Lost connect!"
exit
}

7. 参考博客

本文转载自:「 Escape 的博客 」,原文:https://tinyurl.com/y4wa98s9,版权归原作者所有。欢迎投稿,投稿邮箱: editor@hi-linux.com