十六、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         设置超时时间

三、简单实现expect

实验环境
node1: 192.168.32.211
node2: 192.168.32.212

1、在node1使用expect实现自动修改ywx用户的密码
2、在node1上远程执行node2的uptime命令

1、在node1使用expect实现自动修改ywx用户的密码

#! /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]# 

2、在node1上远程执行node2的uptime命令

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

四、expect脚本说明

1)在Expect自动交互程序执行的过程中,spawn命令是一开始就需要使用的命令,通过spawn执行一个命令或程序,之后所有的Expect操作都会在这个执行过的命令或程序进程中进行,包括自动交互功能,因此如果没有spawn命令,Expect程序将会无法实现自动交互。

2)在Expect自动交互程序的执行过程中,当使用spawn命令执行一个命令或程序之后,会提示某些交互式信息,expect命令的作用就是获取spawn命令执行后的信息,看看是否和其事先指定的相匹配,一旦匹配上指定的内容就执行expect后面的动作,expect命令也有一些选项,相对用得较多的是-re,表示使用正则表达式的方式来匹配。

1、把expect与send放在一行

 
#!/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结束。

2、expect和send放在不同行

#!/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,最后一个匹配的字符串除外。

4、利用expect响应Shell脚本中的多个read读入

[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.

开发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

执行结果如下:

[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

5、send

在上面的案例中,我们已经看到了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

send命令可用的参数

 
·-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命令

send_user命令可用来打印Expect脚本信息,类似Shell里的echo命令,而默认的send、exp_send命令都是将字符串输出到Expect程序中去,有关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

8、exit命令

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常用命令总结

六、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

2、expect特殊参数变量

在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关键字前后都要有空格,{条件表达式}大括号里面靠近大括号处可以没有空格,将指令括起来的起始大括号“{”前要有空格。

2、案例

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 #<==这是脚本后面的三个参数。

2、使用if语句判断脚本传参的个数,不管是否符合都给予提示

 
#!/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.

八、Expect中的关键字

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

2、timeout关键字

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.

上面的处理中,首先将timeout变量设置为30秒,此时Expect脚本的执行只要超过了30秒,就会直接执行结尾的timeout动作,打印一个信息,停止运行脚本。

在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.

timeout变量设置为0,表示立即超时,为-1则表示永不超时。

九、企业生产场景下的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:
 142053 up 1626,  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

2、利用Shell循环执行Expect脚本命令

#!/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

 

 

 

posted @ 2020-10-20 14:43  yaowx  阅读(1147)  评论(0编辑  收藏  举报