一、expect简介

  expect语言可以用来实现自动的交互式任务,而无需人为干预。在实际工作中,运行某些命令、脚本或程序时,这些命令、脚本或程序都需要从终端输入某些继续运行的指令,比如scp,ssh等,而这些输入都需要人为的手工进行。而利用expect,则可以根据程序的提示,模拟标准输入提供给程序,从而实现自动化交互执行。

二、expect用法

2.1 基本命令

  expect的工作方式是通过spawn命令启动进程,然后expect命令匹配进程的输出字符,执行指定的操作。因此在使用expect时,基本上都是和以下四个命令打交道:

  1. spawn:用来启动进程,spawn后的expect和send命令都是和spawn打开的进程进行交互。
  2. expect:用来捕捉spwan进程的输出,主要包括:标准输入的提示信息,eof和timeout。
  3. send:一般是和expect命令配合使用,向spawn启动的进程发送字符串。:字符串如果包含回车键,使用\r
  4. interact:主要用于退出自动化,进入人工交互。比如ssh登录服务器后希望停留在目的服务器,以便手动的执行后续命令。

2.2 expect-send语法

  expect-send对采用了tcl的模式-动作语法,此语法目前有两种使用方式:

  • 单一分支语法

    1
    expect "hello" {send "world"}    # 匹配到spawn进程输出包含"hello"字符串后,发送"world"
  • 多分支语法

    1
    2
    3
    4
    5
    expect {
    "hello" {send "world"; exp_continue} # exp_continue表示循环式匹配,通常匹配之后都会退出语句,但如果有exp_continue则可以不断循环匹配,输入多条命令,简化写法。
    "world" {send "hello"; exp_continue}
    "you" {send "she"}
    }

  当spawn进程的输出中包含"hello"时,发送"world",同时循环此多分支语句;当输出中包含"world"时,发送"hello",同时循环此多分支语句;当输出中包含"you"时,发送"she"。:多分支语法的匹配是顺序的。

2.3 命令行传参

  expect支持命令行传参的方式,$argc表示参数个数,而参数值存放在$argv中,比如取第一个参数就是[lindex $argv 0],以此类推。例如:./test.exp hello world:

1
2
3
4
5
6
7
#!/usr/bin/expect
if {$argc != 2} {
puts "Usage:cmd <ip> <password>"
exit 1
}
set name1 [lindex $argv 0] # 获取第1个参数:hello,赋值给变量name1
set name2 [lindex $argv 1] # 获取第2个参数:world,赋值给变量name2

2.4 expect执行shell命令

  在expect里面可以执行shell命令,并且如果想在shell里面使用expect的变量,需要在expect里面先设置环境变量,比如:

1
2
3
4
5
6
7
8
9
10
[root@test ~]# cat hello.exp 
#!/usr/bin/expect
# 设置变量
set ::env(flag) 1
exec sh -c {
echo $flag > /tmp/hello.txt
}
[root@test ~]# ./hello.exp
[root@test ~]# cat /tmp/hello.txt
1

2.5 语法示例

  举个简单的例子介绍基本语法:

1
2
3
4
5
6
7
#!/usr/bin/expect
set timeout 30 # 设置脚本的超时时间,单位:秒,-1永不超时
spawn ssh root@192.168.1.1 # 启动ssh连接进程
expect "password:" # 匹配进程的输出包含"paswwword:"
send "123456\r" # 发送"123456"和回车键
expect eof # 退出和spawn进程的交互
interact # 执行完成后保持交互状态,把控制权交给控制台。如果没有这一句登录完成后会退出,而不是留在远程终端上。

三、示例

  下面介绍一个示例,作用是ssh登录远程服务器去修改用户密码:

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
45
46
47
48
49
50
#!/usr/bin/expect
# 退出码为0:远程服务器密码修改成功
# 退出码为1:传参个数不符合要求
# 退出码为2:输入ip为本机
# 退出码为3:远程端口无法连接
# 退出码为4:连接超时,可能ip输入有误
# 退出码为5:密码错误

set timeout 5
set user [lindex $argv 0]
set old_passwd [lindex $argv 1]
set ::env(ip) [lindex $argv 2]
set port [lindex $argv 3]
set new_passwd [lindex $argv 4]

# 判断传参
if { $argc!=5 } {
send_user "USAGE: $argv0 user old_passwd ip port new_passwd\n"
exit 1
}

# 判断ip地址是否为本机,本机则退出脚本
set ip_status [ exec sh -c { ip a | grep $ip | wc -l } ]
if { $ip_status == 1 } {
exit 2
}

# 远程连接服务器
set flag 0
spawn ssh -p $port ${user}@${ip}
expect {
"(yes/no)" {send "yes\r";exp_continue}
"password" {send "$old_passwd\r";exp_continue}
"Connection refused" {exit 3}
"Last login:" {set flag 1}
timeout {exit 4}
eof {exit 5}
}

# flag为1,代表登录成功
if { $flag == 1 } {
send "export LANG=en_US.UTF-8\r"
send "passwd\r"
expect {
"password:" {send "$new_passwd\r";exp_continue}
}
send "exit\r"
}

expect eof

  上面的脚本对部分异常进行了处理,如果在使用过程中遇到了其他的情况,可以评论下,一起优化下代码。