Expect工作原理
从最简单的层次来说,Expect的工作方式象一个通用化的Chat脚本工具。Chat脚本最早用于UUCP网络内,以用来实现计算机之间需要建立连接时进行特定的登录会话的自动化。
Chat脚本由一系列expect-send对组成:expect等待输出中输出特定的字符,通常是一个提示符,然后发送特定的响应。例如下面的Chat 脚本实现等待标准输出出现Login:字符串,然后发送somebody作为用户名;然后等待Password:提示符,并发出响应sillyme。
Login: somebody Password: sillyme
这个脚本用来实现一个登录过程,并用特定的用户名和密码实现登录。
Expect最简单的脚本操作模式本质上和Chat脚本工作模式是一样的。下面我们分析一个响应chsh命令的脚本。我们首先回顾一下这个交互命令的格式。假设我们为用户chavez改变登录脚本,命令交互过程如下:
# chsh chavez
Changing the login shell for chavez
Enter the new value, or press return for the default
Login Shell [/bin/bash]: /bin/tcsh
#
可以看到该命令首先输出若干行提示信息并且提示输入用户新的登录shell。我们必须在提示信息后面输入用户的登录shell或者直接回车不修改登录shell。
下面是一个能用来实现自动执行该命令的Expect脚本:
#!/usr/bin/expect
# Change a login shell to tcsh
set user [lindex $argv 0]
spawn chsh $user
expect "]:"
send "/bin/tcsh "
expect eof
exit
这个简单的脚本可以解释很多Expect程序的特性。和其他脚本一样首行指定用来执行该脚本的命令程序,这里是/usr/bin/expect。程序第一行用来获得脚本的执行参数(其保存在数组$argv中,从0号开始是参数),并将其保存到变量user中。
第二个参数使用Expect的spawn命令来启动脚本和命令的会话,这里启动的是chsh命令,实际上命令是以衍生子进程的方式来运行的。
随后的expect和send命令用来实现交互过程。脚本首先等待输出中出现]:字符串,一旦在输出中出现chsh输出到的特征字符串(一般特征字符串往往是等待输入的最后的提示符的特征信息)。对于其他不匹配的信息则会完全忽略。当脚本得到特征字符串时,expect将发送/bin/tcsh和一个回车符给chsh命令。最后脚本等待命令退出(chsh结束),一旦接收到标识子进程已经结束的eof字符,expect脚本也就退出结束。
决定如何响应
管理员往往有这样的需求,希望根据当前的具体情况来以不同的方式对一个命令进行响应。我们可以通过后面的例子看到expect可以实现非常复杂的条件响应,而仅仅通过简单的修改预处理脚本就可以实现。下面的例子是一个更复杂的expect-send例子:
expect -re "\[(.*)]:"
if {$expect_out(1,string)!="/bin/tcsh"} {
send "/bin/tcsh" }
send " "
expect eof
在这个例子中,第一个expect命令现在使用了-re参数,这个参数表示指定的的字符串是一个正则表达式,而不是一个普通的字符串。对于上面这个例子里是查找一个左方括号字符(其必须进行三次逃逸(escape),因此有三个符号,因为它对于expect和正则表达时来说都是特殊字符)后面跟有零个或多个字符,最后是一个右方括号字符。这里.*表示表示一个或多个任意字符,将其存放在()中是因为将匹配结果存放在一个变量中以实现随后的对匹配结果的访问。
当发现一个匹配则检查包含在[]中的字符串,查看是否为/bin/tcsh。如果不是则发送/bin/tcsh给chsh命令作为输入,如果是则仅仅发送一个回车符。这个简单的针对具体情况发出不同相响应的小例子说明了expect的强大功能。
在一个正则表达时中,可以在()中包含若干个部分并通过expect_out数组访问它们。各个部分在表达式中从左到右进行编码,从1开始(0包含有整个匹配输出)。()可能会出现嵌套情况,这这种情况下编码从最内层到最外层来进行的。
使用超时
下一个expect例子中将阐述具有超时功能的提示符函数。这个脚本提示用户输入,如果在给定的时间内没有输入,则会超时并返回一个默认的响应。这个脚本接收三个参数:提示符字串,默认响应和超时时间(秒)。
#!/usr/bin/expect
# Prompt function with timeout and default.
set prompt [lindex $argv 0]
set def [lindex $argv 1]
set response $def
set tout [lindex $argv 2]
脚本的第一部分首先是得到运行参数并将其保存到内部变量中。
send_tty "$prompt: "
set timeout $tout
expect " " {
set raw $expect_out(buffer)
# remove final carriage return
set response [string trimright "$raw" " "]
}
if {"$response" == "} {set response $def}
send "$response "
# Prompt function with timeout and default.
set prompt [lindex $argv 0]
set def [lindex $argv 1]
set response $def
set tout [lindex $argv 2]
这是脚本其余的内容。可以看到send_tty命令用来实现在终端上显示提示符字串和一个冒号及空格。set timeout命令设置后面所有的expect命令的等待响应的超时时间为$tout(-l参数用来关闭任何超时设置)。
然后expect命令就等待输出中出现回车字符。如果在超时之前得到回车符,那么set命令就会将用户输入的内容赋值给变脸raw。随后的命令将用户输入内容最后的回车符号去除以后赋值给变量response。
然后,如果response中内容为空则将response值置为默认值(如果用户在超时以后没有输入或者用户仅仅输入了回车符)。最后send命令将response变量的值加上回车符发送给标准输出。
一个有趣的事情是该脚本没有使用spawn命令。 该expect脚本会与任何调用该脚本的进程交互。
如果该脚本名为prompt,那么它可以用在任何C风格的shell中。
% set a='prompt "Enter an answer" silence 10'
Enter an answer: test
% echo Answer was "$a"
Answer was test
prompt设定的超时为10秒。如果超时或者用户仅仅输入了回车符号,echo命令将输出
Answer was "silence"
一个更复杂的例子
下面我们将讨论一个更加复杂的expect脚本例子,这个脚本使用了一些更复杂的控制结构和很多复杂的交互过程。这个例子用来实现发送write命令给任意的用户,发送的消息来自于一个文件或者来自于键盘输入。
#!/usr/bin/expect
# Write to multiple users from a prepared file
# or a message input interactively
if {$argc<2} {
send_user "usage: $argv0 file user1 user2 ... "
exit
}
send_user命令用来显示使用帮助信息到父进程(一般为用户的shell)的标准输出。
set nofile 0
# get filename via the Tcl lindex function
set file [lindex $argv 0]
if {$file=="i"} {
set nofile 1
} else {
# make sure message file exists
if {[file isfile $file]!=1} {
send_user "$argv0: file $file not found. "
exit }}
这部分实现处理脚本启动参数,其必须是一个储存要发送的消息的文件名或表示使用交互输入得到发送消的内容的"i"命令。
变量file被设置为脚本的第一个参数的值,是通过一个Tcl函数lindex来实现的,该函数从列表/数组得到一个特定的元素。[]用来实现将函数lindex的返回值作为set命令的参数。
如果脚本的第一个参数是小写的"i",那么变量nofile被设置为1,否则通过调用Tcl的函数isfile来验证参数指定的文件存在,如果不存在就报错退出。
可以看到这里使用了if命令来实现逻辑判断功能。该命令后面直接跟判断条件,并且执行在判断条件后的{}内的命令。if条件为false时则运行else后的程序块。
set procs {}
# start write processes
for {set i 1} {$i<$argc}
{incr i} {
spawn -noecho write
[lindex $argv $i]
lappend procs $spawn_id
}
最后一部分使用spawn命令来启动write进程实现向用户发送消息。这里使用了for命令来实现循环控制功能,循环变量首先设置为1,然后因此递增。循环体是最后的{}的内容。这里我们是用脚本的第二个和随后的参数来spawn一个write命令,并将每个参数作为发送消息的用户名。lappend命令使用保存每个spawn的进程的进程ID号的内部变量$spawn_id在变量procs中构造了一个进程ID号列表。
if {$nofile==0} {
setmesg [open "$file" "r"]
} else {
send_user "enter message,
ending with ^D: " }
最后脚本根据变量nofile的值实现打开消息文件或者提示用户输入要发送的消息。
set timeout -1
while 1 {
if {$nofile==0} {
if {[gets $mesg chars] == -1} break
set line "$chars "
} else {
expect_user {
-re " " {}
eof break }
set line $expect_out(buffer) }
foreach spawn_id $procs {
send $line }
sleep 1}
exit
上面这段代码说明了实际的消息文本是如何通过无限循环while被发送的。while循环中的 if判断消息是如何得到的。在非交互模式下,下一行内容从消息文件中读出,当文件内容结束时while循环也就结束了。(break命令实现终止循环) 。
在交互模式下,expect_user命令从用户接收消息,当用户输入ctrl+D时结束输入,循环同时结束。 两种情况下变量$line都被用来保存下一行消息内容。当是消息文件时,回车会被附加到消息的尾部。
foreach循环遍历spawn的所有进程,这些进程的ID号都保存在列表变量$procs中,实现分别和各个进程通信。send命令组成了 foreach的循环体,发送一行消息到当前的write进程。while循环的最后是一个sleep命令,主要是用于处理非交互模式情况下,以确保消息不会太快的发送给各个write进程。当while循环退出时,expect脚本结束。
参考资源
Expect软件版本深带有很多例子脚本,不但可以用于学习和理解expect脚本,而且是非常使用的工具。一般可以在 /usr/doc/packages/expect/example看到它们,在某些linux发布中有些expect脚本保存在/usr/bin目录下。
Don Libes, Exploring Expect, O'Reilly & Associates, 1995.
John Ousterhout, Tcl and the Tk Toolkit, Addison-Wesley, 1994.
一些有用的expect脚本
autoexpect:这个脚本将根据自身在运行时用户的操作而生成一个expect脚本。它的功能某种程度上类似于在Emacs编辑器的键盘宏工具。一个自动创建的脚本可能是创建自己定制脚本的好的开始。
kibitz:这是一个非常有用的工具。通过它两个或更多的用户可以连接到同一个shell进程。可以用于技术支持或者培训(参见下图)。
同样可以用于其他一些要求同步的协同任务。例如我希望和另外一个同事一起编辑一封信件,这样通过kibitz我们可以共享同一个运行编辑器的脚本,同时进行编辑和查看信件内容。
tkpasswd: 这个脚本提供了修改用户密码的GUI工具,包括可以检查密码是否是基于字典模式。这个工具同时是一个学习expect和tk的好实例。
==================================================
Expect 超出预期
[From] http://www-128.ibm.com/developerworks/cn/linux/server/clinic/part1/index.htmlCameron Laird 用一篇对受欢迎的 Expect 工具的概述开启了他新的月度专栏,Expect 是一种功能大大超出大多数程序员和管理员认识的语言。Expect 非常适合保持服务器正常运转所需的通用工作,实际上,它可以作为一种(几乎)通用的编程语言。通过单击本文顶部或底部的 讨论在 论坛中将您对本文的想法与作者和其他读者一起分享。
您是一名“系统程序员”― 您编写代码以保持服务器正常运转,并且为您的应用程序开发人员同事提供所需的底层功能。您从哪里获取所需的信息呢?大多数编程参考大全关心客户机或者“应用程序”问题,而管理书籍通常回避编程而致力于“配置”。
我希望您会发现这一新的“服务器诊所”专栏是有用的来源之一。每个月,我都将解决在服务器的“维护与支持”中遇到的一个编程问题或一类共同问题。
专栏第一部分将 Expect 作为您最应该了解的一种语言进行介绍。您可能已经熟悉 Expect 了。不过,您也可能从未见过 Expect 所管理任务的完整范围。Expect 实现了一种 Linux 系统编程的通用性,其它语言 ― 即使是 C、Java 或 bash ― 都无法与之相比。虽然未来的专栏文章将展示使用各种语言的解决方案,但 Expect 很可能是出现频率最高的一个。
什么使 Expect“通用”呢?首先,应了解 Expect 是 Tcl/Tk 编程语言的适当超集。Tcl 是在各种程序中使用的一种高级语言。它过去通常与 Perl、Python、Ruby 和其它语言一起被归为“脚本编制”语言。在 2002 年,最明智的做法是抛开某些历史事件,简单地将所有这些语言视为高效率的开放源码语言。Tcl 在计算机辅助设计(CAD)领域中特别流行,象 Cisco 和 Nortel 这样的联网设备供应商也都使用它。与其它“脚本编制”语言一样,Tcl 的内置功能适用于文本处理、数据库管理、联网和算法等领域中的最常见问题。
Tcl 是 Expect 的基础。任何 Tcl 程序都自动是 Expect 程序。因为有下面两个原因,所以强调这一点很重要:
- 许多人只知道 Expect 是一种“工具”,而从不了解它是一种完全成熟的编程语言。
- 1994 年,许多真正认识到 Expect 的通用能力的程序员都被它迷住了。
Expect 的作者是(美国)国家标准与技术协会(National Institute of Standards and Technology)的 Don Libes。他在 1994 年出版了一本关于 Expect 的出色书籍。该书现在仍只有第一版,它没有竞争者;这本书写得太好了,以至于没有出版商出版另一本书。最引人注目的是, Exploring Expect(请参阅本文后面的 参考资料)一直不需要更新。它的清晰和精确很好地经受了时间的考验。
这里的问题是,过去八年以来,Expect 的底层 Tcl 基础已经有了极大发展。最初编写 Expect 时,Tcl 并不追求成为通用的编程语言。从那时起,Tcl 已经:
- 知道如何处理完整的八位数据,甚至能方便地处理 Unicode;
- 添加了方便的 TCP/IP 抽象;
- 获取了数据和时间计算以及格式化方面的能力;
- 改进并合理化了其字符串处理;
因此,请记住:如果 Perl、Java 或 C 可以解决一个问题,那么 Tcl 以及 Expect 很可能也可以解决。
Tcl 有一项任何其它编程语言都“无与伦比(out of the box)”的工作,这就是图形用户界面(GUI)的构建。虽然从 ActiveState Tools Corporation 下载的 Linux 版标准 ActiveTcl 二进制分发版只有大约 10 兆字节,但它不仅包含 Expect,而且还包含功能齐全的集成 GUI 工具箱。下面的示例将说明这个名为“Tk”的工具箱如何简洁地表达 GUI 解决方案。
|
Expect 的 Tcl/Tk 基础适用于范围非常广的编程。请记住,Expect 可以完成 Tcl/Tk 所能做的一切。除此之外,Expect 添加了三大类别的附加功能:
- 扩展的调试选项
- 描述面向字符对话框的便利命令
- 棘手的面向字符终端的独一无二的管理
这些功能中第一个是常规的。Expect 有各种“开关”来记录或报告其操作的各个方面。
Expect 的用途是使面向字符的交互自动化。您可能已经自己完成了许多这种工作。每次编写命令行管道或重定向输入/输出(I/O)流时,您都在让计算机管理这些工作,否则您必须自己输入。
Expect 以两种方式深化了这一控制:首先,它提供了表达对话框复杂程度的语言。Expect 不只使用固定“脚本”作为应用程序的输入,而是使交互的每个击键都可编程。
如 Libes 所说,更关键的是:“最终,Expect 是为处理蹩脚的界面而设计的工具。”特别是 Expect 具有管理抵制 I/O 重定向的应用程序的能力。典型示例是命令行 passwd 程序。每个负责管理服务器的人员迟早都需要使密码更新自动化。第一次尝试可能是作为 root 用户运行类似下面的代码:
失败的 passwd 自动化
|
正如每个尝试它的人很快会发现,这根本不起作用。shell 的 < 和 << 重定向对于象 passwd 这样的程序不起作用。
但是,Expect 可以使重定向起作用。Expect 知道如何与所有面向字符的应用程序对话,即使是象 passwd 那样操纵终端设置的应用程序。
正是这一点完善了 Expect 的通用性。原则上,其它语言或库可以提供终端特征的信息。例如,Perl 的 Expect.pm 模块在这方面已经做了很多。虽然经过十多年生产使用,但却没出现其它有力的竞争对手。
这就是您应该学习 Expect 的原因。您将处理带有“蹩脚界面”的程序 ― 您周围有很多这样的程序 ― 而 Expect 通过让它们完成您所需的工作,可以减少几小时甚至几天的开发时间。同时,还可以将 Expect 用于通常由 bash 或 Perl 完成的所有作业。
|
您还应该了解有关 Expect 的其它信息。本专栏的最后部分包括对 Expect 局限的说明、对解决常见问题的 Expect 工作代码的概述以及可以引导您更深入了解 Expect 编程的参考。
Expect 所做的比大多数人所认识到的要多;这就是本专栏的主题。Expect 也有不足之处。系统程序员通常需要使象 FTP 操作、电子邮件发送或处理以及 GUI 测试这样的任务自动化。对于其中的前两项,Expect 无法提供帮助。更准确地说,虽然可以使用 Expect 来使 FTP 和电子邮件自动化(这样做在前几年也很常见),但是现在 Expect 在这些领域方面没有特别优势。其它语言和方法与面向 Expect 的编码功效相同,或者更胜一筹。这个“服务器诊所”专栏的未来部分将说明简便联网自动化的示例。
Expect 的著名用法是用于测试。Expect 是用于几个高端产品(包括 gcc)质量控制中使用的 DejaGnu 系统的基础。然而,虽然 Expect 可用于构建 GUI,并且在几个测试框架中也很关键,但是通常 Expect 在用于 GUI 系统的测试框架中 不起作用。
暂时回到上面提到的 passwd 问题。Expect 对它的展望是什么呢?
要了解 Expect 源代码,目前更简便的做法是忽略安全性考虑事项。下面的程序需要作为 root 用户运行。Expect 提供有用的功能以实现更安全的操作;不过在掌握 Expect 基础知识后更容易理解这些。
您已经知道简单 I/O 重定向对 passwd 不起作用。何种 Expect 程序提供了更好的结果呢?
更新密码的简单 Expect 程序
|
这就是 Expect 用来使程序自动化所需的全部代码,其它语言几乎不可能做到。再多用几行,您可以一次对成百上千用户进行批处理更新。这是一种常见需求;我经常被请去恢复密码文件被严重毁坏的服务器,这里说明了一种开始的方法:
简单迭代
|
|
给 Expect 自动化加上 GUI 外观也只需要多加几行。假定您想为一名非程序员提供方便地更新密码的应用程序。同样忽略安全性考虑事项,这就象完成下列代码一样简单:
简单迭代
|
这个小工作应用程序具有下面的视觉外观:
简单 Expect 密码管理器的抓屏
|
Expect 具有系统程序员通常需要的独特能力。同时,Expect 是出色的通用编程语言,它在联网和 GUI 构造方面具有优势。如果您必须只选择一种用于日常工作的语言,Expect 近乎是理想选择。
请告诉我您如何使用 Expect 以及想让它为您做什么。在以后的几个月,这个“服务器诊所”专栏将回头研究高端版本控制、网络代理和更多的自动化。在那以前,祝您和您的服务器都“身体健康”。
|
Cameron Laird 是 Phaseit, Inc 的一名全职顾问。他经常就开放源码和其它技术主题撰写文章并发表演说。Cameron 感谢 SuSE 的 Reinhard Max 在他准备这个专栏期间所提供的帮助。可以通过 claird@phaseit.net与 Cameron 联系。 | ||








