[版權(quán)聲明]
Copyright(c) 1999
本教程由*葫蘆娃*翻譯,并做了適當(dāng)?shù)男薷模梢宰杂傻挠糜诜巧虡I(yè)目的。
但Redistribution時(shí)必須拷貝本[版權(quán)聲明]。
[BUG]
有不少部分,翻譯的時(shí)候不能作到“信,達(dá)”。當(dāng)然了,任何時(shí)候都沒(méi)有做到“雅”,希望各位諒解。
[原著]
Don Libes: National Institute of Standards and Technology
libes@cme.nist.gov
[目錄](méi)
1.摘要
2.關(guān)鍵字
3.簡(jiǎn)介
4.Expect綜述
5.callback
6.passwd 和一致性檢查
7.rogue 和偽終端
8.ftp
9.fsck
10.多進(jìn)程控制:作業(yè)控制
11.交互式使用Expect
12.交互式Expect編程
13.非交互式程序的控制
14.Expect的速度
15.安全方面的考慮
16.Expect資源
17.參考書(shū)籍
1.[摘要]
現(xiàn)代的Shell對(duì)程序提供了最小限度的控制(開(kāi)始,停止,等等),而把交互的特性留給了用戶。 這意味著有些程序,你不能非交互的運(yùn)行,比如說(shuō)passwd。 有一些程序可以非交互的運(yùn)行,但在很大程度上喪失了靈活性,比如說(shuō)fsck。這表明Unix的工具構(gòu)造邏輯開(kāi)始出現(xiàn)問(wèn)題。Expect恰恰填補(bǔ)了其中的一些裂痕,解決了在Unix環(huán)境中長(zhǎng)期存在著的一些問(wèn)題。
Expect使用Tcl作為語(yǔ)言核心。不僅如此,不管程序是交互和還是非交互的,Expect都能運(yùn)用。這是一個(gè)小語(yǔ)言和Unix的其他工具配合起來(lái)產(chǎn)生強(qiáng)大功能的經(jīng)典例子。
本部分教程并不是有關(guān)Expect的實(shí)現(xiàn),而是關(guān)于Expect語(yǔ)言本身的使用,這主要也是通過(guò)不同的腳本描述例子來(lái)體現(xiàn)。其中的幾個(gè)例子還例證了Expect的幾個(gè)新特征。
2.[關(guān)鍵字]
Expect,交互,POSIX,程序化的對(duì)話,Shell,Tcl,Unix;
3.[簡(jiǎn)介]
一個(gè)叫做fsck的Unix文件系統(tǒng)檢查程序,可以從Shell里面用-y或者-n選項(xiàng)來(lái)執(zhí)行。 在手冊(cè)[1]里面,-y選項(xiàng)的定義是象這樣的。
“對(duì)于fsck的所有問(wèn)題都假定一個(gè)“yes”響應(yīng);在這樣使用的時(shí)候,必須特別的小心,因?yàn)樗鼘?shí)際上允許程序無(wú)條件的繼續(xù)運(yùn)行,即使是遇到了一些非常嚴(yán)重的錯(cuò)誤”
相比之下,-n選項(xiàng)就安全的多,但它實(shí)際上幾乎一點(diǎn)用都沒(méi)有。這種接口非常的糟糕,但是卻有許多的程序都是這種風(fēng)格。 文件傳輸程序ftp有一個(gè)選項(xiàng)可以禁止交互式的提問(wèn),以便能從一個(gè)腳本里面運(yùn)行。但一旦發(fā)生了錯(cuò)誤,它沒(méi)有提供的處理措施。
Expect是一個(gè)控制交互式程序的工具。他解決了fsck的問(wèn)題,用非交互的方式實(shí)現(xiàn)了所有交互式的功能。Expect不是特別為fsck設(shè)計(jì)的,它也能進(jìn)行類似ftp的出錯(cuò)處理。
fsck和ftp的問(wèn)題向我們展示了象sh,csh和別的一些shell提供的用戶接口的局限性。 Shell沒(méi)有提供從一個(gè)程序讀和象一個(gè)程序?qū)懙墓δ堋_@意味著shell可以運(yùn)行fsck但只能以犧牲一部分fsck的靈活性做代價(jià)。有一些程序根本就不能被執(zhí)行。比如說(shuō),如果沒(méi)有一個(gè)用戶接口交互式的提供輸入,就沒(méi)法運(yùn)行下去。其他還有象Telnet,crypt,su,rlogin等程序無(wú)法在shell腳本里面自動(dòng)執(zhí)行。還有很多其他的應(yīng)用程序在設(shè)計(jì)是也是要求用戶輸入的。
Expect被設(shè)計(jì)成專門針和交互式程序的交互。一個(gè)Expect程序員可以寫一個(gè)腳本來(lái)描述程序和用戶的對(duì)話。接著Expect程序可以非交互的運(yùn)行“交互式”的程序。寫交互式程序的腳本和寫非交互式程序的腳本一樣簡(jiǎn)單。Expect還可以用于對(duì)對(duì)話的一部分進(jìn)行自動(dòng)化,因?yàn)槌绦虻目刂瓶梢栽阪I盤和腳本之間進(jìn)行切換。
bes[2]里面有詳細(xì)的描述。簡(jiǎn)單的說(shuō),腳本是用一種解釋性語(yǔ)言寫的。(也有C和C++的Expect庫(kù)可供使用,但這超出了本文的范圍).Expect提供了創(chuàng)建交互式進(jìn)程和讀寫它們的輸入和輸出的命令。 Expect是由于它的一個(gè)同名的命令而命名的。
Expect語(yǔ)言是基于Tcl的。Tcl實(shí)際上是一個(gè)子程序庫(kù),這些子程序庫(kù)可以嵌入到程序里從而提供語(yǔ)言服務(wù)。 最終的語(yǔ)言有點(diǎn)象一個(gè)典型的Shell語(yǔ)言。里面有給變量賦值的set命令,控制程序執(zhí)行的if,for,continue等命令,還能進(jìn)行普通的數(shù)學(xué)和字符串操作。當(dāng)然了,還可以用exec來(lái)調(diào)用Unix程序。所有這些功能,Tcl都有。Tcl在參考書(shū)籍 Outerhour[3][4]里有詳細(xì)的描述。
Expect是在Tcl基礎(chǔ)上創(chuàng)建起來(lái)的,它還提供了一些Tcl所沒(méi)有的命令。spawn命令激活一個(gè)Unix程序來(lái)進(jìn)行交互式的運(yùn)行。 send命令向進(jìn)程發(fā)送字符串。expect命令等待進(jìn)程的某些字符串。 expect支持正規(guī)表達(dá)式并能同時(shí)等待多個(gè)字符串,并對(duì)每一個(gè)字符串執(zhí)行不同的操作。expect還能理解一些特殊情況,如超時(shí)和遇到文件尾。
expect命令和Tcl的case命令的風(fēng)格很相似。都是用一個(gè)字符串去匹配多個(gè)字符串。(只要有可能,新的命令總是和已有的Tcl命令相似,以使得該語(yǔ)言保持工具族的繼承性)。下面關(guān)于expect的定義是從手冊(cè)[5]上摘錄下來(lái)的。
expect patlist1 action1 patlist2 action2.....
該命令一直等到當(dāng)前進(jìn)程的輸出和以上的某一個(gè)模式相匹配,或者等 到時(shí)間超過(guò)一個(gè)特定的時(shí)間長(zhǎng)度,或者等到遇到了文件的結(jié)束為止。
如果最后一個(gè)action是空的,就可以省略它。
每一個(gè)patlist都由一個(gè)模式或者模式的表(lists)組成。如果有一個(gè)模式匹配成功,相應(yīng)的action就被執(zhí)行。執(zhí)行的結(jié)果從expect返回。
被精確匹配的字符串(或者當(dāng)超時(shí)發(fā)生時(shí),已經(jīng)讀取但未進(jìn)行匹配的字符串)被存貯在變量expect_match里面。如果patlist是eof或者timeout,則發(fā)生文件結(jié)束或者超時(shí)時(shí)才執(zhí)行相應(yīng)的action.一般超時(shí)的時(shí)值是10秒,但可以用類似"set timeout 30"之類的命令把超時(shí)時(shí)值設(shè)定為30秒。
下面的一個(gè)程序段是從一個(gè)有關(guān)登錄的腳本里面摘取的。abort是在腳本的別處定義的過(guò)程,而其他的action使用類似與C語(yǔ)言的Tcl原語(yǔ)。
expect "*welcome*" break
"*busy*" {print busy;continue}
"*failed*" abort
timeout abort
模式是通常的C Shell風(fēng)格的正規(guī)表達(dá)式。模式必須匹配當(dāng)前進(jìn)程的從上一個(gè)expect或者interact開(kāi)始的所有輸出(所以統(tǒng)配符*使用的非常)的普遍。但是,一旦輸出超過(guò)2000個(gè)字節(jié),前面的字符就會(huì)被忘記,這可以通過(guò)設(shè)定match_max的值來(lái)改變。
expect命令確實(shí)體現(xiàn)了expect語(yǔ)言的最好和最壞的性質(zhì)。特別是,expect命令的靈活性是以經(jīng)常出現(xiàn)令人迷惑的語(yǔ)法做代價(jià)。除了關(guān)鍵字模式(比如說(shuō)eof,timeout)那些模式表可以包括多個(gè)模式。這保證提供了一種方法來(lái)區(qū)分他們。但是分開(kāi)這些表需要額外的掃描,如果沒(méi)有恰當(dāng)?shù)挠肹"]括起來(lái),這有可能會(huì)把和當(dāng)成空白字符。由于Tcl提供了兩種字符串引用的方法:?jiǎn)我碗p引,情況變的更糟。(在Tcl里面,如果不會(huì)出現(xiàn)二義性話,沒(méi)有必要使用引號(hào))。在expect的手冊(cè)里面,還有一個(gè)獨(dú)立的部分來(lái)解釋這種復(fù)雜性。幸運(yùn)的是:有一些很好的例子似乎阻止了這種抱怨。但是,這個(gè)復(fù)雜性很有可能在將來(lái)的版本中再度出現(xiàn)。為了增強(qiáng)可讀性,在本文中,提供的腳本都假定雙引號(hào)是足夠的。
字符可以使用反斜杠來(lái)單獨(dú)的引用,反斜杠也被用于對(duì)語(yǔ)句的延續(xù),如果不加反斜杠的話,語(yǔ)句到一行的結(jié)尾處就結(jié)束了。這和Tcl也是一致的。Tcl在發(fā)現(xiàn)有開(kāi)的單引號(hào)或者開(kāi)的雙引號(hào)時(shí)都會(huì)繼續(xù)掃描。而且,分號(hào)可以用于在一行中分割多個(gè)語(yǔ)句。這乍聽(tīng)起來(lái)有點(diǎn)讓人困惑,但是,這是解釋性語(yǔ)言的風(fēng)格,但是,這確實(shí)是Tcl的不太漂亮的部分。
5.[callback]
令人非常驚訝的是,一些小的腳本如何的產(chǎn)生一些有用的功能。下面是一個(gè)撥電話號(hào)碼的腳本。他用來(lái)把收費(fèi)反向,以便使得長(zhǎng)途電話對(duì)計(jì)算機(jī)計(jì)費(fèi)。這個(gè)腳本用類似“expect callback.exp 12016442332”來(lái)激活。其中,腳本的名字便是callback.exp,而+1(201)644-2332是要撥的電話號(hào)碼。
#first give the user some time to logout
exec sleep 4
spawn tip modem
expect "*connected*"
send "ATD [index $argv 1] "
#modem takes a while to connect
set timeout 60
expect "*CONNECT*"
第一行是注釋,第二行展示了如何調(diào)用沒(méi)有交互的Unix程序。sleep 4會(huì)使程序阻塞4秒,以使得用戶有時(shí)間來(lái)退出,因?yàn)閙odem總是會(huì)回叫用戶已經(jīng)使用的電話號(hào)碼。
下面一行使用spawn命令來(lái)激活tip程序,以便使得tip的輸出能夠被expect所讀取,使得tip能從send讀輸入。一旦tip說(shuō)它已經(jīng)連接上,modem就會(huì)要求去撥打大哥電話號(hào)碼。(假定modem都是賀氏兼容的,但是本腳本可以很容易的修改成能適應(yīng)別的類型的modem)。不論發(fā)生了什么,expect都會(huì)終止。如果呼叫失敗,expect腳本可以設(shè)計(jì)成進(jìn)行重試,但這里沒(méi)有。如果呼叫成功,getty會(huì)在expect退出后檢測(cè)到DTR,并且向用戶提示loging:。(實(shí)用的腳本往往提供更多的錯(cuò)誤檢測(cè))。
這個(gè)腳本展示了命令行參數(shù)的使用,命令行參數(shù)存貯在一個(gè)叫做argv的表里面(這和C語(yǔ)言的風(fēng)格很象)。在這種情況下,第一個(gè)元素就是電話號(hào)碼。方括號(hào)使得被括起來(lái)的部分當(dāng)作命令來(lái)執(zhí)行,結(jié)果就替換被括起來(lái)的部分。這也和C Shell的風(fēng)格很象。
這個(gè)腳本和一個(gè)大約60K的C語(yǔ)言程序?qū)崿F(xiàn)的功能相似。
6.[passwd和一致性檢查]
在前面,我們提到passwd程序在缺乏用戶交互的情況下,不能運(yùn)行,passwd會(huì)忽略I/O重定向,也不能嵌入到管道里邊以便能從別的程序或者文件里讀取輸入。這個(gè)程序堅(jiān)持要求真正的與用戶進(jìn)行交互。因?yàn)榘踩脑颍琾asswd被設(shè)計(jì)成這樣,但結(jié)果導(dǎo)致沒(méi)有非交互式的方法來(lái)檢驗(yàn)passwd。這樣一個(gè)對(duì)系統(tǒng)安全至關(guān)重要的程序竟然沒(méi)有辦法進(jìn)行可靠的檢驗(yàn),真實(shí)具有諷刺意味。
passwd以一個(gè)用戶名作為參數(shù),交互式的提示輸入密碼。下面的expect腳本以用戶名和密碼作為參數(shù)而非交互式的運(yùn)行。
spawn oasswd [index $argv 1]
set password [index $argv 2]
expect "*password:"
send "$password "
expect "*password:"
send "$password "
expect eof
第一行以用戶名做參數(shù)啟動(dòng)passwd程序,為方便起見(jiàn),第二行把密碼存到一個(gè)變量里面。和shell類似,變量的使用也不需要提前聲明。
在第三行,expect搜索模式"*password:",其中*允許匹配任意輸入,所以對(duì)于避免指定所有細(xì)節(jié)而言是非常有效的。 上面的程序里沒(méi)有action,所以expect檢測(cè)到該模式后就繼續(xù)運(yùn)行。
一旦接收到提示后,下一行就就把密碼送給當(dāng)前進(jìn)程。表明回車。(實(shí)際上,所有的C的關(guān)于字符的約定都支持)。上面的程序中有兩個(gè)expect-send序列,因?yàn)閜asswd為了對(duì)輸入進(jìn)行確認(rèn),要求進(jìn)行兩次輸入。在非交互式程序里面,這是毫無(wú)必要的,但由于假定passwd是在和用戶進(jìn)行交互,所以我們的腳本還是這樣做了。
最后,"expect eof"這一行的作用是在passwd的輸出中搜索文件結(jié)束符,這一行語(yǔ)句還展示了關(guān)鍵字的匹配。另外一個(gè)關(guān)鍵字匹配就是timeout了,timeout被用于表示所有匹配的失敗而和一段特定長(zhǎng)度的時(shí)間相匹配。在這里eof是非常有必要的,因?yàn)閜asswd被設(shè)計(jì)成會(huì)檢查它的所有I/O是否都成功了,包括第二次輸入密碼時(shí)產(chǎn)生的最后一個(gè)新行。
這個(gè)腳本已經(jīng)足夠展示passwd命令的基本交互性。另外一個(gè)更加完備的例子回檢查別的一些行為。比如說(shuō),下面的這個(gè)腳本就能檢查passwd程序的別的幾個(gè)方面。所有的提示都進(jìn)行了檢查。對(duì)垃圾輸入的檢查也進(jìn)行了適當(dāng)?shù)奶幚怼_M(jìn)程死亡,超乎尋常的慢響應(yīng),或者別的非預(yù)期的行為都進(jìn)行了處理。
spawn passwd [index $argv 1]
expect eof {exit 1}
timeout {exit 2}
"*No such user.*" {exit 3}
"*New password:"
send "[index $argv 2 "
expect eof {exit 4}
timeout {exit 2}
"*Password too long*" {exit 5}
"*Password too short*" {exit 5}
"*Retype ew password:"
send "[index $argv 3] "
expect timeout {exit 2}
"*Mismatch*" {exit 6}
"*Password unchanged*" {exit 7}
" "
expect timeout {exit 2}
"*" {exit 6}
eof
這個(gè)腳本退出時(shí)用一個(gè)數(shù)字來(lái)表示所發(fā)生的情況。0表示passwd程序正常運(yùn)行,1表示非預(yù)期的死亡,2表示鎖定,等等。使用數(shù)字是為了簡(jiǎn)單起見(jiàn)。expect返回字符串和返回?cái)?shù)字是一樣簡(jiǎn)單的,即使是派生程序自身產(chǎn)生的消息也是一樣的。實(shí)際上,典型的做法是把整個(gè)交互的過(guò)程存到一個(gè)文件里面,只有當(dāng)程序的運(yùn)行和預(yù)期一樣的時(shí)候才把這個(gè)文件刪除。否則這個(gè)log被留待以后進(jìn)一步的檢查。
這個(gè)passwd檢查腳本被設(shè)計(jì)成由別的腳本來(lái)驅(qū)動(dòng)。這第二個(gè)腳本從一個(gè)文件里面讀取參數(shù)和預(yù)期的結(jié)果。對(duì)于每一個(gè)輸入?yún)?shù)集,它調(diào)用第一個(gè)腳本并且把結(jié)果和預(yù)期的結(jié)果相比較。(因?yàn)檫@個(gè)任務(wù)是非交互的,一個(gè)普通的老式shell就可以用來(lái)解釋第二個(gè)腳本)。比如說(shuō),一個(gè)passwd的數(shù)據(jù)文件很有可能就象下面一樣。
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 -
第一個(gè)域的名字是要被運(yùn)行的回歸腳本。第二個(gè)域是需要和結(jié)果相匹配的退出值。第三個(gè)域就是用戶名。第四個(gè)域和第五個(gè)域就是提示時(shí)應(yīng)該輸入的密碼。減號(hào)僅僅表示那里有一個(gè)域,這個(gè)域其實(shí)絕對(duì)不會(huì)用到。在第一個(gè)行中,bogus表示用戶名是非法的,因此passwd會(huì)響應(yīng)說(shuō):沒(méi)有此用戶。expect在退出時(shí)會(huì)返回3,3恰好就是第二個(gè)域。在最后一行中,^C就是被切實(shí)的送給程序來(lái)驗(yàn)證程序是否恰當(dāng)?shù)耐顺觥?br />
通過(guò)這種方法,expect可以用來(lái)檢驗(yàn)和調(diào)試交互式軟件,這恰恰是IEEE的POSIX 1003.2(shell和工具)的一致性檢驗(yàn)所要求的。進(jìn)一步的說(shuō)明請(qǐng)參考Libes[6]。
7.[rogue 和偽終端]
Unix用戶肯定對(duì)通過(guò)管道來(lái)和其他進(jìn)程相聯(lián)系的方式非常的熟悉(比如說(shuō):一個(gè)shell管道)。expect使用偽終端來(lái)和派生的進(jìn)程相聯(lián)系。偽終端提供了終端語(yǔ)義以便程序認(rèn)為他們正在和真正的終端進(jìn)行I/O操作。
比如說(shuō),BSD的探險(xiǎn)游戲rogue在生模式下運(yùn)行,并假定在連接的另一端是一個(gè)可尋址的字符終端。可以用expect編程,使得通過(guò)使用用戶界面可以玩這個(gè)游戲。
rogue這個(gè)探險(xiǎn)游戲首先提供給你一個(gè)有各種物理屬性,比如說(shuō)力量值,的角色。在大部分時(shí)間里,力量值都是16,但在幾乎每20次里面就會(huì)有一個(gè)力量值是18。很多的rogue玩家都知道這一點(diǎn),但沒(méi)有人愿意啟動(dòng)程序20次以獲得一個(gè)好的配置。下面的這個(gè)腳本就能達(dá)到這個(gè)目的。
for {} {1} {} {
spawn rogue
expect "*Str:18*" break
"*Str:16*"
close
wait
}
interact
第一行是個(gè)for循環(huán),和C語(yǔ)言的控制格式很象。rogue啟動(dòng)后,expect就檢查看力量值是18還是16,如果是16,程序就通過(guò)執(zhí)行close和wait來(lái)退出。這兩個(gè)命令的作用分別是關(guān)閉和偽終端的連接和等待進(jìn)程退出。rogue讀到一個(gè)文件結(jié)束符就推出,從而循環(huán)繼續(xù)運(yùn)行,產(chǎn)生一個(gè)新的rogue游戲來(lái)檢查。
當(dāng)一個(gè)值為18的配置找到后,控制就推出循環(huán)并跳到最后一行腳本。interact把控制轉(zhuǎn)移給用戶以便他們能夠玩這個(gè)特定的游戲。
想象一下這個(gè)腳本的運(yùn)行。你所能真正看到的就是20或者30個(gè)初始的配置在不到一秒鐘的時(shí)間里掠過(guò)屏幕,最后留給你的就是一個(gè)有著很好配置的游戲。唯一比這更好的方法就是使用調(diào)試工具來(lái)玩游戲。
我們很有必要認(rèn)識(shí)到這樣一點(diǎn):rogue是一個(gè)使用光標(biāo)的圖形游戲。expect程序員必須了解到:光標(biāo)的運(yùn)動(dòng)并不一定以一種直觀的方式在屏幕上體現(xiàn)。幸運(yùn)的是,在我們這個(gè)例子里,這不是一個(gè)問(wèn)題。將來(lái)的對(duì)expect的改進(jìn)可能會(huì)包括一個(gè)內(nèi)嵌的能支持字符圖形區(qū)域的終端模擬器。
8.[ftp]
我們使用expect寫第一個(gè)腳本并沒(méi)有打印出"Hello,World"。實(shí)際上,它實(shí)現(xiàn)了一些更有用的功能。它能通過(guò)非交互的方式來(lái)運(yùn)行ftp。ftp是用來(lái)在支持TCP/IP的網(wǎng)絡(luò)上進(jìn)行文件傳輸?shù)某绦颉3艘恍┖?jiǎn)單的功能,一般的實(shí)現(xiàn)都要求用戶的參與。
下面這個(gè)腳本從一個(gè)主機(jī)上使用匿名ftp取下一個(gè)文件來(lái)。其中,主機(jī)名是第一個(gè)參數(shù)。文件名是第二個(gè)參數(shù)。
spawn ftp [index $argv 1]
expect "*Name*"
send "anonymous "
expect "*Password:*"
send [exec whoami]
expect "*ok*ftp>*"
send "get [index $argv 2] "
expect "*ftp>*"
上面這個(gè)程序被設(shè)計(jì)成在后臺(tái)進(jìn)行ftp。雖然他們?cè)诘讓邮褂煤蚭xpect類似的機(jī)制,但他們的可編程能力留待改進(jìn)。因?yàn)閑xpect提供了高級(jí)語(yǔ)言,你可以對(duì)它進(jìn)行修改來(lái)滿足你的特定需求。比如說(shuō),你可以加上以下功能:
:堅(jiān)持--如果連接或者傳輸失敗,你就可以每分鐘或者每小時(shí),甚
至可以根據(jù)其他因素,比如說(shuō)用戶的負(fù)載,來(lái)進(jìn)行不定期的
重試。
:通知--傳輸時(shí)可以通過(guò)mail,write或者其他程序來(lái)通知你,甚至
可以通知失敗。
:初始化-每一個(gè)用戶都可以有自己的用高級(jí)語(yǔ)言編寫的初始化文件
(比如說(shuō),.ftprc)。這和C shell對(duì).cshrc的使用很類似。
expect還可以執(zhí)行其他的更復(fù)雜的任務(wù)。比如說(shuō),他可以使用McGill大學(xué)的Archie系統(tǒng)。Archie是一個(gè)匿名的Telnet服務(wù),它提供對(duì)描述Internet上可通過(guò)匿名ftp獲取的文件的數(shù)據(jù)庫(kù)的訪問(wèn)。通過(guò)使用這個(gè)服務(wù),腳本可以詢問(wèn)Archie某個(gè)特定的文件的位置,并把它從ftp服務(wù)器上取下來(lái)。這個(gè)功能的實(shí)現(xiàn)只要求在上面那個(gè)腳本中加上幾行就可以。
現(xiàn)在還沒(méi)有什么已知的后臺(tái)-ftp能夠?qū)崿F(xiàn)上面的幾項(xiàng)功能,能不要說(shuō)所有的功能了。在expect里面,它的實(shí)現(xiàn)卻是非常的簡(jiǎn)單。“堅(jiān)持”的實(shí)現(xiàn)只要求在expect腳本里面加上一個(gè)循環(huán)。“通知”的實(shí)現(xiàn)只要執(zhí)行mail和write就可以了。“初始化文件”的實(shí)現(xiàn)可以使用一個(gè)命令,source .ftprc,就可以了,在.ftprc里面可以有任何的expect命令。
雖然這些特征可以通過(guò)在已有的程序里面加上鉤子函數(shù)就可以,但這也不能保證每一個(gè)人的要求都能得到滿足。唯一能夠提供保證的方法就是提供一種通用的語(yǔ)言。一個(gè)很好的解決方法就是把Tcl自身融入到ftp和其他的程序中間去。實(shí)際上,這本來(lái)就是Tcl的初衷。在還沒(méi)有這樣做之前,expect提供了一個(gè)能實(shí)現(xiàn)大部分功能但又不需要任何重寫的方案。
9.[fsck]
fsck是另外一個(gè)缺乏足夠的用戶接口的例子。fsck幾乎沒(méi)有提供什么方法來(lái)預(yù)先的回答一些問(wèn)題。你能做的就是給所有的問(wèn)題都回答"yes"或者都回答"no"。
下面的程序段展示了一個(gè)腳本如何的使的自動(dòng)的對(duì)某些問(wèn)題回答"yes",而對(duì)某些問(wèn)題回答"no"。下面的這個(gè)腳本一開(kāi)始先派生fsck進(jìn)程,然后對(duì)其中兩種類型的問(wèn)題回答"yes",而對(duì)其他的問(wèn)題回答"no"。
for {} {1} {} {
expect
eof break
"*UNREF FILE*CLEAR?" {send "r "}
"*BAD INODE*FIX?" {send "y "}
"*?" {send "n "}
}
在下面這個(gè)版本里面,兩個(gè)問(wèn)題的回答是不同的。而且,如果腳本遇到了什么它不能理解的東西,就會(huì)執(zhí)行interact命令把控制交給用戶。用戶的擊鍵直接交給fsck處理。當(dāng)執(zhí)行完后,用戶可以通過(guò)按"+"鍵來(lái)退出或者把控制交還給expect。如果控制是交還給腳本了,腳本就會(huì)自動(dòng)的控制進(jìn)程的剩余部分的運(yùn)行。
for {} {1} {}{
expect
eof break
"*UNREF FILE*CLEAR?" {send "y "}
"*BAD INODE*FIX?" {send "y "}
"*?" {interact +}
}
如果沒(méi)有expect,fsck只有在犧牲一定功能的情況下才可以非交互式的運(yùn)行。fsck幾乎是不可編程的,但它卻是系統(tǒng)管理的最重要的工具。許多別的工具的用戶接口也一樣的不足。實(shí)際上,正是其中的一些程序的不足導(dǎo)致了expect的誕生。
10.[控制多個(gè)進(jìn)程:作業(yè)控制]
expect的作業(yè)控制概念精巧的避免了通常的實(shí)現(xiàn)困難。其中包括了兩個(gè)問(wèn)題:一個(gè)是expect如何處理經(jīng)典的作業(yè)控制,即當(dāng)你在終端上按下^Z鍵時(shí)expect如何處理;另外一個(gè)就是expect是如何處理多進(jìn)程的。
對(duì)第一個(gè)問(wèn)題的處理是:忽略它。expect對(duì)經(jīng)典的作業(yè)控制一無(wú)所知。比如說(shuō),你派生了一個(gè)程序并且發(fā)送一個(gè)^Z給它,它就會(huì)停下來(lái)(這是偽終端的完美之處)而expect就會(huì)永遠(yuǎn)的等下去。
但是,實(shí)際上,這根本就不成一個(gè)問(wèn)題。對(duì)于一個(gè)expect腳本,沒(méi)有必要向進(jìn)程發(fā)送^Z。也就是說(shuō),沒(méi)有必要停下一個(gè)進(jìn)程來(lái)。expect僅僅是忽略了一個(gè)進(jìn)程,而把自己的注意力轉(zhuǎn)移到其他的地方。這就是expect的作業(yè)控制思想,這個(gè)思想也一直工作的很好。
從用戶的角度來(lái)看是象這樣的:當(dāng)一個(gè)進(jìn)程通過(guò)spawn命令啟動(dòng)時(shí),變量spawn_id就被設(shè)置成某進(jìn)程的描述符。由spawn_id描述的進(jìn)程就被認(rèn)為是當(dāng)前進(jìn)程。(這個(gè)描述符恰恰就是偽終端文件的描述符,雖然用戶把它當(dāng)作一個(gè)不透明的物體)。expect和send命令僅僅和當(dāng)前進(jìn)程進(jìn)行交互。所以,切換一個(gè)作業(yè)所需要做的僅僅是把該進(jìn)程的描述符賦給spawn_id。
這兒有一個(gè)例子向我們展示了如何通過(guò)作業(yè)控制來(lái)使兩個(gè)chess進(jìn)程進(jìn)行交互。在派生完兩個(gè)進(jìn)程之后,一個(gè)進(jìn)程被通知先動(dòng)一步。在下面的循環(huán)里面,每一步動(dòng)作都送給另外一個(gè)進(jìn)程。其中,read_move和write_move兩個(gè)過(guò)程留給讀者來(lái)實(shí)現(xiàn)。(實(shí)際上,它們的實(shí)現(xiàn)非常的容易,但是,由于太長(zhǎng)了所以沒(méi)有包含在這里)。
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
}
有一些應(yīng)用程序和chess程序不太一樣,在chess程序里,的兩個(gè)玩家輪流動(dòng)。下面這個(gè)腳本實(shí)現(xiàn)了一個(gè)冒充程序。它能夠控制一個(gè)終端以便用戶能夠登錄和正常的工作。但是,一旦系統(tǒng)提示輸入密碼或者輸入用戶名的時(shí)候,expect就開(kāi)始把擊鍵記下來(lái),一直到用戶按下回車鍵。這有效的收集了用戶的密碼和用戶名,還避免了普通的冒充程序的"Incorrect password-tryagain"。而且,如果用戶連接到另外一個(gè)主機(jī)上,那些額外的登錄也會(huì)被記錄下來(lái)。
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
}
}
這個(gè)腳本是這樣工作的。首先連接到一個(gè)login進(jìn)程和終端。缺省的,所有的對(duì)話都記錄到標(biāo)準(zhǔn)輸出上(通過(guò)send_user)。因?yàn)槲覀儗?duì)此并不感興趣,所以,我們通過(guò)命令"log_user 0"來(lái)禁止這個(gè)功能。(有很多的命令來(lái)控制可以看見(jiàn)或者可以記錄的東西)。
在循環(huán)里面,select等待終端或者login進(jìn)程上的動(dòng)作,并且返回一個(gè)等待輸入的spawn_id表。如果在表里面找到了一個(gè)值的話,case就執(zhí)行一個(gè)action。比如說(shuō),如果字符串"login"出現(xiàn)在login進(jìn)程的輸出中,提示就會(huì)被記錄到標(biāo)準(zhǔn)輸出上,并且有一個(gè)標(biāo)志被設(shè)置以便通知腳本開(kāi)始記錄用戶的擊鍵,直至用戶按下了回車鍵。無(wú)論收到什么,都會(huì)回顯到終端上,一個(gè)相應(yīng)的action會(huì)在腳本的終端那一部分執(zhí)行。
這些例子顯示了expect的作業(yè)控制方式。通過(guò)把自己插入到對(duì)話里面,expect可以在進(jìn)程之間創(chuàng)建復(fù)雜的I/O流。可以創(chuàng)建多扇出,復(fù)用扇入的,動(dòng)態(tài)的數(shù)據(jù)相關(guān)的進(jìn)程圖。
相比之下,shell使得它自己一次一行的讀取一個(gè)文件顯的很困難。shell強(qiáng)迫用戶按下控制鍵(比如,^C,^Z)和關(guān)鍵字(比如fg和bg)來(lái)實(shí)現(xiàn)作業(yè)的切換。這些都無(wú)法從腳本里面利用。相似的是:以非交互方式運(yùn)行的shell并不處理“歷史記錄”和其他一些僅僅為交互式使用設(shè)計(jì)的特征。這也出現(xiàn)了和前面哪個(gè)passwd程序的相似問(wèn)題。相似的,也無(wú)法編寫能夠回歸的測(cè)試shell的某些動(dòng)作的shell腳本。結(jié)果導(dǎo)致shell的這些方面無(wú)法進(jìn)行徹底的測(cè)試。
如果使用expect的話,可以使用它的交互式的作業(yè)控制來(lái)驅(qū)動(dòng)shell。一個(gè)派生的shell認(rèn)為它是在交互的運(yùn)行著,所以會(huì)正常的處理作業(yè)控制。它不僅能夠解決檢驗(yàn)處理作業(yè)控制的shell和其他一些程序的問(wèn)題。還能夠在必要的時(shí)候,讓shell代替expect來(lái)處理作業(yè)。可以支持使用shell風(fēng)格的作業(yè)控制來(lái)支持進(jìn)程的運(yùn)行。這意味著:首先派生一個(gè)shell,然后把命令送給shell來(lái)啟動(dòng)進(jìn)程。如果進(jìn)程被掛起,比如說(shuō),發(fā)送了一個(gè)^Z,進(jìn)程就會(huì)停下來(lái),并把控制返回給shell。對(duì)于expect而言,它還在處理同一個(gè)進(jìn)程(原來(lái)那個(gè)shell)。
expect的解決方法不僅具有很大的靈活性,它還避免了重復(fù)已經(jīng)存在于shell中的作業(yè)控制軟件。通過(guò)使用shell,由于你可以選擇你想派生的shell,所以你可以根據(jù)需要獲得作業(yè)控制權(quán)。而且,一旦你需要(比如說(shuō)檢驗(yàn)的時(shí)候),你就可以驅(qū)動(dòng)一個(gè)shell來(lái)讓這個(gè)shell以為它正在交互式的運(yùn)行。這一點(diǎn)對(duì)于在檢測(cè)到它們是否在交互式的運(yùn)行之后會(huì)改變輸出的緩沖的程序來(lái)說(shuō)也是很重要的。
為了進(jìn)一步的控制,在interact執(zhí)行期間,expect把控制終端(是啟動(dòng)expect的那個(gè)終端,而不是偽終端)設(shè)置成生模式以便字符能夠正確的傳送給派生的進(jìn)程。當(dāng)expect在沒(méi)有執(zhí)行interact的時(shí)候,終端處于熟模式下,這時(shí)候作業(yè)控制就可以作用于expect本身。
11.[交互式的使用expect]
在前面,我們提到可以通過(guò)interact命令來(lái)交互式的使用腳本。基本上來(lái)說(shuō),interact命令提供了對(duì)對(duì)話的自由訪問(wèn),但我們需要一些更精細(xì)的控制。這一點(diǎn),我們也可以使用expect來(lái)達(dá)到,因?yàn)閑xpect從標(biāo)準(zhǔn)輸入中讀取輸入和從進(jìn)程中讀取輸入一樣的簡(jiǎn)單。 但是,我們要使用expect_user和send_user來(lái)進(jìn)行標(biāo)準(zhǔn)I/O,同時(shí)不改變spawn_id。
下面的這個(gè)腳本在一定的時(shí)間內(nèi)從標(biāo)準(zhǔn)輸入里面讀取一行。這個(gè)腳本叫做timed_read,可以從csh里面調(diào)用,比如說(shuō),set answer="timed_read 30"就能調(diào)用它。
#!/usr/local/bin/expect -f
set timeout [index $argv 1]
expect_user "* "
send_user $expect_match
第三行從用戶那里接收任何以新行符結(jié)束的任何一行。最后一行把它返回給標(biāo)準(zhǔn)輸出。如果在特定的時(shí)間內(nèi)沒(méi)有得到任何鍵入,則返回也為空。
第一行支持"#!"的系統(tǒng)直接的啟動(dòng)腳本。(如果把腳本的屬性加上可執(zhí)行屬性則不要在腳本前面加上expect)。當(dāng)然了腳本總是可以顯式的用"expect scripot"來(lái)啟動(dòng)。在-c后面的選項(xiàng)在任何腳本語(yǔ)句執(zhí)行前就被執(zhí)行。比如說(shuō),不要修改腳本本身,僅僅在命令行上加上-c "trace...",該腳本可以加上trace功能了(省略號(hào)表示trace的選項(xiàng))。
在命令行里實(shí)際上可以加上多個(gè)命令,只要中間以";"分開(kāi)就可以了。比如說(shuō),下面這個(gè)命令行:
expect -c "set timeout 20;spawn foo;expect"
一旦你把超時(shí)時(shí)限設(shè)置好而且程序啟動(dòng)之后,expect就開(kāi)始等待文件結(jié)束符或者20秒的超時(shí)時(shí)限。 如果遇到了文件結(jié)束符(EOF),該程序就會(huì)停下來(lái),然后expect返回。如果是遇到了超時(shí)的情況,expect就返回。在這兩中情況里面,都隱式的殺死了當(dāng)前進(jìn)程。
如果我們不使用expect而來(lái)實(shí)現(xiàn)以上兩個(gè)例子的功能的話,我們還是可以學(xué)習(xí)到很多的東西的。在這兩中情況里面,通常的解決方案都是fork另一個(gè)睡眠的子進(jìn)程并且用signal通知原來(lái)的shell。如果這個(gè)過(guò)程或者讀先發(fā)生的話,shell就會(huì)殺司那個(gè)睡眠的進(jìn)程。 傳遞pid和防止后臺(tái)進(jìn)程產(chǎn)生啟動(dòng)信息是一個(gè)讓除了高手級(jí)shell程序員之外的人頭痛的事情。提供一個(gè)通用的方法來(lái)象這樣啟動(dòng)多個(gè)進(jìn)程會(huì)使shell腳本非常的復(fù)雜。 所以幾乎可以肯定的是,程序員一般都用一個(gè)專門C程序來(lái)解決這樣一個(gè)問(wèn)題。
expect_user,send_user,send_error(向標(biāo)準(zhǔn)錯(cuò)誤終端輸出)在比較長(zhǎng)的,用來(lái)把從進(jìn)程來(lái)的復(fù)雜交互翻譯成簡(jiǎn)單交互的expect腳本里面使用的比較頻繁。在參考[7]里面,Libs描述怎樣用腳本來(lái)安全的包裹(wrap)adb,怎樣把系統(tǒng)管理員從需要掌握adb的細(xì)節(jié)里面解脫出來(lái),同時(shí)大大的降低了由于錯(cuò)誤的擊鍵而導(dǎo)致的系統(tǒng)崩潰。
一個(gè)簡(jiǎn)單的例子能夠讓ftp自動(dòng)的從一個(gè)私人的帳號(hào)里面取文件。在這種情況里,要求提供密碼。 即使文件的訪問(wèn)是受限的,你也應(yīng)該避免把密碼以明文的方式存儲(chǔ)在文件里面。把密碼作為腳本運(yùn)行時(shí)的參數(shù)也是不合適的,因?yàn)橛胮s命令能看到它們。有一個(gè)解決的方法就是在腳本運(yùn)行的開(kāi)始調(diào)用expect_user來(lái)讓用戶輸入以后可能使用的密碼。這個(gè)密碼必須只能讓這個(gè)腳本知道,即使你是每個(gè)小時(shí)都要重試ftp。
即使信息是立即輸入進(jìn)去的,這個(gè)技巧也是非常有用。比如說(shuō),你可以寫一個(gè)腳本,把你每一個(gè)主機(jī)上不同的帳號(hào)上的密碼都改掉,不管他們使用的是不是同一個(gè)密碼數(shù)據(jù)庫(kù)。如果你要手工達(dá)到這樣一個(gè)功能的話,你必須Telnet到每一個(gè)主機(jī)上,并且手工輸入新的密碼。而使用expect,你可以只輸入密碼一次而讓腳本來(lái)做其它的事情。
expect_user和interact也可以在一個(gè)腳本里面混合的使用。考慮一下在調(diào)試一個(gè)程序的循環(huán)時(shí),經(jīng)過(guò)好多步之后才失敗的情況。一個(gè)expect腳本可以驅(qū)動(dòng)哪個(gè)調(diào)試器,設(shè)置好斷點(diǎn),執(zhí)行該程序循環(huán)的若干步,然后將控制返回給鍵盤。它也可以在返回控制之前,在循環(huán)體和條件測(cè)試之間來(lái)回的切換。
6.[passwd和一致性檢查]
在前面,我們提到passwd程序在缺乏用戶交互的情況下,不能運(yùn)行,passwd
會(huì)忽略I/O重定向,也不能嵌入到管道里邊以便能從別的程序或者文件里讀取輸
入。這個(gè)程序堅(jiān)持要求真正的與用戶進(jìn)行交互。因?yàn)榘踩脑颍琾asswd被設(shè)計(jì)
成這樣,但結(jié)果導(dǎo)致沒(méi)有非交互式的方法來(lái)檢驗(yàn)passwd。這樣一個(gè)對(duì)系統(tǒng)安全
至關(guān)重要的程序竟然沒(méi)有辦法進(jìn)行可靠的檢驗(yàn),真實(shí)具有諷刺意味。
passwd以一個(gè)用戶名作為參數(shù),交互式的提示輸入密碼。下面的expect腳
本以用戶名和密碼作為參數(shù)而非交互式的運(yùn)行。
spawn oasswd [index $argv 1]
set password [index $argv 2]
expect "*password:"
send "$password "
expect "*password:"
send "$password "
expect eof
第一行以用戶名做參數(shù)啟動(dòng)passwd程序,為方便起見(jiàn),第二行把密碼存到
一個(gè)變量里面。和shell類似,變量的使用也不需要提前聲明。
在第三行,expect搜索模式"*password:",其中*允許匹配任意輸入,所
以對(duì)于避免指定所有細(xì)節(jié)而言是非常有效的。 上面的程序里沒(méi)有action,所以
expect檢測(cè)到該模式后就繼續(xù)運(yùn)行。
一旦接收到提示后,下一行就就把密碼送給當(dāng)前進(jìn)程。表明回車。(實(shí)
際上,所有的C的關(guān)于字符的約定都支持)。上面的程序中有兩個(gè)expect-send
序列,因?yàn)閜asswd為了對(duì)輸入進(jìn)行確認(rèn),要求進(jìn)行兩次輸入。在非交互式程序
里面,這是毫無(wú)必要的,但由于假定passwd是在和用戶進(jìn)行交互,所以我們的
腳本還是這樣做了。
最后,"expect eof"這一行的作用是在passwd的輸出中搜索文件結(jié)束符,
這一行語(yǔ)句還展示了關(guān)鍵字的匹配。另外一個(gè)關(guān)鍵字匹配就是timeout了,
timeout被用于表示所有匹配的失敗而和一段特定長(zhǎng)度的時(shí)間相匹配。在這里
eof是非常有必要的,因?yàn)閜asswd被設(shè)計(jì)成會(huì)檢查它的所有I/O是否都成功了,
包括第二次輸入密碼時(shí)產(chǎn)生的最后一個(gè)新行。
這個(gè)腳本已經(jīng)足夠展示passwd命令的基本交互性。另外一個(gè)更加完備的例
子回檢查別的一些行為。比如說(shuō),下面的這個(gè)腳本就能檢查passwd程序的別的
幾個(gè)方面。所有的提示都進(jìn)行了檢查。對(duì)垃圾輸入的檢查也進(jìn)行了適當(dāng)?shù)奶?br />理。進(jìn)程死亡,超乎尋常的慢響應(yīng),或者別的非預(yù)期的行為都進(jìn)行了處理。
spawn passwd [index $argv 1]
expect eof {exit 1}
timeout {exit 2}
"*No such user.*" {exit 3}
"*New password:"
send "[index $argv 2 "
expect eof {exit 4}
timeout {exit 2}
"*Password too long*" {exit 5}
"*Password too short*" {exit 5}
"*Retype ew password:"
send "[index $argv 3] "
expect timeout {exit 2}
"*Mismatch*" {exit 6}
"*Password unchanged*" {exit 7}
" "
expect timeout {exit 2}
"*" {exit 6}
eof
這個(gè)腳本退出時(shí)用一個(gè)數(shù)字來(lái)表示所發(fā)生的情況。0表示passwd程序正常
運(yùn)行,1表示非預(yù)期的死亡,2表示鎖定,等等。使用數(shù)字是為了簡(jiǎn)單起見(jiàn)。
expect返回字符串和返回?cái)?shù)字是一樣簡(jiǎn)單的,即使是派生程序自身產(chǎn)生的消息
也是一樣的。實(shí)際上,典型的做法是把整個(gè)交互的過(guò)程存到一個(gè)文件里面,只
有當(dāng)程序的運(yùn)行和預(yù)期一樣的時(shí)候才把這個(gè)文件刪除。否則這個(gè)log被留待以
后進(jìn)一步的檢查。
這個(gè)passwd檢查腳本被設(shè)計(jì)成由別的腳本來(lái)驅(qū)動(dòng)。這第二個(gè)腳本從一個(gè)文
件里面讀取參數(shù)和預(yù)期的結(jié)果。對(duì)于每一個(gè)輸入?yún)?shù)集,它調(diào)用第一個(gè)腳本并
且把結(jié)果和預(yù)期的結(jié)果相比較。(因?yàn)檫@個(gè)任務(wù)是非交互的,一個(gè)普通的老式
shell就可以用來(lái)解釋第二個(gè)腳本)。比如說(shuō),一個(gè)passwd的數(shù)據(jù)文件很有可能
就象下面一樣。
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 -
第一個(gè)域的名字是要被運(yùn)行的回歸腳本。第二個(gè)域是需要和結(jié)果相匹配的
退出值。第三個(gè)域就是用戶名。第四個(gè)域和第五個(gè)域就是提示時(shí)應(yīng)該輸入的密
碼。減號(hào)僅僅表示那里有一個(gè)域,這個(gè)域其實(shí)絕對(duì)不會(huì)用到。在第一個(gè)行中
,bogus表示用戶名是非法的,因此passwd會(huì)響應(yīng)說(shuō):沒(méi)有此用戶。expect在
退出時(shí)會(huì)返回3,3恰好就是第二個(gè)域。在最后一行中,^C就是被切實(shí)的送給程
序來(lái)驗(yàn)證程序是否恰當(dāng)?shù)耐顺觥?br />
通過(guò)這種方法,expect可以用來(lái)檢驗(yàn)和調(diào)試交互式軟件,這恰恰是IEEE的
POSIX 1003.2(shell和工具)的一致性檢驗(yàn)所要求的。進(jìn)一步的說(shuō)明請(qǐng)參考
Libes[6]。
7.[rogue 和偽終端]
Unix用戶肯定對(duì)通過(guò)管道來(lái)和其他進(jìn)程相聯(lián)系的方式非常的熟悉(比如說(shuō):
一個(gè)shell管道)。expect使用偽終端來(lái)和派生的進(jìn)程相聯(lián)系。偽終端提供了終
端語(yǔ)義以便程序認(rèn)為他們正在和真正的終端進(jìn)行I/O操作。
比如說(shuō),BSD的探險(xiǎn)游戲rogue在生模式下運(yùn)行,并假定在連接的另一端是
一個(gè)可尋址的字符終端。可以用expect編程,使得通過(guò)使用用戶界面可以玩這
個(gè)游戲。
rogue這個(gè)探險(xiǎn)游戲首先提供給你一個(gè)有各種物理屬性,比如說(shuō)力量值,的
角色。在大部分時(shí)間里,力量值都是16,但在幾乎每20次里面就會(huì)有一個(gè)力量
值是18。很多的rogue玩家都知道這一點(diǎn),但沒(méi)有人愿意啟動(dòng)程序20次以獲得一
個(gè)好的配置。下面的這個(gè)腳本就能達(dá)到這個(gè)目的。
for {} {1} {} {
spawn rogue
expect "*Str:18*" break
"*Str:16*"
close
wait
}
interact
第一行是個(gè)for循環(huán),和C語(yǔ)言的控制格式很象。rogue啟動(dòng)后,expect就
檢查看力量值是18還是16,如果是16,程序就通過(guò)執(zhí)行close和wait來(lái)退出。
這兩個(gè)命令的作用分別是關(guān)閉和偽終端的連接和等待進(jìn)程退出。rogue讀到一
個(gè)文件結(jié)束符就推出,從而循環(huán)繼續(xù)運(yùn)行,產(chǎn)生一個(gè)新的rogue游戲來(lái)檢查。
當(dāng)一個(gè)值為18的配置找到后,控制就推出循環(huán)并跳到最后一行腳本。
interact把控制轉(zhuǎn)移給用戶以便他們能夠玩這個(gè)特定的游戲。
想象一下這個(gè)腳本的運(yùn)行。你所能真正看到的就是20或者30個(gè)初始的配置
在不到一秒鐘的時(shí)間里掠過(guò)屏幕,最后留給你的就是一個(gè)有著很好配置的游戲
。唯一比這更好的方法就是使用調(diào)試工具來(lái)玩游戲。
我們很有必要認(rèn)識(shí)到這樣一點(diǎn):rogue是一個(gè)使用光標(biāo)的圖形游戲。
expect程序員必須了解到:光標(biāo)的運(yùn)動(dòng)并不一定以一種直觀的方式在屏幕上體
現(xiàn)。幸運(yùn)的是,在我們這個(gè)例子里,這不是一個(gè)問(wèn)題。將來(lái)的對(duì)expect的改
進(jìn)可能會(huì)包括一個(gè)內(nèi)嵌的能支持字符圖形區(qū)域的終端模擬器。
8.[ftp]
我們使用expect寫第一個(gè)腳本并沒(méi)有打印出"Hello,World"。實(shí)際上,它
實(shí)現(xiàn)了一些更有用的功能。它能通過(guò)非交互的方式來(lái)運(yùn)行ftp。ftp是用來(lái)在支
持TCP/IP的網(wǎng)絡(luò)上進(jìn)行文件傳輸?shù)某绦颉3艘恍┖?jiǎn)單的功能,一般的實(shí)現(xiàn)都
要求用戶的參與。
下面這個(gè)腳本從一個(gè)主機(jī)上使用匿名ftp取下一個(gè)文件來(lái)。其中,主機(jī)名
是第一個(gè)參數(shù)。文件名是第二個(gè)參數(shù)。
spawn ftp [index $argv 1]
expect "*Name*"
send "anonymous "
expect "*Password:*"
send [exec whoami]
expect "*ok*ftp>*"
send "get [index $argv 2] "
expect "*ftp>*"
上面這個(gè)程序被設(shè)計(jì)成在后臺(tái)進(jìn)行ftp。雖然他們?cè)诘讓邮褂煤蚭xpect類
似的機(jī)制,但他們的可編程能力留待改進(jìn)。因?yàn)閑xpect提供了高級(jí)語(yǔ)言,你可
以對(duì)它進(jìn)行修改來(lái)滿足你的特定需求。比如說(shuō),你可以加上以下功能:
:堅(jiān)持--如果連接或者傳輸失敗,你就可以每分鐘或者每小時(shí),甚
至可以根據(jù)其他因素,比如說(shuō)用戶的負(fù)載,來(lái)進(jìn)行不定期的
重試。
:通知--傳輸時(shí)可以通過(guò)mail,write或者其他程序來(lái)通知你,甚至
可以通知失敗。
:初始化-每一個(gè)用戶都可以有自己的用高級(jí)語(yǔ)言編寫的初始化文件
(比如說(shuō),.ftprc)。這和C shell對(duì).cshrc的使用很類似。
expect還可以執(zhí)行其他的更復(fù)雜的任務(wù)。比如說(shuō),他可以使用McGill大學(xué)
的Archie系統(tǒng)。Archie是一個(gè)匿名的Telnet服務(wù),它提供對(duì)描述Internet上可
通過(guò)匿名ftp獲取的文件的數(shù)據(jù)庫(kù)的訪問(wèn)。通過(guò)使用這個(gè)服務(wù),腳本可以詢問(wèn)
Archie某個(gè)特定的文件的位置,并把它從ftp服務(wù)器上取下來(lái)。這個(gè)功能的實(shí)
現(xiàn)只要求在上面那個(gè)腳本中加上幾行就可以。
現(xiàn)在還沒(méi)有什么已知的后臺(tái)-ftp能夠?qū)崿F(xiàn)上面的幾項(xiàng)功能,能不要說(shuō)所有
的功能了。在expect里面,它的實(shí)現(xiàn)卻是非常的簡(jiǎn)單。“堅(jiān)持”的實(shí)現(xiàn)只要求
在expect腳本里面加上一個(gè)循環(huán)。“通知”的實(shí)現(xiàn)只要執(zhí)行mail和write就可以
了。“初始化文件”的實(shí)現(xiàn)可以使用一個(gè)命令,source .ftprc,就可以了,
在.ftprc里面可以有任何的expect命令。
雖然這些特征可以通過(guò)在已有的程序里面加上鉤子函數(shù)就可以,但這也不
能保證每一個(gè)人的要求都能得到滿足。唯一能夠提供保證的方法就是提供一種
通用的語(yǔ)言。一個(gè)很好的解決方法就是把Tcl自身融入到ftp和其他的程序中間
去。實(shí)際上,這本來(lái)就是Tcl的初衷。在還沒(méi)有這樣做之前,expect提供了一
個(gè)能實(shí)現(xiàn)大部分功能但又不需要任何重寫的方案。
9.[fsck]
fsck是另外一個(gè)缺乏足夠的用戶接口的例子。fsck幾乎沒(méi)有提供什么方法
來(lái)預(yù)先的回答一些問(wèn)題。你能做的就是給所有的問(wèn)題都回答"yes"或者都回答
"no"。
下面的程序段展示了一個(gè)腳本如何的使的自動(dòng)的對(duì)某些問(wèn)題回答"yes",
而對(duì)某些問(wèn)題回答"no"。下面的這個(gè)腳本一開(kāi)始先派生fsck進(jìn)程,然后對(duì)其
中兩種類型的問(wèn)題回答"yes",而對(duì)其他的問(wèn)題回答"no"。
for {} {1} {} {
expect
eof break
"*UNREF FILE*CLEAR?" {send "r "}
"*BAD INODE*FIX?" {send "y "}
"*?" {send "n "}
}
在下面這個(gè)版本里面,兩個(gè)問(wèn)題的回答是不同的。而且,如果腳本遇到
了什么它不能理解的東西,就會(huì)執(zhí)行interact命令把控制交給用戶。用戶的
擊鍵直接交給fsck處理。當(dāng)執(zhí)行完后,用戶可以通過(guò)按"+"鍵來(lái)退出或者把
控制交還給expect。如果控制是交還給腳本了,腳本就會(huì)自動(dòng)的控制進(jìn)程的
剩余部分的運(yùn)行。
for {} {1} {}{
expect
eof break
"*UNREF FILE*CLEAR?" {send "y "}
"*BAD INODE*FIX?" {send "y "}
"*?" {interact +}
}
如果沒(méi)有expect,fsck只有在犧牲一定功能的情況下才可以非交互式的
運(yùn)行。fsck幾乎是不可編程的,但它卻是系統(tǒng)管理的最重要的工具。許多別
的工具的用戶接口也一樣的不足。實(shí)際上,正是其中的一些程序的不足導(dǎo)致
了expect的誕生。
10.[控制多個(gè)進(jìn)程:作業(yè)控制]
expect的作業(yè)控制概念精巧的避免了通常的實(shí)現(xiàn)困難。其中包括了兩個(gè)問(wèn)
題:一個(gè)是expect如何處理經(jīng)典的作業(yè)控制,即當(dāng)你在終端上按下^Z鍵時(shí)
expect如何處理;另外一個(gè)就是expect是如何處理多進(jìn)程的。
對(duì)第一個(gè)問(wèn)題的處理是:忽略它。expect對(duì)經(jīng)典的作業(yè)控制一無(wú)所知。比
如說(shuō),你派生了一個(gè)程序并且發(fā)送一個(gè)^Z給它,它就會(huì)停下來(lái)(這是偽終端的
完美之處)而expect就會(huì)永遠(yuǎn)的等下去。
但是,實(shí)際上,這根本就不成一個(gè)問(wèn)題。對(duì)于一個(gè)expect腳本,沒(méi)有必要
向進(jìn)程發(fā)送^Z。也就是說(shuō),沒(méi)有必要停下一個(gè)進(jìn)程來(lái)。expect僅僅是忽略了
一個(gè)進(jìn)程,而把自己的注意力轉(zhuǎn)移到其他的地方。這就是expect的作業(yè)控制
思想,這個(gè)思想也一直工作的很好。
從用戶的角度來(lái)看是象這樣的:當(dāng)一個(gè)進(jìn)程通過(guò)spawn命令啟動(dòng)時(shí),變量
spawn_id就被設(shè)置成某進(jìn)程的描述符。由spawn_id描述的進(jìn)程就被認(rèn)為是當(dāng)
前進(jìn)程。(這個(gè)描述符恰恰就是偽終端文件的描述符,雖然用戶把它當(dāng)作一個(gè)
不透明的物體)。expect和send命令僅僅和當(dāng)前進(jìn)程進(jìn)行交互。所以,切換一
個(gè)作業(yè)所需要做的僅僅是把該進(jìn)程的描述符賦給spawn_id。
這兒有一個(gè)例子向我們展示了如何通過(guò)作業(yè)控制來(lái)使兩個(gè)chess進(jìn)程進(jìn)行
交互。在派生完兩個(gè)進(jìn)程之后,一個(gè)進(jìn)程被通知先動(dòng)一步。在下面的循環(huán)里
面,每一步動(dòng)作都送給另外一個(gè)進(jìn)程。其中,read_move和write_move兩個(gè)過(guò)
程留給讀者來(lái)實(shí)現(xiàn)。(實(shí)際上,它們的實(shí)現(xiàn)非常的容易,但是,由于太長(zhǎng)了所
以沒(méi)有包含在這里)。
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
}
有一些應(yīng)用程序和chess程序不太一樣,在chess程序里,的兩個(gè)玩家
輪流動(dòng)。下面這個(gè)腳本實(shí)現(xiàn)了一個(gè)冒充程序。它能夠控制一個(gè)終端以便用戶
能夠登錄和正常的工作。但是,一旦系統(tǒng)提示輸入密碼或者輸入用戶名的時(shí)
候,expect就開(kāi)始把擊鍵記下來(lái),一直到用戶按下回車鍵。這有效的收集了
用戶的密碼和用戶名,還避免了普通的冒充程序的"Incorrect password-try
again"。而且,如果用戶連接到另外一個(gè)主機(jī)上,那些額外的登錄也會(huì)被
記錄下來(lái)。
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
}
}
這個(gè)腳本是這樣工作的。首先連接到一個(gè)login進(jìn)程和終端。缺省的,
所有的對(duì)話都記錄到標(biāo)準(zhǔn)輸出上(通過(guò)send_user)。因?yàn)槲覀儗?duì)此并不感興趣,
所以,我們通過(guò)命令"log_user 0"來(lái)禁止這個(gè)功能。(有很多的命令來(lái)控制
可以看見(jiàn)或者可以記錄的東西)。
在循環(huán)里面,select等待終端或者login進(jìn)程上的動(dòng)作,并且返回一個(gè)
等待輸入的spawn_id表。如果在表里面找到了一個(gè)值的話,case就執(zhí)行一個(gè)
action。比如說(shuō),如果字符串"login"出現(xiàn)在login進(jìn)程的輸出中,提示就會(huì)
被記錄到標(biāo)準(zhǔn)輸出上,并且有一個(gè)標(biāo)志被設(shè)置以便通知腳本開(kāi)始記錄用戶的
擊鍵,直至用戶按下了回車鍵。無(wú)論收到什么,都會(huì)回顯到終端上,一個(gè)相
應(yīng)的action會(huì)在腳本的終端那一部分執(zhí)行。
這些例子顯示了expect的作業(yè)控制方式。通過(guò)把自己插入到對(duì)話里面,
expect可以在進(jìn)程之間創(chuàng)建復(fù)雜的I/O流。可以創(chuàng)建多扇出,復(fù)用扇入的,
動(dòng)態(tài)的數(shù)據(jù)相關(guān)的進(jìn)程圖。
相比之下,shell使得它自己一次一行的讀取一個(gè)文件顯的很困難。
shell強(qiáng)迫用戶按下控制鍵(比如,^C,^Z)和關(guān)鍵字(比如fg和bg)來(lái)實(shí)現(xiàn)作業(yè)的
切換。這些都無(wú)法從腳本里面利用。相似的是:以非交互方式運(yùn)行的shell并
不處理“歷史記錄”和其他一些僅僅為交互式使用設(shè)計(jì)的特征。這也出現(xiàn)了和
前面哪個(gè)passwd程序的相似問(wèn)題。相似的,也無(wú)法編寫能夠回歸的測(cè)試shell
的某些動(dòng)作的shell腳本。結(jié)果導(dǎo)致shell的這些方面無(wú)法進(jìn)行徹底的測(cè)試。
如果使用expect的話,可以使用它的交互式的作業(yè)控制來(lái)驅(qū)動(dòng)shell。一
個(gè)派生的shell認(rèn)為它是在交互的運(yùn)行著,所以會(huì)正常的處理作業(yè)控制。它不
僅能夠解決檢驗(yàn)處理作業(yè)控制的shell和其他一些程序的問(wèn)題。還能夠在必要
的時(shí)候,讓shell代替expect來(lái)處理作業(yè)。可以支持使用shell風(fēng)格的作業(yè)控
制來(lái)支持進(jìn)程的運(yùn)行。這意味著:首先派生一個(gè)shell,然后把命令送給shell
來(lái)啟動(dòng)進(jìn)程。如果進(jìn)程被掛起,比如說(shuō),發(fā)送了一個(gè)^Z,進(jìn)程就會(huì)停下來(lái),并
把控制返回給shell。對(duì)于expect而言,它還在處理同一個(gè)進(jìn)程(原來(lái)那個(gè)
shell)。
expect的解決方法不僅具有很大的靈活性,它還避免了重復(fù)已經(jīng)存在于
shell中的作業(yè)控制軟件。通過(guò)使用shell,由于你可以選擇你想派生的shell,
所以你可以根據(jù)需要獲得作業(yè)控制權(quán)。而且,一旦你需要(比如說(shuō)檢驗(yàn)的時(shí)
候),你就可以驅(qū)動(dòng)一個(gè)shell來(lái)讓這個(gè)shell以為它正在交互式的運(yùn)行。這一
點(diǎn)對(duì)于在檢測(cè)到它們是否在交互式的運(yùn)行之后會(huì)改變輸出的緩沖的程序來(lái)說(shuō)也
是很重要的。
為了進(jìn)一步的控制,在interact執(zhí)行期間,expect把控制終端(是啟動(dòng)
expect的那個(gè)終端,而不是偽終端)設(shè)置成生模式以便字符能夠正確的傳送給
派生的進(jìn)程。當(dāng)expect在沒(méi)有執(zhí)行interact的時(shí)候,終端處于熟模式下,這時(shí)
候作業(yè)控制就可以作用于expect本身。
11.[交互式的使用expect]
在前面,我們提到可以通過(guò)interact命令來(lái)交互式的使用腳本。基本上
來(lái)說(shuō),interact命令提供了對(duì)對(duì)話的自由訪問(wèn),但我們需要一些更精細(xì)的控
制。這一點(diǎn),我們也可以使用expect來(lái)達(dá)到,因?yàn)閑xpect從標(biāo)準(zhǔn)輸入中讀取
輸入和從進(jìn)程中讀取輸入一樣的簡(jiǎn)單。 但是,我們要使用expect_user和
send_user來(lái)進(jìn)行標(biāo)準(zhǔn)I/O,同時(shí)不改變spawn_id。
下面的這個(gè)腳本在一定的時(shí)間內(nèi)從標(biāo)準(zhǔn)輸入里面讀取一行。這個(gè)腳本叫
做timed_read,可以從csh里面調(diào)用,比如說(shuō),set answer="timed_read 30"
就能調(diào)用它。
#!/usr/local/bin/expect -f
set timeout [index $argv 1]
expect_user "* "
send_user $expect_match
第三行從用戶那里接收任何以新行符結(jié)束的任何一行。最后一行把它
返回給標(biāo)準(zhǔn)輸出。如果在特定的時(shí)間內(nèi)沒(méi)有得到任何鍵入,則返回也為空。
第一行支持"#!"的系統(tǒng)直接的啟動(dòng)腳本。(如果把腳本的屬性加上可執(zhí)
行屬性則不要在腳本前面加上expect)。當(dāng)然了腳本總是可以顯式的用
"expect scripot"來(lái)啟動(dòng)。在-c后面的選項(xiàng)在任何腳本語(yǔ)句執(zhí)行前就被執(zhí)行。
比如說(shuō),不要修改腳本本身,僅僅在命令行上加上-c "trace...",該腳本可
以加上trace功能了(省略號(hào)表示trace的選項(xiàng))。
在命令行里實(shí)際上可以加上多個(gè)命令,只要中間以";"分開(kāi)就可以了。
比如說(shuō),下面這個(gè)命令行:
expect -c "set timeout 20;spawn foo;expect"
一旦你把超時(shí)時(shí)限設(shè)置好而且程序啟動(dòng)之后,expect就開(kāi)始等待文件
結(jié)束符或者20秒的超時(shí)時(shí)限。 如果遇到了文件結(jié)束符(EOF),該程序就會(huì)停
下來(lái),然后expect返回。如果是遇到了超時(shí)的情況,expect就返回。在這兩
中情況里面,都隱式的殺死了當(dāng)前進(jìn)程。
如果我們不使用expect而來(lái)實(shí)現(xiàn)以上兩個(gè)例子的功能的話,我們還是可
以學(xué)習(xí)到很多的東西的。在這兩中情況里面,通常的解決方案都是fork另一個(gè)
睡眠的子進(jìn)程并且用signal通知原來(lái)的shell。如果這個(gè)過(guò)程或者讀先發(fā)生的
話,shell就會(huì)殺司那個(gè)睡眠的進(jìn)程。 傳遞pid和防止后臺(tái)進(jìn)程產(chǎn)生啟動(dòng)信息
是一個(gè)讓除了高手級(jí)shell程序員之外的人頭痛的事情。提供一個(gè)通用的方法
來(lái)象這樣啟動(dòng)多個(gè)進(jìn)程會(huì)使shell腳本非常的復(fù)雜。 所以幾乎可以肯定的是,
程序員一般都用一個(gè)專門C程序來(lái)解決這樣一個(gè)問(wèn)題。
expect_user,send_user,send_error(向標(biāo)準(zhǔn)錯(cuò)誤終端輸出)在比較長(zhǎng)
的,用來(lái)把從進(jìn)程來(lái)的復(fù)雜交互翻譯成簡(jiǎn)單交互的expect腳本里面使用的比較
頻繁。在參考[7]里面,Libs描述怎樣用腳本來(lái)安全的包裹(wrap)adb,怎樣
把系統(tǒng)管理員從需要掌握adb的細(xì)節(jié)里面解脫出來(lái),同時(shí)大大的降低了由于錯(cuò)
誤的擊鍵而導(dǎo)致的系統(tǒng)崩潰。
一個(gè)簡(jiǎn)單的例子能夠讓ftp自動(dòng)的從一個(gè)私人的帳號(hào)里面取文件。在這
種情況里,要求提供密碼。 即使文件的訪問(wèn)是受限的,你也應(yīng)該避免把密碼
以明文的方式存儲(chǔ)在文件里面。把密碼作為腳本運(yùn)行時(shí)的參數(shù)也是不合適的,
因?yàn)橛胮s命令能看到它們。有一個(gè)解決的方法就是在腳本運(yùn)行的開(kāi)始調(diào)用
expect_user來(lái)讓用戶輸入以后可能使用的密碼。這個(gè)密碼必須只能讓這個(gè)腳
本知道,即使你是每個(gè)小時(shí)都要重試ftp。
即使信息是立即輸入進(jìn)去的,這個(gè)技巧也是非常有用。比如說(shuō),你可
以寫一個(gè)腳本,把你每一個(gè)主機(jī)上不同的帳號(hào)上的密碼都改掉,不管他們使用
的是不是同一個(gè)密碼數(shù)據(jù)庫(kù)。如果你要手工達(dá)到這樣一個(gè)功能的話,你必須
Telnet到每一個(gè)主機(jī)上,并且手工輸入新的密碼。而使用expect,你可以只輸
入密碼一次而讓腳本來(lái)做其它的事情。
expect_user和interact也可以在一個(gè)腳本里面混合的使用。考慮一下
在調(diào)試一個(gè)程序的循環(huán)時(shí),經(jīng)過(guò)好多步之后才失敗的情況。一個(gè)expect腳本
可以驅(qū)動(dòng)哪個(gè)調(diào)試器,設(shè)置好斷點(diǎn),執(zhí)行該程序循環(huán)的若干步,然后將控制
返回給鍵盤。它也可以在返回控制之前,在循環(huán)體和條件測(cè)試之間來(lái)回的切
換。