通常的shell脚本只能将拥有固定输入或输出的应用程序自动化。当遇到passwd这样无法应用管道或输入输出重定向功能的命令时,shell脚本显得无能为力。当面对telnet、ftp这种强依赖与交互输出的程序时,shell脚本能完成的任务更是少的可怜了。
相对于这种需要进行交互式输入输出的场景,expect脚本是常见的一种选择,一来,expect脚本无论从风格还是语法上,还是比较接近shell脚本的。其次expect脚本基于tcl语言,可利用的变成因素也是很丰富完备。
简介
expect不是linux默认安装的工具集,通常需要额外安装。在ubuntu上安装的方法十分简单:
apt-get install expectd
expect依赖与tcl语言,所以apt在安装的过程中会自动帮你安装好tcl程序包,如果是自己手动安装则需要你自己安装好tcl语言环境。
在行为上expect和shell很像。当你执行了expect命令后,实际上进入了expect的执行环境中,在这个环境中你可以使用tcl语言编写你自己的逻辑。
之所以expect可以提供交互式程序的自动化,关键在与spawn命令,expect脚本利用该命令打开一个需要交互的程序,并捕获该程序的所有输入和输出接口,并使用send命令向该程序输入信息,通过expect命令从该程序中捕获输出。
expect脚本基本的思想便是利用expect等待某种特定模式的输出,当该模式被满足时,执行一组操作,提取信息或利用send命令,向被捕获的程序输入内容。仔细比较,该脚本很类似与一个自动聊天机器人,不同的是,聊天的对象是另一个交互式程序。
让你的脚本运行起来
expect脚本有两种运行的模式,一种是作为独立的脚本运行:
文件名:testexpect.exp
#!/usr/bin/expect
spawn telnet 127.0.0.1
expect "*login:*" {
send "username\r"
}
expect "*Password:" {
send "password\r"
}
send "exit\r"
另外一种模式是作为shell脚本的一部分,通过“此处文档”的方式标记expect脚本的起始和结束。
#!/bin/sh
expect > /dev/null <<END
spawn telnet 127.0.0.1
expect "*login:*" {
send "username\r"
}
expect "*Password:" {
send "password\r"
}
send "exit\r"
END
无论哪种方式,主要思想还是一样的。需要注意的是:
- “{”必须位于expect命令同一行上,表明花括弧中的内容同该expect是一体的。
- 在send送出的内容往往需要增加一个\r作为结尾,因为大部分的交互式程序都是需要一个换行符作为输入的结尾的。
- 当使用shell脚本内嵌expect脚本时,请注意”$”需要使用”\”进行转义,否则将被作为shell中的变量被替换。
expect中的常用命令
spawn
通常每个expect脚本中只有一个spawn命令,但是该命令却是整个expect脚本的关键,正是这个命令启动了一个交互式应用程序,并允许expect脚本同这个程序进行交互。
expect命令后面进跟着需要交互的命令,后面的命令中可以包含全局变量,使用$var_name的方式进行引用。
例如
spawn telnet $ip_addr
expect
不同于启动expect脚本时使用的命令,expect是expect脚本中的一个关键字,表明期待特定的输出并执行一个指定的动作。
当使用了spawn命令捕获了一个程序的输入输出之后,就可以用expect命令进行固定输出内容的捕获。需要注意的是,如果spawn命令没有成功捕获一个交互式程序或者该程序已经退出运行,则expect命令将报错。
expect后面进跟着一个表达式,或者使用-re参数指定一个正则表达式。当该模式被命中时,将触发expect命令后面紧跟着的处理命令。
例如,下面的代码表示,当出现“Password:”这样的提示时,输入password+回车。由于一些命令行交互的程序喜欢在提示字符串后面添加空格,所有expect后面的模式往往使用两个*包围起来,以命中可能的白字符。
expect "*Password:*" {
send "password\r"
}
一个expect命令可以想switch语句一样,期待多个结果,分别进行处理:
set timeout 10
expect {
"*login:* { send "user_name\r"; exp_continue }
"*Password:*" { send "password\r" }
timeout { exit 1 }
}
可以使用”;”来区分一行中的多条命令。也可以使用timeout关键字来指定匹配超时对应的动作,使用set timeout 10可设定超时时间为10秒。
send
和expect从应用程序获取输出相对,send命令可以向应用程序发送一个字符串作为输入。
send命令可以利用变量中的值。例如
send “ping $ip_addr\r”
exit
无论在expect脚本的任何位置,你都可以使用exit命令退出expect脚本。并经exit命令后面紧跟的数字作为脚本的返回值。
其他技巧
变量
expect脚本使用tcl语言作为脚本的实现方式,因此expect脚本中的变量,即tcl语言中的变量。
声明一个变量或者是给一个变量赋值的方法为:
set user_name "admin"
这段代码给变量user_name赋了一个值”admin”。如果变量user_name是第一次出现,则创建了一个新的变量,并赋值为”admin”。
引用变量的方式为$var_name,如果是在嵌入在shell脚本中,则需要注意用”\$”对”$”进行转义。
如果在proc中引用全局变量,则需要在proc开始的地方,使用global var_name的方式进行声明。
proc
在tcl中定义过程的方式就是proc了。
proc do_something { var1 var2 } {
global g_var
expect "*something*" {
send "do_xxx $var1 $var2\r"
set g_var "ok"
return 1
}
return 0
}
执行proc的方式和普通的命令是一样的。例如上面的命令可以通过下述方式调用:
do_something $var_name1 $var_name2
return命令可以为proc指定返回值。获取该返回值,需要set命令和 []协作完成。
set ret_val [ do_something $var_name1 $var_name2 ]
expect_out
交互式程序的灵活之处在于根据不同的提示完成不同的操作,提示的内容中往往存在很多变化的内容,如何捕获这些变化的内容并进行处理,则成了处理交互式程序的关键。使用正则表达式和expect_out变量可以很好的完成这个工作。
expect -re "the number (a|b) is (\[0-9\]{1,3})" {
set number_name $expect_out(1,string)
set number_value $expect_out(2,string)
set the_mached_string $expect_out(buffer)
}
关于expect_out变量,帮助手册中有比较详细的说明,此处仅介绍一种比较使用的应用场景。在使用正则表达是的情况下,$expect_out(1,string)可以将命中正则表达式中第一个括号(第一个分组)中的内容取出来;依此类推,$expect_out(2,string)可以取出第二个分组中的值。$expect_out(buffer)可以取出完整的被命中的字符串。
string
string命令提供了一些tcl中简单的字符串处理函数,不再赘述,有需要的同学可以自行搜索相关内容。
控制流
tcl提供了if、for、while作为控制流程的关键字。此处尽介绍if的用法
if { $var == 1 } {
send "string1\r"
} elseif { $var == 2 } {
send "string2\r"
} else {
send "end\r"
}
注意花括弧的位置。而且tcl中条件部分两侧的是花括弧,不是圆括弧。
其他
本文仅仅介绍了expect和tcl语言的极小一部分。如果有想进一步了解的同学,请参展expect的帮助手册和tcl语言的相关教程。