expect是linux中的一个用来处理交互的命令。借助Expect,我们可以将交互过程写在一个脚本上,使之自动化完成。形象的说,ssh登录,ftp登录等都符合交互的定义。下文我们首先提出一个问题,然后介绍基础知四个命令
Expect详解
expect命令简单说就是通过其内置的各种命令实现在交互式软件中自动交互的工具。
用在ssh时,可以配合spawn命令实现ssh的自动登录。又因为可以在脚本中编写判断,赋值等逻辑,具有很高的灵活性。
问题
如何从机器A上ssh到机器B上,然后执行机器B上的命令?如何使之自动化完成?
四个命令
Expect中最关键的四个命令是send,expect,spawn,interact。
send:用于向进程发送字符串
expect:从进程接收字符串
spawn:启动新的进程
interact:允许用户交互
1. send命令
send命令接收一个字符串参数,并将该参数发送到进程。
expect1.1> send "hello world\n"
hello world
2. expect命令
(1)基础知识
expect命令和send命令正好相反,expect通常是用来等待一个进程的反馈。expect可以接收一个字符串参数,也可以接收正则表达式参数。和上文的send命令结合,现在我们可以看一个最简单的交互式的例子:
expect "hi\n"
send "hello there!\n"
这两行代码的意思是:从标准输入中等到hi和换行键后,向标准输出输出hello there。
tips: $expect_out(buffer)存储了所有对expect的输入,<$expect_out(0,string)>存储了匹配到expect参数的输入。
比如如下程序:
expect "hi\n"
send "you typed <$expect_out(buffer)>"
send "but I only expected <$expect_out(0,string)>"
当在标准输入中输入
test
hi
是,运行结果如下
you typed: test
hi
I only expect: hi
(2)模式-动作
expect最常用的语法是来自tcl语言的模式-动作。这种语法极其灵活,下面我们就各种语法分别说明。
单一分支模式语法:
expect "hi" {send "You said hi"}
匹配到hi后,会输出"you said hi"
多分支模式语法:
expect "hi" { send "You said hi\n" } \
"hello" { send "Hello yourself\n" } \
"bye" { send "That was unexpected\n" }
匹配到hi,hello,bye任意一个字符串时,执行相应的输出。等同于如下写法:
expect {
"hi" { send "You said hi\n"}
"hello" { send "Hello yourself\n"}
"bye" { send "That was unexpected\n"}
}
3. spawn命令
上文的所有demo都是和标准输入输出进行交互,但是我们跟希望他可以和某一个进程进行交互。spawm命令就是用来启动新的进程的。spawn后
的send和expect命令都是和spawn打开的进程进行交互的。结合上文的send和expect命令我们可以看一下更复杂的程序段了。
set timeout -1
spawn ftp ftp.test.com //打开新的进程,该进程用户连接远程ftp服务器
expect "Name" //进程返回Name时
send "user\r" //向进程输入anonymous\r
expect "Password:" //进程返回Password:时
send "123456\r" //向进程输入don@libes.com\r
expect "ftp> " //进程返回ftp>时
send "binary\r" //向进程输入binary\r
expect "ftp> " //进程返回ftp>时
send "get test.tar.gz\r" //向进程输入get test.tar.gz\r
这段代码的作用是登录到ftp服务器ftp ftp.uu.net上,并以二进制的方式下载服务器上的文件test.tar.gz。程序中有详细的注释。
4.interact
到现在为止,我们已经可以结合spawn、expect、send自动化的完成很多任务了。但是,如何让人在适当的时候干预这个过程了。比如下载完
ftp文件时,仍然可以停留在ftp命令行状态,以便手动的执行后续命令。interact可以达到这些目的。下面的demo在自动登录ftp后,允许用
户交互。
spawn ftp ftp.test.com
expect "Name"
send "user\r"
expect "Password:"
send "123456\r"
interact
解决方法
上文中提到:
如何从机器A上ssh到机器B上,然后执行机器B上的命令?如何使之自动化完成?
下面一段脚本实现了从机器A登录到机器B,然后执行机器B上的pwd命令,并停留在B机器上,等待用户交互。具体含义请参考上文。
#!/home/tools/bin/64/expect -f
set timeout -1
spawn ssh $BUser@$BHost
expect "*password:" { send "$password\r" }
expect "$*" { send "pwd\r" }
interact
实例
前言
前段时间将工作用的机器换成了 Ubuntu + 128G SSD +
8G内存的台式机,终于走上了用linux办公的道路,总的说来,Ubuntu用起来还是比我想象的顺畅得多,其“apt-get式”的包管理方式,安装
软件非常方便,apt-get install xxx 即可安装。唯一的缺点就是银行的“U盾”等不支持linux,所以,使用网银时略有不便。
言归正传,在换台式后公司还做了一件事就是统一了公司内的测试环境,将所有的测试环境机器统一到机房内用虚拟机的方式统一管理(皆大欢喜)。为了以后开发
测试方便,项目组申请了多套测试环境,比如:开发环境:用于开发人员每次迭代自测时的环境。测试环境:稳定的和线上代码同步的测试环境,通常用于演示或者
对测试环境其他应用提供服务。除此以外,还有一套性能测试环境。这么多环境的问题就导致,每次需要更新环境:update 代码,重启tomcat时都需要ssh到某台机器上。这其实是一个浪费时间的重复性劳动,通常情况,这种事情都可以使用代码解决。这就是我们今天的主角:expect。
demo实例如下:
#!/usr/bin/expect -f ##声明使用哪种解析器解析该脚本,如果没有安装expect的话,使用apt-get install expect安装
if { [llength $argv] < 1 } { ##$argv为执行该脚本是的参数数组,判断长度,以决定是否继续
puts "Usage: $argv0 need ssh ip" //expect中的echo,System.out.println()
exit 1
}
set envs [lindex $argv 0] ##将数组中的第一个参数赋值给 envs
set timeout 30 //设置等待终端响应的超时时间为30秒
if { $envs == 61 } { ##注意 if和{间有个空格,{和$envs间有个空格,等号前后有空格,61后有空格,}{间有空格!!!
set ips xxx.xx.xxx.61
}
if { $envs == 141 } {
set ips xxx.xx.xxx.141
}
if { $envs == 136 } {
set ips xxx.xx.xxx.136
}
spawn ssh root@$ips ##执行ssh命令实现登陆
expect {
##第一次登陆的时候,会询问是否有在本地保存该密钥,仔细的你应该发现,这里的匹配是用正则的,简答说,只匹配connecting也是可以的。
"Are you sure you want to continue connecting (yes/no)?" {send "yes\r"}
"password:" {send "cdyanfa\r"}##保存过一次后,就会直接返回需要密码的阶段了。使用send 命令发送密码。
}
interact ##是Expect用来打开用户与产生进程之间通信的命令,简单说就是登陆以后将远程服务器的终端保持在当前终端,而不是将远程终端关掉。
短短几行命令就将每天重复N次的操作简化成一行了,对于提高效率很有用。
1. expect 是基于tcl 演变而来的,所以很多语法和tcl 类似,基本的语法如下
所示:
1.1 首行加上/usr/bin/expect
1.2 spawn: 后面加上需要执行的shell 命令,比如说spawn sudo touch testfile
1.3 expect: 只有spawn 执行的命令结果才会被expect 捕捉到,因为spawn 会启
动一个进程,只有这个进程的相关信息才会被捕捉到,主要包括:标准输入的提
示信息,eof 和timeout。
1.4 send 和send_user:send 会将expect 脚本中需要的信息发送给spawn 启动
的那个进程,而send_user 只是回显用户发出的信息,类似于shell 中的echo 而
已。
2. 一个小例子,用于linux 下账户的建立:
filename: account.sh,可以使用./account.sh newaccout 来执行;
1 #!/usr/bin/expect
2
3 set passwd "mypasswd"
4 set timeout 60
5
6 if {$argc != 1} {
7 send "usage ./account.sh \$newaccount\n"
8 exit
9 }
10
11 set user [lindex $argv [expr $argc-1]]
12
13 spawn sudo useradd -s /bin/bash -g mygroup -m $user
14
15 expect {
16 "assword" {
17 send_user "sudo now\n"
18 send "$passwd\n"
19 exp_continue
20 }
21 eof
22 {
23 send_user "eof\n"
24 }
25 }
26
27 spawn sudo passwd $user
28 expect {
29 "assword" {
30 send "$passwd\n"
31 exp_continue
32 }
33 eof
34 {
35 send_user "eof"
36 }
37 }
38
39 spawn sudo smbpasswd -a $user
40 expect {
41 "assword" {
42 send "$passwd\n"
43 exp_continue
44 }
45 eof
46 {
47 send_user "eof"
48 }
49 }
3. 注意点:
第3 行: 对变量赋值的方法;
第4 行: 默认情况下,timeout 是10 秒;
第6 行: 参数的数目可以用$argc 得到;
第11 行:参数存在$argv 当中,比如取第一个参数就是[lindex $argv 0];并且
如果需要计算的话必须用expr,如计算2-1,则必须用[expr 2-1];
第13 行:用spawn 来执行一条shell 命令,shell 命令根据具体情况可自行调整;
有文章说sudo 要加-S,经过实际测试,无需加-S 亦可;
第15 行:一般情况下,如果连续做两个expect,那么实际上是串行执行的,用。expect 与“{ ”之间直接必须有空格或则TAB间隔,否则会出麻烦,会报错invalid command name "expect{"
例子中的结构则是并行执行的,主要是看匹配到了哪一个;在这个例子中,如果
你写成串行的话,即
expect "assword"
send "$passwd\n"
expect eof
send_user "eof"
那么第一次将会正确运行,因为第一次sudo 时需要密码;但是第二次运行时由于
密码已经输过(默认情况下sudo 密码再次输入时间为5 分钟),则不会提示用户
去输入,所以第一个expect 将无法匹配到assword,而且必须注意的是如果是
spawn 命令出现交互式提问的但是expect 匹配不上的话,那么程序会按照timeout
的设置进行等待;可是如果spawn 直接发出了eof 也就是本例的情况,那么expect
"assword"将不会等待,而直接去执行expect eof。
这时就会报expect: spawn id exp6 not open,因为没有spawn 在执行,后面的
expect 脚本也将会因为这个原因而不再执行;所以对于类似sudo 这种命令分支
不定的情况,最好是使用并行的方式进行处理;
第17 行:仅仅是一个用户提示而已,可以删除;
第18 行:向spawn 进程发送password;
第19 行:使得spawn 进程在匹配到一个后再去匹配接下来的交互提示;
第21 行:eof 是必须去匹配的,在spawn 进程结束后会向expect 发送eof;如果
不去匹配,有时也能运行,比如sleep 多少秒后再去spawn 下一个命令,但是不
要依赖这种行为,很有可能今天还可以,明天就不能用了;
4. 其他
下面这个例子比较特殊,在整个过程中就不能expect eof 了:
1 #!/usr/bin/expect
2
3 set timeout 30
4 spawn ssh 10.192.224.224
5 expect "password:"
6 send "mypassword\n"
7 expect "*$"
8 send "mkdir tmpdir\n" #远程执行命令用send发送,不用spawn
9 expect "*$" #注意这个地方,要与操作系统上环境变量PS1相匹配,尤其是有PS1有空格的情况下,一定在expct "*$ "把空格加上,加不上你就完蛋了。我试过。
这个例子实际上是通过ssh 去登录远程机器,并且在远程机器上创佳一个目录,
我们看到在我们输入密码后并没有去expect eof,这是因为ssh 这个spawn 并没
有结束,而且手动操作时ssh 实际上也不会自己结束除非你exit;所以你只能
expect bash 的提示符,当然也可以是机器名等,这样才可以在远程创建一个目
录。
注意,请不要用spawn mkdir tmpdir,这样会使得上一个spawn 即ssh 结束,那
么你的tmpdir 将在本机建立。
当然实际情况下可能会要你确认ssh key,可以通过并行的expect 进行处理,不
多赘述。
5. 觉得bash 很多情况下已经很强大,所以可能用expect 只需要掌握这些就好了,
其他的如果用到可以再去google 了。
源代码图片:
6 \实例:下面这个脚本是完成对单个服务器scp任务。
1: #!/usr/bin/expect
2:
3: set timeout 10
4: set host [lindex $argv 0]
5: set username [lindex $argv 1]
6: set password [lindex $argv 2]
7: set src_file [lindex $argv 3]
8: set dest_file [lindex $argv 4]
9:
10: spawn scp $src_file $username@$host:$dest_file
11: expect {
12: "(yes/no)?"
13: {
14: send "yes\n"
15: expect "*assword:" { send "$password\n"}
16: }
17: "*assword:"
18: {
19: send "$password\n"
20: }
21: }
22: expect "100%"
23: expect eof
参考源代码图片:
注意代码刚开始的第一行,指定了expect的路径,与shell脚本相同,这一句指定了程序在执行时到哪里去寻找相应的启动程序。代码刚开始还设定了timeout的时间为10秒,如果在执行scp任务时遇到了代码中没有指定的异常,则在等待10秒后该脚本的执行会自动终止。
spawn代表在本地终端执行的语句,在该语句开始执行后,expect开始捕获终端的输出信息,然后做出对应的操作。expect代码中的捕获的(yes/no)内容用于完成第一次访问目标主机时保存密钥的操作。有了这一句,scp的任务减少了中断的情况。代码结尾的expect eof与spawn对应,表示捕获终端输出信息的终止。
有了这段expect的代码,还只能完成对单个远程主机的scp任务。如果需要实现批量scp的任务,则需要再写一个shell脚本来调用这个expect脚本。
1: #!/bin/sh
2:
3: list_file=$1
4: src_file=$2
5: dest_file=$3
6:
7: cat $list_file | while read line
8: do
9: host_ip=`echo $line | awk '{print $1}'`
10: username=`echo $line | awk '{print $2}'`
11: password=`echo $line | awk '{print $3}'`
12: echo "$host_ip"
13: ./expect_scp $host_ip $username $password $src_file $dest_file
15: done
参考代码图片如下:
很简单的代码,指定了3个参数:列表文件的位置、本地源文件路径、远程主机目标文件路径。需要说明的是其中的列表文件指定了远程主机ip、用户名、密码,这些信息需要写成以下的格式:
IP username password
中间用空格或tab键来分隔,多台主机的信息需要写多行内容。
这样就指定了两台远程主机的信息。注意,如果远程主机密码中有“$”、“#”这类特殊字符的话,在编写列表文件时就需要在这些特殊字符前加上转义字符,否则expect在执行时会输入错误的密码。
对于这个shell脚本,保存为batch_scp.sh文件,与刚才保存的expect_scp文件和列表文件(就定义为hosts.list文件吧)放到同一目录下,执行时按照以下方式输入命令就可以了:
转载请注明:学时网 » linux expect命令详解