这个脚本退出时用一个数字来表示所发生的情况。0表示passwd程序正常运行,1表示非预期的死亡,2表示锁定,等等。使用数字是为了简单起见。 expect返回字符串和返回数字是一样简单的,即使是派生程序自身产生的消息也是一样的。实际上,典型的做法是把整个交互的过程存到一个文件里面,只有当程序的运行和预期一样的时候才把这个文件删除。否则这个log被留待以后进一步的检查。
这个passwd检查脚本被设计成由别的脚本来驱动。这第二个脚本从一个文件里面读取参数和预期的结果。对于每一个输入参数集,它调用第一个脚本并且把结果和预期的结果相比较。(因为这个任务是非交互的,一个普通的老式shell就可以用来解释第二个脚本)。比如说,一个passwd的数据文件很有可能就象下面一样。
passwd.exp 3 bogus - -
passwd.exp 0 fred abledabl abledabl
passwd.exp 5 fred abcdefghijklm -
passwd.exp 5 fred abc -
passwd.exp 6 fred foobar bar
passwd.exp 4 fred ^C -
第一个域的名字是要被运行的回归脚本。第二个域是需要和结果相匹配的退出值。第三个域就是用户名。第四个域和第五个域就是提示时应该输入的密码。减号仅仅表示那里有一个域,这个域其实绝对不会用到。在第一个行中,bogus表示用户名是非法的,因此passwd会响应说:没有此用户。expect在退出时会返回3,3恰好就是第二个域。在最后一行中,^C就是被切实的送给程序来验证程序是否恰当的退出。
通过这种方法,expect可以用来检验和调试交互式软件,这恰恰是IEEE的POSIX 1003.2(shell和工具)的一致性检验所要求的。进一步的说明请参考Libes[6]。
7.[rogue 和伪终端]
Unix用户肯定对通过管道来和其他进程相联系的方式非常的熟悉(比如说:一个shell管道)。expect使用伪终端来和派生的进程相联系。伪终端提供了终端语义以便程序认为他们正在和真正的终端进行I/O操作。
比如说,BSD的探险游戏rogue在生模式下运行,并假定在连接的另一端是一个可寻址的字符终端。可以用expect编程,使得通过使用用户界面可以玩这个游戏。
rogue这个探险游戏首先提供给你一个有各种物理属性,比如说力量值,的角色。在大部分时间里,力量值都是16,但在几乎每20次里面就会有一个力量值是18。很多的rogue玩家都知道这一点,但没有人愿意启动程序20次以获得一个好的配置。下面的这个脚本就能达到这个目的。
for {} {1} {} {
spawn rogue
expect *Str:18* break
*Str:16*
close
wait
}
interact
第一行是个for循环,和C语言的控制格式很象。rogue启动后,expect就检查看力量值是18还是16,如果是16,程序就通过执行 close和wait来退出。这两个命令的作用分别是关闭和伪终端的连接和等待进程退出。rogue读到一个文件结束符就推出,从而循环继续运行,产生一个新的rogue游戏来检查。
当一个值为18的配置找到后,控制就推出循环并跳到最后一行脚本。interact把控制转移给用户以便他们能够玩这个特定的游戏。
想象一下这个脚本的运行。你所能真正看到的就是20或者30个初始的配置在不到一秒钟的时间里掠过屏幕,最后留给你的就是一个有着很好配置的游戏。唯一比这更好的方法就是使用调试工具来玩游戏。
我们很有必要认识到这样一点:rogue是一个使用光标的图形游戏。expect程序员必须了解到:光标的运动并不一定以一种直观的方式在屏幕上体现。幸运的是,在我们这个例子里,这不是一个问题。将来的对expect的改进可能会包括一个内嵌的能支持字符图形区域的终端模拟器。
8.[ftp]
我们使用expect写第一个脚本并没有打印出Hello,World。实际上,它实现了一些更有用的功能。它能通过非交互的方式来运行ftp。ftp是用来在支持TCP/IP的网络上进行文件传输的程序。除了一些简单的功能,一般的实现都要求用户的参与。
下面这个脚本从一个主机上使用匿名ftp取下一个文件来。其中,主机名是第一个参数。文件名是第二个参数。
spawn ftp [index $argv 1]
expect *Name*
send anonymous
expect *Password:*
send [exec whoami]
expect *ok*ftp>*
send get [index $argv 2]
expect *ftp>*
上面这个程序被设计成在后台进行ftp。虽然他们在底层使用和expect类似的机制,但他们的可编程能力留待改进。因为expect提供了高级语言,你可以对它进行修改来满足你的特定需求。比如说,你可以加上以下功能:
:坚持--如果连接或者传输失败,你就可以每分钟或者每小时,甚
至可以根据其他因素,比如说用户的负载,来进行不定期的
重试。
:通知--传输时可以通过mail,write或者其他程序来通知你,甚至
可以通知失败。
:初始化-每一个用户都可以有自己的用高级语言编写的初始化文件
(比如说,.ftprc)。这和C shell对.cshrc的使用很类似。
expect还可以执行其他的更复杂的任务。比如说,他可以使用McGill大学的Archie系统。Archie是一个匿名的Telnet服务,它提供对描述Internet上可通过匿名ftp获取的文件的数据库的访问。通过使用这个服务,脚本可以询问Archie某个特定的文件的位置,并把它从 ftp服务器上取下来。这个功能的实现只要求在上面那个脚本中加上几行就可以。
现在还没有什么已知的后台-ftp能够实现上面的几项功能,能不要说所有的功能了。在expect里面,它的实现却是非常的简单。“坚持”的实现只要求在expect脚本里面加上一个循环。“通知”的实现只要执行mail和write就可以了。“初始化文件”的实现可以使用一个命令,source .ftprc,就可以了,在.ftprc里面可以有任何的expect命令。
虽然这些特征可以通过在已有的程序里面加上钩子函数就可以,但这也不能保证每一个人的要求都能得到满足。唯一能够提供保证的方法就是提供一种通用的语言。一个很好的解决方法就是把Tcl自身融入到ftp和其他的程序中间去。实际上,这本来就是Tcl的初衷。在还没有这样做之前,expect提供了一个能实现大部分功能但又不需要任何重写的方案。
9.[fsck]
fsck是另外一个缺乏足够的用户接口的例子。fsck几乎没有提供什么方法来预先的回答一些问题。你能做的就是给所有的问题都回答yes或者都回答no。
下面的程序段展示了一个脚本如何的使的自动的对某些问题回答yes,而对某些问题回答no。下面的这个脚本一开始先派生fsck进程,然后对其中两种类型的问题回答yes,而对其他的问题回答no。
for {} {1} {} {
expect
eof break
*UNREF FILE*CLEAR? {send r }
*BAD INODE*FIX? {send y }
*? {send n }
}
在下面这个版本里面,两个问题的回答是不同的。而且,如果脚本遇到了什么它不能理解的东西,就会执行interact命令把控制交给用户。用户的击键直接交给fsck处理。当执行完后,用户可以通过按+键来退出或者把控制交还给expect。如果控制是交还给脚本了,脚本就会自动的控制进程的剩余部分的运行。
for {} {1} {}{
expect
eof break
*UNREF FILE*CLEAR? {send y }
*BAD INODE*FIX? {send y }
*? {interact +}
}
如果没有expect,fsck只有在牺牲一定功能的情况下才可以非交互式的运行。fsck几乎是不可编程的,但它却是系统管理的最重要的工具。许多别的工具的用户接口也一样的不足。实际上,正是其中的一些程序的不足导致了expect的诞生。
10.[控制多个进程:作业控制]
expect的作业控制概念精巧的避免了通常的实现困难。其中包括了两个问题:一个是expect如何处理经典的作业控制,即当你在终端上按下^Z键时expect如何处理;另外一个就是expect是如何处理多进程的。
对第一个问题的处理是:忽略它。expect对经典的作业控制一无所知。比如说,你派生了一个程序并且发送一个^Z给它,它就会停下来(这是伪终端的完美之处)而expect就会永远的等下去。
但是,实际上,这根本就不成一个问题。对于一个expect脚本,没有必要向进程发送^Z。也就是说,没有必要停下一个进程来。expect仅仅是忽略了一个进程,而把自己的注意力转移到其他的地方。这就是expect的作业控制思想,这个思想也一直工作的很好。
从用户的角度来看是象这样的:当一个进程通过spawn命令启动时,变量spawn_id就被设置成某进程的描述符。由spawn_id描述的进程就被认为是当前进程。(这个描述符恰恰就是伪终端文件的描述符,虽然用户把它当作一个不透明的物体)。expect和send命令仅仅和当前进程进行交互。所以,切换一个作业所需要做的仅仅是把该进程的描述符赋给spawn_id。
这儿有一个例子向我们展示了如何通过作业控制来使两个 chess进程进行交互。在派生完两个进程之后,一个进程被通知先动一步。在下面的循环里面,每一步动作都送给另外一个进程。其中,read_move和 write_move两个过程留给读者来实现。(实际上,它们的实现非常的容易,但是,由于太长了所以没有包含在这里)。
spawn chess ;# start player one
set id1 $spawn_id
expect Chess
send first ;# force it to go first
read_move
spawn chess ;# start player two
set id2 $spawn_id
expect Chess
for {} {1} {}{
send_move
read_move
set spawn_id $id1
send_move
read_move
set spawn_id $id2
}
有一些应用程序和chess程序不太一样,在chess程序里,的两个玩家轮流动。下面这个脚本实现了一个冒充程序。它能够控制一个终端以便用户能够登录和正常的工作。但是,一旦系统提示输入密码或者输入用户名的时候,expect就开始把击键记下来,一直到用户按下回车键。这有效的收集了用户的密码和用户名,还避免了普通的冒充程序的Incorrect password-tryagain。而且,如果用户连接到另外一个主机上,那些额外的登录也会被记录下来。
spawn tip /dev/tty17 ;# open connection to
set tty $spawn_id ;# tty to be spoofed
spawn login
set login $spawn_id
log_user 0
for {} {1} {} {
set ready [select $tty $login]
case $login in $ready {
set spawn_id $login
expect
{*password* *login*}{
send_user $expect_match
set log 1
}
* ;# ignore everything else
set spawn_id $tty;
send $expect_match
}
case $tty in $ready {
set spawn_id $tty
expect * *{
if $log {
send_user $expect_match
set log 0
}
}
* {
send_user $expect_match
}
set spawn_id $login;
send $expect_match
}
}
这个脚本退出时用一个数字来表示所发生的情况。0表示passwd程序正常
运行,1表示非预期的死亡,2表示锁定,等等。使用数字是为了简单起见。
expect返回字符串和返回数字是一样简单的,即使是派生程序自身产生的消息
也是一样的。实际上,典型的做法是把整个交互的过程存到一个文件里面,只
有当程序的运行和预期一样的时候才把这个文件删除。否则这个log被留待以
后进一步的检查。
这个passwd检查脚本被设计成由别的脚本来驱动。这第二个脚本从一个文
件里面读取参数和预期的结果。对于每一个输入参数集,它调用第一个脚本并
且把结果和预期的结果相比较。(因为这个任务是非交互的,一个普通的老式
shell就可以用来解释第二个脚本)。比如说,一个passwd的数据文件很有可能
就象下面一样。
passwd.exp 3 bogus - -
passwd.exp 0 fred abledabl abledabl
passwd.exp 5 fred abcdefghijklm -
passwd.exp 5 fred abc -
passwd.exp 6 fred foobar bar
passwd.exp 4 fred ^C -
第一个域的名字是要被运行的回归脚本。第二个域是需要和结果相匹配的
退出值。第三个域就是用户名。第四个域和第五个域就是提示时应该输入的密
码。减号仅仅表示那里有一个域,这个域其实绝对不会用到。在第一个行中
,bogus表示用户名是非法的,因此passwd会响应说:没有此用户。expect在
退出时会返回3,3恰好就是第二个域。在最后一行中,^C就是被切实的送给程
序来验证程序是否恰当的退出。
通过这种方法,expect可以用来检验和调试交互式软件,这恰恰是IEEE的
POSIX 1003.2(shell和工具)的一致性检验所要求的。进一步的说明请参考
Libes[6]。
7.[rogue 和伪终端]
Unix用户肯定对通过管道来和其他进程相联系的方式非常的熟悉(比如说:
一个shell管道)。expect使用伪终端来和派生的进程相联系。伪终端提供了终
端语义以便程序认为他们正在和真正的终端进行I/O操作。
比如说,BSD的探险游戏rogue在生模式下运行,并假定在连接的另一端是
一个可寻址的字符终端。可以用expect编程,使得通过使用用户界面可以玩这
个游戏。
rogue这个探险游戏首先提供给你一个有各种物理属性,比如说力量值,的
角色。在大部分时间里,力量值都是16,但在几乎每20次里面就会有一个力量
值是18。很多的rogue玩家都知道这一点,但没有人愿意启动程序20次以获得一
个好的配置。下面的这个脚本就能达到这个目的。
for {} {1} {} {
spawn rogue
expect *Str:18* break
*Str:16*
close
wait
}
interact
第一行是个for循环,和C语言的控制格式很象。rogue启动后,expect就
检查看力量值是18还是16,如果是16,程序就通过执行close和wait来退出。
这两个命令的作用分别是关闭和伪终端的连接和等待进程退出。rogue读到一
个文件结束符就推出,从而循环继续运行,产生一个新的rogue游戏来检查。
当一个值为18的配置找到后,控制就推出循环并跳到最后一行脚本。
interact把控制转移给用户以便他们能够玩这个特定的游戏。
想象一下这个脚本的运行。你所能真正看到的就是20或者30个初始的配置
在不到一秒钟的时间里掠过屏幕,最后留给你的就是一个有着很好配置的游戏
。唯一比这更好的方法就是使用调试工具来玩游戏。
我们很有必要认识到这样一点:rogue是一个使用光标的图形游戏。
expect程序员必须了解到:光标的运动并不一定以一种直观的方式在屏幕上体
现。幸运的是,在我们这个例子里,这不是一个问题。将来的对expect的改
进可能会包括一个内嵌的能支持字符图形区域的终端模拟器。
8.[ftp]
我们使用expect写第一个脚本并没有打印出Hello,World。实际上,它
实现了一些更有用的功能。它能通过非交互的方式来运行ftp。ftp是用来在支
持TCP/IP的网络上进行文件传输的程序。除了一些简单的功能,一般的实现都
要求用户的参与。
下面这个脚本从一个主机上使用匿名ftp取下一个文件来。其中,主机名
是第一个参数。文件名是第二个参数。
spawn ftp [index $argv 1]
expect *Name*
send anonymous
expect *Password:*
send [exec whoami]
expect *ok*ftp>*
send get [index $argv 2]
expect *ftp>*
上面这个程序被设计成在后台进行ftp。虽然他们在底层使用和expect类
似的机制,但他们的可编程能力留待改进。因为expect提供了高级语言,你可
以对它进行修改来满足你的特定需求。比如说,你可以加上以下功能:
:坚持--如果连接或者传输失败,你就可以每分钟或者每小时,甚
至可以根据其他因素,比如说用户的负载,来进行不定期的
重试。
:通知--传输时可以通过mail,write或者其他程序来通知你,甚至
可以通知失败。
:初始化-每一个用户都可以有自己的用高级语言编写的初始化文件
(比如说,.ftprc)。这和C shell对.cshrc的使用很类似。
expect还可以执行其他的更复杂的任务。比如说,他可以使用McGill大学
的Archie系统。Archie是一个匿名的Telnet服务,它提供对描述Internet上可
通过匿名ftp获取的文件的数据库的访问。通过使用这个服务,脚本可以询问
Archie某个特定的文件的位置,并把它从ftp服务器上取下来。这个功能的实
现只要求在上面那个脚本中加上几行就可以。
现在还没有什么已知的后台-ftp能够实现上面的几项功能,能不要说所有
的功能了。在expect里面,它的实现却是非常的简单。“坚持”的实现只要求
在expect脚本里面加上一个循环。“通知”的实现只要执行mail和write就可以
了。“初始化文件”的实现可以使用一个命令,source .ftprc,就可以了,
在.ftprc里面可以有任何的expect命令。
虽然这些特征可以通过在已有的程序里面加上钩子函数就可以,但这也不
能保证每一个人的要求都能得到满足。唯一能够提供保证的方法就是提供一种
通用的语言。一个很好的解决方法就是把Tcl自身融入到ftp和其他的程序中间
去。实际上,这本来就是Tcl的初衷。在还没有这样做之前,expect提供了一
个能实现大部分功能但又不需要任何重写的方案。
9.[fsck]
fsck是另外一个缺乏足够的用户接口的例子。fsck几乎没有提供什么方法
来预先的回答一些问题。你能做的就是给所有的问题都回答yes或者都回答
no。
下面的程序段展示了一个脚本如何的使的自动的对某些问题回答yes,
而对某些问题回答no。下面的这个脚本一开始先派生fsck进程,然后对其
中两种类型的问题回答yes,而对其他的问题回答no。
for {} {1} {} {
expect
eof break
*UNREF FILE*CLEAR? {send r }
*BAD INODE*FIX? {send y }
*? {send n }
}
在下面这个版本里面,两个问题的回答是不同的。而且,如果脚本遇到
了什么它不能理解的东西,就会执行interact命令把控制交给用户。用户的
击键直接交给fsck处理。当执行完后,用户可以通过按+键来退出或者把
控制交还给expect。如果控制是交还给脚本了,脚本就会自动的控制进程的
剩余部分的运行。
for {} {1} {}{
expect
eof break
*UNREF FILE*CLEAR? {send y }
*BAD INODE*FIX? {send y }
*? {interact +}
}
如果没有expect,fsck只有在牺牲一定功能的情况下才可以非交互式的
运行。fsck几乎是不可编程的,但它却是系统管理的最重要的工具。许多别
的工具的用户接口也一样的不足。实际上,正是其中的一些程序的不足导致
了expect的诞生。
10.[控制多个进程:作业控制]
expect的作业控制概念精巧的避免了通常的实现困难。其中包括了两个问
题:一个是expect如何处理经典的作业控制,即当你在终端上按下^Z键时
expect如何处理;另外一个就是expect是如何处理多进程的。
对第一个问题的处理是:忽略它。expect对经典的作业控制一无所知。比
如说,你派生了一个程序并且发送一个^Z给它,它就会停下来(这是伪终端的
完美之处)而expect就会永远的等下去。
但是,实际上,这根本就不成一个问题。对于一个expect脚本,没有必要
向进程发送^Z。也就是说,没有必要停下一个进程来。expect仅仅是忽略了
一个进程,而把自己的注意力转移到其他的地方。这就是expect的作业控制
思想,这个思想也一直工作的很好。
从用户的角度来看是象这样的:当一个进程通过spawn命令启动时,变量
spawn_id就被设置成某进程的描述符。由spawn_id描述的进程就被认为是当
前进程。(这个描述符恰恰就是伪终端文件的描述符,虽然用户把它当作一个
不透明的物体)。expect和send命令仅仅和当前进程进行交互。所以,切换一
个作业所需要做的仅仅是把该进程的描述符赋给spawn_id。
这儿有一个例子向我们展示了如何通过作业控制来使两个chess进程进行
交互。在派生完两个进程之后,一个进程被通知先动一步。在下面的循环里
面,每一步动作都送给另外一个进程。其中,read_move和write_move两个过
程留给读者来实现。(实际上,它们的实现非常的容易,但是,由于太长了所
以没有包含在这里)。
spawn chess ;# start player one
set id1 $spawn_id
expect Chess
send first ;# force it to go first
read_move
spawn chess ;# start player two
set id2 $spawn_id
expect Chess
for {} {1} {}{
send_move
read_move
set spawn_id $id1
send_move
read_move
set spawn_id $id2
}
有一些应用程序和chess程序不太一样,在chess程序里,的两个玩家
轮流动。下面这个脚本实现了一个冒充程序。它能够控制一个终端以便用户
能够登录和正常的工作。但是,一旦系统提示输入密码或者输入用户名的时
候,expect就开始把击键记下来,一直到用户按下回车键。这有效的收集了
用户的密码和用户名,还避免了普通的冒充程序的Incorrect password-try
again。而且,如果用户连接到另外一个主机上,那些额外的登录也会被
记录下来。
spawn tip /dev/tty17 ;# open connection to
set tty $spawn_id ;# tty to be spoofed
spawn login
set login $spawn_id
log_user 0
for {} {1} {} {
set ready [select $tty $login]
case $login in $ready {
set spawn_id $login
expect
{*password* *login*}{
send_user $expect_match
set log 1
}
* ;# ignore everything else
set spawn_id $tty;
send $expect_match
}
case $tty in $ready {
set spawn_id $tty
expect * *{
if $log {
send_user $expect_match
set log 0
}
}
* {
send_user $expect_match
}
set spawn_id $login;
send $expect_match
}
}