十六、Shell之expect自动化交互程
一、什么是Expect
Expect是一个用来实现自动交互功能的软件套件(Expect is a software suite for automating interactive tools,这是作者的定义),是基于TCL的脚本编程工具语言,方便学习,功能强大。
在现今的企业运维中,自动化运维已经成为运维的主流趋势,但是在很多情况下,执行系统命令或程序时,系统会以交互式的形式要求运维人员输入指定的字符串,之后才能继续执行命令。例如,为用户设置密码时,一般情况下就需要手工输入2次密码,如下:
[root@node1 ~]# passwd ywx Changing password for user ywx. New password: BAD PASSWORD: The password is shorter than 8 characters Retype new password: passwd: all authentication tokens updated successfully. [root@node1 ~]#
SSH远程第一次链接需要两次交互式输入
[root@node1 ~]# ssh 192.168.32.213 The authenticity of host '192.168.32.213 (192.168.32.213)' can't be established. ECDSA key fingerprint is SHA256:ObDtb5FnND2UfusUNwcwuhGZJnTHpUNRrIcruOi1p7c. ECDSA key fingerprint is MD5:cc:ea:8a:39:a9:c3:10:af:a8:73:01:bf:41:c6:07:48. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added '192.168.32.213' (ECDSA) to the list of known hosts. root@192.168.32.213's password: Last login: Sun Sep 20 18:07:00 2020 from 192.168.32.102 [root@node3 ~]#
简单地说,Expect就是用来自动实现与交互式程序通信的,而无需管理员的手工干预。
以下是Expect的自动交互工作流程简单说明,依次执行如下操作: spawn启动指定进程→expect获取期待的关键字→send向指定进程发送指定字符→进程执行完毕,退出结束。
二、Expect的安装
[root@node1 ~]# rpm -qa expect [root@node1 ~]# yum install -y expect [root@node1 ~]# rpm -qa expect expect-5.45-14.el7_1.x86_64 #expect常用命令: spawn 交互程序开始后面跟命令或者指定程序 expect 获取匹配信息匹配成功则执行expect后面的程序动作 send exp_send 用于发送指定的字符串信息 exp_continue 在expect中多次匹配就需要用到 send_user 用来打印输出 相当于shell中的echo exit 退出expect脚本 eof expect执行结束 退出 set 定义变量 puts 输出变量 set timeout 设置超时时间
实验环境 node1: 192.168.32.211 node2: 192.168.32.212 1、在node1使用expect实现自动修改ywx用户的密码 2、在node1上远程执行node2的uptime命令
#! /usr/bin/expect spawn passwd ywx expect { "New password:" { send "123456\r"; exp_continue } "Retype new password:" { send "123456\r" } } expect eof
[root@node1 scripts]# echo 654321|passwd --stdin ywx Changing password for user ywx. passwd: all authentication tokens updated successfully. [root@node1 scripts]# expect expect3.exp spawn passwd ywx Changing password for user ywx. New password: BAD PASSWORD: The password is shorter than 8 characters Retype new password: passwd: all authentication tokens updated successfully. [root@node1 scripts]#
1)不需要输入yes/or
#!/usr/bin/expect #<==脚本开头解析器,和Shell类似,表示程序使用Expect解析。 spawn ssh root@192.168.32.212 uptime #<==执行ssh命令(注意开头必须要有spawn,否则无法实现交互)。 expect "*password" #<==利用Expect获取执行上述ssh命令输出的字符串是否为期待的字符串*password,这里的*是通配符。 send "123456\n" #<==当获取到期待的字符串*password时,则发送123456密码给系统,\n为换行。 expect eof #<==处理完毕后结束Expect。
测试脚本
[root@node1 scripts]# expect expect1.exp #使用expect命令来执行expect脚本,相当于bash;或者./expect1.exp(需要x权限) spawn ssh root@192.168.32.212 uptime root@192.168.32.212's password: 19:25:08 up 27 days, 22:14, 0 users, load average: 0.00, 0.01, 0.05
2)输入yes/no
#! /usr/bin/expect spawn ssh root@192.168.32.212 uptime expect { "*yes/no" { send "yes\r"; exp_continue } "*password:" { send "123456\r" } } expect eof
[root@node1 scripts]# expect expect2.exp spawn ssh root@192.168.32.212 uptime The authenticity of host '192.168.32.212 (192.168.32.212)' can't be established. ECDSA key fingerprint is SHA256:ObDtb5FnND2UfusUNwcwuhGZJnTHpUNRrIcruOi1p7c. ECDSA key fingerprint is MD5:cc:ea:8a:39:a9:c3:10:af:a8:73:01:bf:41:c6:07:48. Are you sure you want to continue connecting (yes/no)? yes Warning: Permanently added '192.168.32.212' (ECDSA) to the list of known hosts. root@192.168.32.212's password: 21:04:30 up 27 days, 23:53, 0 users, load average: 0.00, 0.01, 0.05
1)在Expect自动交互程序执行的过程中,spawn命令是一开始就需要使用的命令,通过spawn执行一个命令或程序,之后所有的Expect操作都会在这个执行过的命令或程序进程中进行,包括自动交互功能,因此如果没有spawn命令,Expect程序将会无法实现自动交互。 2)在Expect自动交互程序的执行过程中,当使用spawn命令执行一个命令或程序之后,会提示某些交互式信息,expect命令的作用就是获取spawn命令执行后的信息,看看是否和其事先指定的相匹配,一旦匹配上指定的内容就执行expect后面的动作,expect命令也有一些选项,相对用得较多的是-re,表示使用正则表达式的方式来匹配。
#!/usr/bin/expect #<==脚本解释器。 spawn ssh root@192.168.32.212 uptime #<==开启expect自动交互式,执行ssh命令。 expect "*password" {send "123456\n"} #<==如果ssh命令输出匹配*password,就发送123456给系统。 expect eof #<==要想输出结果,还必须加eof,表示expect结束。
#!/usr/bin/expect spawn ssh root@192.168.33.130 uptime expect "*password:" send "123456\n" expect eof
3、多次匹配不同的字符串
expect命令还有一种高级用法,即它可以在一个expect匹配中多次匹配不同的字符串,并给出不同的处理动作,此时只需要将匹配的所有字符串放在一个{}(大括号)中就可以了,当然还要借助exp_continue指令实现继续匹配。
#! /usr/bin/expect spawn ssh root@192.168.32.212 uptime expect { "*yes/no" { send "yes\r"; exp_continue } "*password:" { send "123456\r" } } expect eof #注意: 1)exp_send和send类似,后面的\r(回车)和前文的\n(换行)类似。 2)expect{},类似多行expect。 3)匹配多个字符串,需要在每次匹配并执行动作后,加上exp_continue,最后一个匹配的字符串除外。
[root@node1 scripts]# cat test.sh #<==这里是Shell脚本! #!/bin/sh read -p 'Please input your username:' name read -p 'Please input your password:' pass read -p 'Please input your email:' mail echo -n "your name is $name," echo -n "your password is $pass," echo "your email is $mail."
执行结果如下:
[root@node1 scripts]# sh test.sh Please input your username:ywx #<==提示输入,只能手动输入对应字符串。 Please input your password:123456 #<==提示输入,只能手动输入对应字符串。 Please input your email:441520481@qq.com #<==提示输入,只能手动输入对应字符串。 your name is ywx,your password is 123456,your email is 441520481@qq.com.
#!/usr/bin/expect spawn /bin/sh /script/test.sh #<==执行上述Shell脚本,注意这里使用的是相对路径。 expect { "username" {exp_send "ywx\r";exp_continue} #<==若获取到的是username信息,则自动输入oldboy。 "*pass*" {send "123456\r";exp_continue} #<==若获取到的是*pass*信息,则自动输入123456。 "*mail*" {exp_send "441520481@qq.com\r"} #<==若获取到的是*mail*信息,则自动输入邮件地址。 } expect eof
执行结果如下:
[root@oldboy script]# expect expect4.exp #<==回车后,无任何人工交互,直接输出结果。 spawn /bin/sh /script/test.sh Please input your username:ywx #<==自动输入需要的字符串。 Please input your password:123456 #<==自动输入需要的字符串。 Please input your email:441520481@qq.com #<==自动输入需要的字符串。 your name is ywx,your password is 123456,your email is 441520481@qq.com
在上面的案例中,我们已经看到了exp_send和send命令的使用方法,这两个命令是Expect中的动作命令,用法类似,即在expect命令匹配指定的字符串后,发送指定的字符串给系统,这些命令可以支持一些特殊转义符号,例如:\r表示回车、\n表示换行、\t表示制表符等。
#!/usr/bin/expect spawn /bin/sh /script/test.sh #<==执行上述Shell脚本,注意这里使用的是相对路径。 expect { "username" {exp_send "ywx\r";exp_continue} #<==若获取到的是username信息,则自动输入oldboy。 "*pass*" {send "123456\r";exp_continue} #<==若获取到的是*pass*信息,则自动输入123456。 "*mail*" {exp_send "441520481@qq.com\r"} #<==若获取到的是*mail*信息,则自动输入邮件地址。 } expect eof
·-i:指定spawn_id,用来向不同的spawn_id进程发送命令,是进行多程序控制的参数。
·-s:s代表slowly,即控制发送的速度,使用的时候要与expect中的变量send_slow相关联。
6、exp_continue命令
前面使用过这个exp_continue命令,它一般处于expect命令中,属于一种动作命令,一般用在匹配多次字符串的动作中,从命令的拼写就可以看出命令的作用,即让Expect程序继续匹配的意思。
#!/usr/bin/expect spawn /bin/sh /script/test.sh #<==执行上述Shell脚本,注意这里使用的是相对路径。 expect { "username" {exp_send "ywx\r";exp_continue} #<==若获取到的是username信息,则自动输入oldboy。 "*pass*" {send "123456\r";exp_continue} #<==若获取到的是*pass*信息,则自动输入123456。 "*mail*" {exp_send "441520481@qq.com\r"} #<==若获取到的是*mail*信息,则自动输入邮件地址。 } expect eof
7、send_user命令
#!/usr/bin/expect send_user "I am ywx.\n" #<==\n表示换行。 send_user "I am a linuxer,\t" #<==\t表示Tab键。 send_user "My blog is http://kingseal.top\n"
[root@node1 scripts]# expect expect4.exp
I am ywx.
I am a linuxer, My blog is http://kingseal.top
exit命令的功能类似于Shell中的exit,即直接退出Expect脚本,除了最基本的退出脚本功能之外,还可以利用这个命令对脚本做一些关闭前的清理和提示等工作。
#!/usr/bin/expect send_user "I am ywx.\n" #<==\n表示换行。 send_user "I am a linuxer,\t" #<==\t表示Tab键。 send_user "My blog is http://kingseal.top\n" exit -onexit { send_user "Good bye.\n" }
[root@node1 scripts]# expect expect5.exp I am ywx. I am a linuxer, My blog is http://kingseal.top Good bye.
六、Expect程序变量
1、Expect程序变脸定义
定义变量的基本语法如下:
set 变量名 变量值
puts $变量名
send_user $变量名
案例
#!/usr/bin/expect set PWD "123456" puts $PWD send_user "$PWD\n"
测试脚本
[root@node1 scripts]# expect expect6.exp 123456 123456
在Expect里也有与Shell脚本里的、1、$#等类似的特殊参数变量,用于接收及控制Expect脚本传参。
1、在Expect中$argv表示参数数组,可以使用[lindex$argv n]接收Expect脚本传参,n从0开始,分别表示第一个[lindex$argv 0]参数、第二个[lindex$argv 1]参数、第三个[lindex$argv 2]参数…… 2、Expect接收参数的方式和bash脚本的方式有些区别,bash是通过$0……$n这种方式来接收的,而Expect是通过set<变量名称>[lindex$argv<param index>]来接收的。 3、除了基本的位置参数外,Expect也支持其他的特殊参数,例如:$argc表示传参的个数,$argv0表示脚本的名字。
案例
#!/usr/bin/expect #define var set file [lindex $argv 0] #<==相当于Shell里脚本传参的$1。 set host [lindex $argv 1] #<==相当于Shell里脚本传参的$2。 set dir [lindex $argv 2] #<==相当于Shell里脚本传参的$3。 send_user "$file\t$host\t$dir\n" puts "$file\t$host\t$dir\n"
[root@node1 scripts]# expect expect7.exp /etc/hosts 192.168.32.211 /tmp /etc/hosts 192.168.32.211 /tmp /etc/hosts 192.168.32.211 /tmp
针对Expect脚本传参的个数及脚本名参数
#!/usr/bin/expect set file [lindex $argv 0] set host [lindex $argv 1] set dir [lindex $argv 2] puts "$file\t$host\t$dir" puts $argc #参数个数等于bash中的$# puts $argv0 #等于bash中的$0
测试脚本
[root@node1 scripts]# expect expect8.exp /etc/hosts 192.168.32.211 /tmp /etc/hosts 192.168.32.211 /tmp 3 expect8.exp
七、Expect程序中的if条件语句
1、Expect程序中if条件语句的基本语法
if {条件表达式} { 指令 } 或 if {条件表达式} { 指令 } else { 指令 } #说明:if关键字后面要有空格,else关键字前后都要有空格,{条件表达式}大括号里面靠近大括号处可以没有空格,将指令括起来的起始大括号“{”前要有空格。
1、使用if语句判断脚本传参的个数,如果不符则给予提示。
#!/usr/bin/expect if { $argc!= 3 } { #<==$argc为传参的个数,相当于Shell里的$#。 send_user "usage:expect $argv0 file host dir\n" #<==给予提示,$argv0代表脚本的名字。 exit #<==退出脚本。 } #define var set file [lindex $argv 0] set host [lindex $argv 1] set dir [lindex $argv 2] puts "$file\t$host\t$dir"
测试
[root@node1 scripts]# expect expect9.exp usage:expect expect9.exp file host dir #<==expect9.exp就是$argv0输出的结果。 [root@node1 scripts]# expect expect9.exp /etc/hosts 192.168.32.212 /home/ywx #<==传三个参数。 /etc/hosts 192.168.32.212 /home/ywx #<==这是脚本后面的三个参数。
#!/usr/bin/expect if {$argc!= 26} { puts "bad." } else { puts "good." }
测试脚本
[root@node1 scripts]# expect expect10.exp
bad.
[root@node1 scripts]# expect expect10.exp {a..z}
good.
1、eof关键字
eof(end-of-file)关键字用于匹配结束符,前面已经使用过eof这个关键字了
#!/usr/bin/expect spawn /bin/sh /script/test.sh #<==执行上述Shell脚本,注意这里使用的是相对路径。 expect { "username" {exp_send "ywx\r";exp_continue} #<==若获取到的是username信息,则自动输入oldboy。 "*pass*" {send "123456\r";exp_continue} #<==若获取到的是*pass*信息,则自动输入123456。 "*mail*" {exp_send "441520481@qq.com\r"} #<==若获取到的是*mail*信息,则自动输入邮件地址。 } expect eof
timeout是Expect中的一个控制时间的关键字变量,它是一个全局性的时间控制开关,可以通过为这个变量赋值来规定整个Expect操作的时间,注意这个变量是服务于Expect全局的,而不是某一条命令,即使命令没有任何错误,到了时间仍然会激活这个变量,此外,到时间后还会激活一个处理及提示信息开关。
#!/usr/bin/expect spawn ssh root@192.168.32.212 uptime set timeout 30 #<==设置30秒超时。 expect "yes/no" {exp_send "yes\r";exp_continue} expect timeout {puts "Request timeout by ywx.";return} #<==当到达30秒后就超时,打印指定输出后退出。
测试脚本
[root@node1 scripts]# expect expect11_.exp spawn ssh root@192.168.32.212 uptime root@192.168.32.212's password:Request timeout by ywx.
在expect{}的用法中,还可以使用下面的timeout语法:
#!/usr/bin/expect spawn ssh root@192.168.32.212 uptime expect { -timeout 3 "yes/no" {exp_send "yes\r";exp_continue} timeout {puts "Request timeout by ywx.";return} }
[root@node1 scripts]# expect expect12.exp spawn ssh root@192.168.32.212 uptime root@192.168.32.212's password:Request timeout by ywx.
九、企业生产场景下的Expect案例
1、开发Expect脚本实现自动交互式批量执行命令。
#!/usr/bin/expect if { $argc!= 2 } { puts "usage:expect $argv0 ip command" exit } #define var set ip [lindex $argv 0] set cmd [lindex $argv 1] set password "123456" # spawn ssh root@$ip $cmd expect { "yes/no" {send "yes\r";exp_continue} "*password" {send "$password\r"} } expect eof
测试脚本
[root@node1 scripts]# expect expect13.exp 192.168.32.212 uptime spawn ssh root@192.168.32.212 uptime root@192.168.32.212's password: 14:20:53 up 16:26, 2 users, load average:0.07, 0.03, 0.01 [root@node1 scripts]# expect expect13.exp 192.168.32.212 "free -m" spawn ssh root@192.168.32.212 free -m root@192.168.32.212's password: total used free shared buffers cached Mem: 981 492 488 0 42 293 -/+ buffers/cache: 156 824 Swap: 767 0 767
#!/bin/sh if [ $# -ne 1 ] then echo "USAGE:$0 cmd" exit 1 fi cmd=$1 for n in 128 129 130 do expect /script/expect13.exp 192.168.32.$n "$cmd" #<==带双引号接收带参数的命令。 done
expect13.exp
#!/usr/bin/expect if { $argc!= 2 } { puts "usage:expect $argv0 ip command" exit } #define var set ip [lindex $argv 0] set cmd [lindex $argv 1] set password "123456" # spawn ssh root@$ip $cmd expect { "yes/no" {send "yes\r";exp_continue} "*password" {send "$password\r"} } expect eof
3、开发Expect脚本以实现自动交互式批量发送文件或目录。
1)实现Expect自动交互的脚本:
[root@node1 scripts]# cat 18_13_1.exp #!/usr/bin/expect if { $argc!= 3 } { puts "usage:expect $argv0 file host dir" exit } #define var set file [lindex $argv 0] set host [lindex $argv 1] set dir [lindex $argv 2] set password "123456" spawn scp -P22 -rp $file root@$host:$dir expect { "yes/no" {send "yes\r";exp_continue} "*password" {send "$password\r"} } expect eof
2)利用Shell循环执行Expect脚本命令
[root@node1 scripts]# cat 18_13_2.sh #!/bin/sh if [ $# -ne 2 ] then echo $"USAGE:$0 file dir" exit 1 fi file=$1 dir=$2 for n in 128 129 130 do expect 18_13_1.exp $file 192.168.32.$n $dir done