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
里面。如果 patlist
是 eof
或者 timeout
的情况,则发生文件结束或者超时时才执行相应的 action
动作。一般超时的默认值是 10
秒,但可以用类似 "set timeout 30"
之类的命令把超时时值设定为 30
秒。
1 2 3 4 5 6 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
在退出时会返回 3
,3
恰好就是第二个域。在最后一行中,^C
就是被切实的送给程序来验证程序是否恰当的退出。
2. 工具安装
源代码和下载地址都是由 Linux 软件基金会维护的(sourceforge)
1 2 3 4 5 6 $ sudo yum install -y gcc $ sudo yum install -y tcl tclx tcl-devel $ sudo yum install expect
1 2 3 4 5 6 7 $ sudo apt install -y gcc $ sudo apt install tcl $ sudo apt install expect
1 2 3 4 5 6 7 8 9 10 11 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
命令后的 send
和 expect
命令其实都是和使用 spawn
命令打开的进程进行交互的。
需要说明的是 interact
命令其实用的不多,一般情况下使用 spawn
、send
和 expect
命令就可以很好的完成任务了。但在一些特殊场合下,使用 interact
命令还是能够发挥很好作用的。interact
命令主要用于退出自动化进入人工交互。比如我们使用 spawn
、send
和 expect
命令完成了 ftp
登陆主机,执行下载文件任务,但是我们希望在文件下载结束以后,仍然可以停留在 ftp
命令行状态,以便手动的执行后续命令,此时使用 interact
命令就可以很好的完成这个任务。
编号
命令
作用
1
send
send 命令接收一个字符串并将该参数发送到进程中
2
expect
expect 通常用来等待进程的反馈再发送对应的交互命令
3
spawn
spawn 命令用来启动新的进程
4
interact
允许退出自动化进入人工交互
4. 控制结构
介绍 TCL 语言的控制结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 set timeout 10set alias_host [lindex $argv 0]set b1_password ASJZOMxlgM^9set b2_password a0yDuePSLUGMif {$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]" }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 set timeout 10set alias_host [lindex $argv 0]set b1_password ASJZOMxlgM^9set b2_password a0yDuePSLUGMswitch -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 }
1 2 3 4 5 6 7 8 9 10 set test 0while {$test <10} { set test [expr {$test + 1}] if {$test > 7} break if "$test < 3" continue }
1 2 3 4 5 6 7 8 proc Error {} { error "This is a error for test" } catch Error test puts $test
5. 简单使用
下面是一些简单的示例代码,主要帮助我们理解 expect 的使用。
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 set timeout 30spawn passwd [lindex $argv 1] set password [lindex $argv 2]expect "*New password:*" {send "$password \r" } expect "*Retype new password:*" {send "$password \r" } expect eof
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 set timeout 30 set host "100.200.200.200" set username "root" set password "123456" spawn ssh $username @$host expect { "*yes/no" {send "yes\r" ;exp_continue} "*password*" {send "$password \r" } } interact
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if {$argc < 3} { puts "Usage:cmd <host>:<username> -p <port>" exit 1 } set timeout 30set 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
1 2 3 4 5 6 7 8 #!/bin/bash read -p "please input you user:" -t30 remote_userread -p "please input you ip:" -t30 remote_ipread -p "please input you port:" -t30 remote_portecho "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 #!/bin/bash read -p "please input you user:" -t30 remote_userread -p "please input you ip:" -t30 remote_ipread -p "please input you port:" -t30 remote_portecho "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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 set ip [lindex $argv 0 ] set userid [lindex $argv 1 ] set mypassword [lindex $argv 2 ] set mycommand [lindex $argv 3 ] set timeout 10 spawn telnet $ip expect "username:" send "$userid \r" expect "password:" send "$mypassword \r" expect "%" send "$mycommand \r" expect "%" set results $expect_out (buffer) send "exit\r" expect eof
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 set ip [lindex $argv 0 ] set userid [lindex $argv 1 ] set mypassword [lindex $argv 2 ] set timeout 10 spawn ftp $ip expect "username:" send "$userid \r" expect "password:" send "$mypassword \r" expect "ftp>" send "bin\r" expect "ftp>" send "prompt\r" expect "ftp>" send "mget *\r" expect "ftp>" send "bye\r" expect eof
1 2 3 4 5 6 7 8 9 10 11 12 13 set ip [lindex $argv 0 ] set username [lindex $argv 1 ] set mypassword [lindex $argv 2 ] set timeout 10 spawn ssh $username @$ip expect { "*yes/no" { send "yes\r" ; exp_continue} "*password:" { send "$mypassword \r" } } interact
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 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 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 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 30set 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 。