創建時間:2001-04-29
文章屬性:轉載
文章來源:中國科大BBS站
文章提交:quack (quack_at_xfocus.org)
[版權聲明]
Copyright(c) 1999
本教程由*葫蘆娃*翻譯,並做了適當的修改,可以自由的用於非商業目的。
但Redistribution時必須拷貝本[版權聲明]。
[BUG]
有不少部分,翻譯的時候不能作到“信,達”。當然了,任何時候都沒有做到“雅”,希望各位諒解。
[原著]
Don Libes: National Institute of Standards and Technology
libes@cme.nist.gov
[目錄]
1.摘要
2.關鍵字
3.簡介
4.Expect綜述
5.callback
6.passwd 和一致性檢查
7.rogue 和偽終端
8.ftp
9.fsck
10.多進程控制:作業控制
11.互動式使用Expect
12.互動式Expect程式設計
13.非互動式程式的控制
14.Expect的速度
15.安全方面的考慮
16.Expect資源
17.參考書籍
1.[摘要]
現代的Shell對程式提供了最小限度的控制(開始,停止,等等),而把交互的特性留給了用戶。 這意味著有些程式,你不能非交互的運行,比如說passwd。 有一些程式可以非交互的運行,但在很大程度上喪失了靈活性,比如說fsck。這表明Unix的工具構造邏輯開始出現問題。Expect恰恰填補了其中的一些裂痕,解決了在Unix環境中長期存在著的一些問題。
Expect使用Tcl作為語言核心。不僅如此,不管程式是交互和還是非交互的,Expect都能運用。這是一個小語言和Unix的其他工具配合起來產生強大功能的經典例子。
本部分教程並不是有關Expect的實現,而是關於Expect語言本身的使用,這主要也是通過不同的腳本描述例子來體現。其中的幾個例子還例證了Expect的幾個新特徵。
2.[關鍵字]
Expect,交互,POSIX,程式化的對話,Shell,Tcl,Unix;
3.[簡介]
一個叫做fsck的Unix檔案系統檢查程式,可以從Shell裡面用-y或者-n選項來執行。 在手冊[1]裡面,-y選項的定義是象這樣的。
“對於fsck的所有問題都假定一個“yes”回應;在這樣使用的時候,必須特別的小心,因為它實際上允許程式無條件的繼續運行,即使是遇到了一些非常嚴重的錯誤”
相比之下,-n選項就安全的多,但它實際上幾乎一點用都沒有。這種介面非常的糟糕,但是卻有許多的程式都是這種風格。 檔案傳輸程式ftp有一個選項可以禁止互動式的提問,以便能從一個腳本裡面運行。但一旦發生了錯誤,它沒有提供的處理措施。
Expect是一個控制互動式程式的工具。他解決了fsck的問題,用非交互的方式實現了所有互動式的功能。Expect不是特別為fsck設計的,它也能進行類似ftp的出錯處理。
fsck和ftp的問題向我們展示了象sh,csh和別的一些shell提供的使用者介面的局限性。 Shell沒有提供從一個程式讀和象一個程式寫的功能。這意味著shell可以運行fsck但只能以犧牲一部分fsck的靈活性做代價。有一些程式根本就不能被執行。比如說,如果沒有一個使用者介面互動式的提供輸入,就沒法運行下去。其他還有象Telnet,crypt,su,rlogin等程式無法在shell腳本裡面自動執行。還有很多其他的應用程式在設計是也是要求用戶輸入的。
Expect被設計成專門針和互動式程式的交互。一個Expect程式師可以寫一個腳本來描述程式和使用者的對話。接著Expect程式可以非交互的運行“互動式”的程式。寫互動式程式的腳本和寫非互動式程式的腳本一樣簡單。Expect還可以用於對對話的一部分進行自動化,因為程式的控制可以在鍵盤和腳本之間進行切換。
bes[2]裡面有詳細的描述。簡單的說,腳本是用一種解釋性語言寫的。(也有C和C++的Expect庫可供使用,但這超出了本文的範圍).Expect提供了創建互動式進程和讀寫它們的輸入和輸出的命令。 Expect是由於它的一個同名的命令而命名的。
Expect語言是基於Tcl的。Tcl實際上是一個子程式庫,這些副程式庫可以嵌入到程式裡從而提供語言服務。 最終的語言有點象一個典型的Shell語言。裡面有給變數賦值的set命令,控制程式執行的if,for,continue等命令,還能進行普通的數學和字串操作。當然了,還可以用exec來調用Unix程式。所有這些功能,Tcl都有。Tcl在參考書籍 Outerhour[3][4]裡有詳細的描述。
Expect是在Tcl基礎上創建起來的,它還提供了一些Tcl所沒有的命令。spawn命令啟動一個Unix程式來進行互動式的運行。 send命令向進程發送字串。expect命令等待進程的某些字串。 expect支援正規運算式並能同時等待多個字串,並對每一個字串執行不同的操作。expect還能理解一些特殊情況,如超時和遇到檔案結尾。
expect命令和Tcl的case命令的風格很相似。都是用一個字串去匹配多個字串。(只要有可能,新的命令總是和已有的Tcl命令相似,以使得該語言保持工具族的繼承性)。下面關於expect的定義是從手冊[5]上摘錄下來的。
expect patlist1 action1 patlist2 action2.....
該命令一直等到當前進程的輸出和以上的某一個模式相匹配,或者等 到時間超過一個特定的時間長度,或者等到遇到了檔的結束為止。
如果最後一個action是空的,就可以省略它。
每一個patlist都由一個模式或者模式的表(lists)組成。如果有一個模式匹配成功,相應的action就被執行。執行的結果從expect返回。
被精確匹配的字串(或者當超時發生時,已經讀取但未進行匹配的字串)被存貯在變數expect_match裡面。如果patlist是eof或者timeout,則發生檔結束或者超時時才執行相應的action.一般超時的時值是10秒,但可以用類似"set timeout 30"之類的命令把超時時值設定為30秒。
下面的一個程式段是從一個有關登錄的腳本裡面摘取的。abort是在腳本的別處定義的過程,而其他的action使用類似與C語言的Tcl原語。
expect "*welcome*" break
"*busy*" {print busy;continue}
"*failed*" abort
timeout abort
模式是通常的C Shell風格的正規運算式。模式必須匹配當前進程的從上一個expect或者interact開始的所有輸出(所以統配符*使用的非常)的普遍。但是,一旦輸出超過2000個位元組,前面的字元就會被忘記,這可以通過設定match_max的值來改變。
expect命令確實體現了expect語言的最好和最壞的性質。特別是,expect命令的靈活性是以經常出現令人迷惑的語法做代價。除了關鍵字模式(比如說eof,timeout)那些模式表可以包括多個模式。這保證提供了一種方法來區分他們。但是分開這些表需要額外的掃描,如果沒有恰當的用["]括起來,這有可能會把和當成空白字元。由於Tcl提供了兩種字串引用的方法:單引和雙引,情況變的更糟。(在Tcl裡面,如果不會出現二義性話,沒有必要使用引號)。在expect的手冊裡面,還有一個獨立的部分來解釋這種複雜性。幸運的是:有一些很好的例子似乎阻止了這種抱怨。但是,這個複雜性很有可能在將來的版本中再度出現。為了增強可讀性,在本文中,提供的腳本都假定雙引號是足夠的。
字元可以使用反斜線來單獨的引用,反斜線也被用於對語句的延續,如果不加反斜線的話,語句到一行的結尾處就結束了。這和Tcl也是一致的。Tcl在發現有開的單引號或者開的雙引號時都會繼續掃描。而且,分號可以用於在一行中分割多個語句。這乍聽起來有點讓人困惑,但是,這是解釋性語言的風格,但是,這確實是Tcl的不太漂亮的部分。
5.[callback]
令人非常驚訝的是,一些小的腳本如何的產生一些有用的功能。下面是一個撥電話號碼的腳本。他用來把收費反向,以便使得長途電話對電腦計費。這個腳本用類似“expect callback.exp 12016442332”來啟動。其中,腳本的名字便是callback.exp,而 +1(201)644-2332 是要撥的電話號碼。
#first give the user some time to logout
exec sleep 4
spawn tip modem
expect "*connected*"
send "ATD [llindex $argv 1] "
#modem takes a while to connect
set timeout 60
expect "*CONNECT*"
第一行是注釋,第二行展示了如何調用沒有交互的Unix程式。sleep 4會使程式阻塞4秒,以使得用戶有時間來退出,因為modem總是會回叫用戶已經使用的電話號碼。
下面一行使用spawn命令來啟動tip程式,以便使得tip的輸出能夠被expect所讀取,使得tip能從send讀輸入。一旦tip說它已經連接上,modem就會要求去撥打大哥電話號碼。(假定modem都是賀氏相容的,但是本腳本可以很容易的修改成能適應別的類型的modem)。不論發生了什麼,expect都會終止。如果呼叫失敗,expect腳本可以設計成進行重試,但這裡沒有。如果呼叫成功,getty會在expect退出後檢測到DTR,並且向用戶提示loging:。(實用的腳本往往提供更多的錯誤檢測)。
這個腳本展示了命令列參數的使用,命令列參數存貯在一個叫做argv的表裡面(這和C語言的風格很象)。在這種情況下,第一個元素就是電話號碼。方括號使得被括起來的部分當作命令來執行,結果就替換被括起來的部分。這也和C Shell的風格很象。
這個腳本和一個大約60K的C語言程式實現的功能相似。
6.[passwd和一致性檢查]
在前面,我們提到passwd程式在缺乏使用者交互的情況下,不能運行,passwd會忽略I/O重定向,也不能嵌入到管道裡邊以便能從別的程式或者檔裡讀取輸入。這個程式堅持要求真正的與用戶進行交互。因為安全的原因,passwd被設計成這樣,但結果導致沒有非互動式的方法來檢驗passwd。這樣一個對系統安全至關重要的程式竟然沒有辦法進行可靠的檢驗,真實具有諷刺意味。
passwd以一個用戶名作為參數,互動式的提示輸入密碼。下面的expect腳本以用戶名和密碼作為參數而非互動式的運行。
spawn oasswd [lindex $argv 1]
set password [lindex $argv 2]
expect "*password:"
send "$password "
expect "*password:"
send "$password "
expect eof
第一行以用戶名做參數啟動passwd程式,為方便起見,第二行把密碼存到一個變數裡面。和shell類似,變數的使用也不需要提前聲明。
在第三行,expect搜索模式"*password:",其中*允許匹配任意輸入,所以對於避免指定所有細節而言是非常有效的。 上面的程式裡沒有action,所以expect檢測到該模式後就繼續運行。
一旦接收到提示後,下一行就就把密碼送給當前進程。表明回車。(實際上,所有的C的關於字元的約定都支援)。上面的程式中有兩個expect-send序列,因為passwd為了對輸入進行確認,要求進行兩次輸入。在非互動式程式裡面,這是毫無必要的,但由於假定passwd是在和用戶進行交互,所以我們的腳本還是這樣做了。
最後,"expect eof"這一行的作用是在passwd的輸出中搜索檔結束符,這一行語句還展示了關鍵字的匹配。另外一個關鍵字匹配就是timeout了,timeout被用於表示所有匹配的失敗而和一段特定長度的時間相匹配。在這裡eof是非常有必要的,因為passwd被設計成會檢查它的所有I/O是否都成功了,包括第二次輸入密碼時產生的最後一個新行。
這個腳本已經足夠展示passwd命令的基本交互性。另外一個更加完備的例子回檢查別的一些行為。比如說,下面的這個腳本就能檢查passwd程式的別的幾個方面。所有的提示都進行了檢查。對垃圾輸入的檢查也進行了適當的處理。進程死亡,超乎尋常的慢回應,或者別的非預期的行為都進行了處理。
spawn passwd [lindex $argv 1]
expect eof {exit 1}
timeout {exit 2}
"*No such user.*" {exit 3}
"*New password:"
send "[lindex $argv 2 "
expect eof {exit 4}
timeout {exit 2}
"*Password too long*" {exit 5}
"*Password too short*" {exit 5}
"*Retype ew password:"
send "[lindex $argv 3] "
expect timeout {exit 2}
"*Mismatch*" {exit 6}
"*Password unchanged*" {exit 7}
" "
expect timeout {exit 2}
"*" {exit 6}
eof
這個腳本退出時用一個數字來表示所發生的情況。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 [lindex $argv 1]
expect "*Name*"
send "anonymous "
expect "*Password:*"
send [exec whoami]
expect "*ok*ftp>*"
send "get [lindex $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
}
}
這個腳本是這樣工作的。首先連接到一個login進程和終端。缺省的,所有的對話都記錄到標準輸出上(通過send_user)。因為我們對此並不感興趣,所以,我們通過命令"log_user 0"來禁止這個功能。(有很多的命令來控制可以看見或者可以記錄的東西)。
在迴圈裡面,select等待終端或者login進程上的動作,並且返回一個等待輸入的spawn_id表。如果在表裡面找到了一個值的話,case就執行一個action。比如說,如果字串"login"出現在login進程的輸出中,提示就會被記錄到標準輸出上,並且有一個標誌被設置以便通知腳本開始記錄使用者的擊鍵,直至用戶按下了回車鍵。無論收到什麼,都會回顯到終端上,一個相應的action會在腳本的終端那一部分執行。
這些例子顯示了expect的作業控制方式。通過把自己插入到對話裡面,expect可以在進程之間創建複雜的I/O流。可以創建多扇出,複用扇入的,動態的資料相關的進程圖。
相比之下,shell使得它自己一次一行的讀取一個檔顯的很困難。shell強迫用戶按下控制鍵(比如,^C,^Z)和關鍵字(比如fg和bg)來實現作業的切換。這些都無法從腳本裡面利用。相似的是:以非對話模式運行的shell並不處理“歷史記錄”和其他一些僅僅為互動式使用設計的特徵。這也出現了和前面哪個passwd程式的相似問題。相似的,也無法編寫能夠回歸的測試shell的某些動作的shell腳本。結果導致shell的這些方面無法進行徹底的測試。
如果使用expect的話,可以使用它的互動式的作業控制來驅動shell。一個派生的shell認為它是在交互的運行著,所以會正常的處理作業控制。它不僅能夠解決檢驗處理作業控制的shell和其他一些程式的問題。還能夠在必要的時候,讓shell代替expect來處理作業。可以支援使用shell風格的作業控制來支援進程的運行。這意味著:首先派生一個shell,然後把命令送給shell來啟動進程。如果進程被掛起,比如說,發送了一個^Z,進程就會停下來,並把控制返回給shell。對於expect而言,它還在處理同一個進程(原來那個shell)。
expect的解決方法不僅具有很大的靈活性,它還避免了重複已經存在於shell中的作業控制軟體。通過使用shell,由於你可以選擇你想派生的shell,所以你可以根據需要獲得作業控制權。而且,一旦你需要(比如說檢驗的時候),你就可以驅動一個shell來讓這個shell以為它正在互動式的運行。這一點對於在檢測到它們是否在互動式的運行之後會改變輸出的緩衝的程式來說也是很重要的。
為了進一步的控制,在interact執行期間,expect把控制終端(是啟動expect的那個終端,而不是偽終端)設置成生模式以便字元能夠正確的傳送給派生的進程。當expect在沒有執行interact的時候,終端處於熟模式下,這時候作業控制就可以作用於expect本身。
11.[互動式的使用expect]
在前面,我們提到可以通過interact命令來互動式的使用腳本。基本上來說,interact命令提供了對對話的自由訪問,但我們需要一些更精細的控制。這一點,我們也可以使用expect來達到,因為expect從標準輸入中讀取輸入和從進程中讀取輸入一樣的簡單。 但是,我們要使用expect_user和send_user來進行標準I/O,同時不改變spawn_id。
下面的這個腳本在一定的時間內從標準輸入裡面讀取一行。這個腳本叫做timed_read,可以從csh裡面調用,比如說,set answer="timed_read 30"就能調用它。
#!/usr/local/bin/expect -f
set timeout [lindex $argv 1]
expect_user "* "
send_user $expect_match
第三行從用戶那裡接收任何以新行符結束的任何一行。最後一行把它返回給標準輸出。如果在特定的時間內沒有得到任何鍵入,則返回也為空。
第一行支持"#!"的系統直接的啟動腳本。(如果把腳本的屬性加上可執行屬性則不要在腳本前面加上expect)。當然了腳本總是可以顯式的用"expect scripot"來啟動。在-c後面的選項在任何腳本語句執行前就被執行。比如說,不要修改腳本本身,僅僅在命令列上加上-c "trace...",該腳本可以加上trace功能了(省略號表示trace的選項)。
在命令列裡實際上可以加上多個命令,只要中間以";"分開就可以了。比如說,下面這個命令列:
expect -c "set timeout 20;spawn foo;expect"
一旦你把超時時限設置好而且程式啟動之後,expect就開始等待檔結束符或者20秒的超時時限。 如果遇到了檔結束符(EOF),該程式就會停下來,然後expect返回。如果是遇到了超時的情況,expect就返回。在這兩中情況裡面,都隱式的殺死了當前進程。
如果我們不使用expect而來實現以上兩個例子的功能的話,我們還是可以學習到很多的東西的。在這兩中情況裡面,通常的解決方案都是fork另一個睡眠的子進程並且用signal通知原來的shell。如果這個過程或者讀先發生的話,shell就會殺司那個睡眠的進程。 傳遞pid和防止後臺進程產生啟動資訊是一個讓除了高手級shell程式師之外的人頭痛的事情。提供一個通用的方法來象這樣啟動多個進程會使shell腳本非常的複雜。 所以幾乎可以肯定的是,程式師一般都用一個專門C程式來解決這樣一個問題。
expect_user,send_user,send_error(向標準錯誤終端輸出)在比較長的,用來把從進程來的複雜交互翻譯成簡單交互的expect腳本裡面使用的比較頻繁。在參考[7]裡面,Libs描述怎樣用腳本來安全的包裹(wrap)adb,怎樣把系統管理員從需要掌握adb的細節裡面解脫出來,同時大大的降低了由於錯誤的擊鍵而導致的系統崩潰。
一個簡單的例子能夠讓ftp自動的從一個私人的帳號裡面取檔。在這種情況裡,要求提供密碼。 即使檔的訪問是受限的,你也應該避免把密碼以明文的方式存儲在檔裡面。把密碼作為腳本運行時的參數也是不合適的,因為用ps命令能看到它們。有一個解決的方法就是在腳本運行的開始調用expect_user來讓用戶輸入以後可能使用的密碼。這個密碼必須只能讓這個腳本知道,即使你是每個小時都要重試ftp。
即使資訊是立即輸入進去的,這個技巧也是非常有用。比如說,你可以寫一個腳本,把你每一個主機上不同的帳號上的密碼都改掉,不管他們使用的是不是同一個密碼資料庫。如果你要手工達到這樣一個功能的話,你必須Telnet到每一個主機上,並且手工輸入新的密碼。而使用expect,你可以只輸入密碼一次而讓腳本來做其它的事情。
expect_user和interact也可以在一個腳本裡面混合的使用。考慮一下在調試一個程式的迴圈時,經過好多步之後才失敗的情況。一個expect腳本可以驅動哪個調試器,設置好中斷點,執行該程式迴圈的若干步,然後將控制返回給鍵盤。它也可以在返回控制之前,在循環體和條件測試之間來回的切換。
文章來源:中國科大BBS站
文章提交:quack (quack_at_xfocus.org)
[版權聲明]
Copyright(c) 1999
本教程由*葫蘆娃*翻譯,並做了適當的修改,可以自由的用於非商業目的。
但Redistribution時必須拷貝本[版權聲明]。
[BUG]
有不少部分,翻譯的時候不能作到“信,達”。當然了,任何時候都沒有做到“雅”,希望各位諒解。
[原著]
Don Libes: National Institute of Standards and Technology
libes@cme.nist.gov
[目錄]
1.摘要
2.關鍵字
3.簡介
4.Expect綜述
5.callback
6.passwd 和一致性檢查
7.rogue 和偽終端
8.ftp
9.fsck
10.多進程控制:作業控制
11.互動式使用Expect
12.互動式Expect程式設計
13.非互動式程式的控制
14.Expect的速度
15.安全方面的考慮
16.Expect資源
17.參考書籍
1.[摘要]
現代的Shell對程式提供了最小限度的控制(開始,停止,等等),而把交互的特性留給了用戶。 這意味著有些程式,你不能非交互的運行,比如說passwd。 有一些程式可以非交互的運行,但在很大程度上喪失了靈活性,比如說fsck。這表明Unix的工具構造邏輯開始出現問題。Expect恰恰填補了其中的一些裂痕,解決了在Unix環境中長期存在著的一些問題。
Expect使用Tcl作為語言核心。不僅如此,不管程式是交互和還是非交互的,Expect都能運用。這是一個小語言和Unix的其他工具配合起來產生強大功能的經典例子。
本部分教程並不是有關Expect的實現,而是關於Expect語言本身的使用,這主要也是通過不同的腳本描述例子來體現。其中的幾個例子還例證了Expect的幾個新特徵。
2.[關鍵字]
Expect,交互,POSIX,程式化的對話,Shell,Tcl,Unix;
3.[簡介]
一個叫做fsck的Unix檔案系統檢查程式,可以從Shell裡面用-y或者-n選項來執行。 在手冊[1]裡面,-y選項的定義是象這樣的。
“對於fsck的所有問題都假定一個“yes”回應;在這樣使用的時候,必須特別的小心,因為它實際上允許程式無條件的繼續運行,即使是遇到了一些非常嚴重的錯誤”
相比之下,-n選項就安全的多,但它實際上幾乎一點用都沒有。這種介面非常的糟糕,但是卻有許多的程式都是這種風格。 檔案傳輸程式ftp有一個選項可以禁止互動式的提問,以便能從一個腳本裡面運行。但一旦發生了錯誤,它沒有提供的處理措施。
Expect是一個控制互動式程式的工具。他解決了fsck的問題,用非交互的方式實現了所有互動式的功能。Expect不是特別為fsck設計的,它也能進行類似ftp的出錯處理。
fsck和ftp的問題向我們展示了象sh,csh和別的一些shell提供的使用者介面的局限性。 Shell沒有提供從一個程式讀和象一個程式寫的功能。這意味著shell可以運行fsck但只能以犧牲一部分fsck的靈活性做代價。有一些程式根本就不能被執行。比如說,如果沒有一個使用者介面互動式的提供輸入,就沒法運行下去。其他還有象Telnet,crypt,su,rlogin等程式無法在shell腳本裡面自動執行。還有很多其他的應用程式在設計是也是要求用戶輸入的。
Expect被設計成專門針和互動式程式的交互。一個Expect程式師可以寫一個腳本來描述程式和使用者的對話。接著Expect程式可以非交互的運行“互動式”的程式。寫互動式程式的腳本和寫非互動式程式的腳本一樣簡單。Expect還可以用於對對話的一部分進行自動化,因為程式的控制可以在鍵盤和腳本之間進行切換。
bes[2]裡面有詳細的描述。簡單的說,腳本是用一種解釋性語言寫的。(也有C和C++的Expect庫可供使用,但這超出了本文的範圍).Expect提供了創建互動式進程和讀寫它們的輸入和輸出的命令。 Expect是由於它的一個同名的命令而命名的。
Expect語言是基於Tcl的。Tcl實際上是一個子程式庫,這些副程式庫可以嵌入到程式裡從而提供語言服務。 最終的語言有點象一個典型的Shell語言。裡面有給變數賦值的set命令,控制程式執行的if,for,continue等命令,還能進行普通的數學和字串操作。當然了,還可以用exec來調用Unix程式。所有這些功能,Tcl都有。Tcl在參考書籍 Outerhour[3][4]裡有詳細的描述。
Expect是在Tcl基礎上創建起來的,它還提供了一些Tcl所沒有的命令。spawn命令啟動一個Unix程式來進行互動式的運行。 send命令向進程發送字串。expect命令等待進程的某些字串。 expect支援正規運算式並能同時等待多個字串,並對每一個字串執行不同的操作。expect還能理解一些特殊情況,如超時和遇到檔案結尾。
expect命令和Tcl的case命令的風格很相似。都是用一個字串去匹配多個字串。(只要有可能,新的命令總是和已有的Tcl命令相似,以使得該語言保持工具族的繼承性)。下面關於expect的定義是從手冊[5]上摘錄下來的。
expect patlist1 action1 patlist2 action2.....
該命令一直等到當前進程的輸出和以上的某一個模式相匹配,或者等 到時間超過一個特定的時間長度,或者等到遇到了檔的結束為止。
如果最後一個action是空的,就可以省略它。
每一個patlist都由一個模式或者模式的表(lists)組成。如果有一個模式匹配成功,相應的action就被執行。執行的結果從expect返回。
被精確匹配的字串(或者當超時發生時,已經讀取但未進行匹配的字串)被存貯在變數expect_match裡面。如果patlist是eof或者timeout,則發生檔結束或者超時時才執行相應的action.一般超時的時值是10秒,但可以用類似"set timeout 30"之類的命令把超時時值設定為30秒。
下面的一個程式段是從一個有關登錄的腳本裡面摘取的。abort是在腳本的別處定義的過程,而其他的action使用類似與C語言的Tcl原語。
expect "*welcome*" break
"*busy*" {print busy;continue}
"*failed*" abort
timeout abort
模式是通常的C Shell風格的正規運算式。模式必須匹配當前進程的從上一個expect或者interact開始的所有輸出(所以統配符*使用的非常)的普遍。但是,一旦輸出超過2000個位元組,前面的字元就會被忘記,這可以通過設定match_max的值來改變。
expect命令確實體現了expect語言的最好和最壞的性質。特別是,expect命令的靈活性是以經常出現令人迷惑的語法做代價。除了關鍵字模式(比如說eof,timeout)那些模式表可以包括多個模式。這保證提供了一種方法來區分他們。但是分開這些表需要額外的掃描,如果沒有恰當的用["]括起來,這有可能會把和當成空白字元。由於Tcl提供了兩種字串引用的方法:單引和雙引,情況變的更糟。(在Tcl裡面,如果不會出現二義性話,沒有必要使用引號)。在expect的手冊裡面,還有一個獨立的部分來解釋這種複雜性。幸運的是:有一些很好的例子似乎阻止了這種抱怨。但是,這個複雜性很有可能在將來的版本中再度出現。為了增強可讀性,在本文中,提供的腳本都假定雙引號是足夠的。
字元可以使用反斜線來單獨的引用,反斜線也被用於對語句的延續,如果不加反斜線的話,語句到一行的結尾處就結束了。這和Tcl也是一致的。Tcl在發現有開的單引號或者開的雙引號時都會繼續掃描。而且,分號可以用於在一行中分割多個語句。這乍聽起來有點讓人困惑,但是,這是解釋性語言的風格,但是,這確實是Tcl的不太漂亮的部分。
5.[callback]
令人非常驚訝的是,一些小的腳本如何的產生一些有用的功能。下面是一個撥電話號碼的腳本。他用來把收費反向,以便使得長途電話對電腦計費。這個腳本用類似“expect callback.exp 12016442332”來啟動。其中,腳本的名字便是callback.exp,而 +1(201)644-2332 是要撥的電話號碼。
#first give the user some time to logout
exec sleep 4
spawn tip modem
expect "*connected*"
send "ATD [llindex $argv 1] "
#modem takes a while to connect
set timeout 60
expect "*CONNECT*"
第一行是注釋,第二行展示了如何調用沒有交互的Unix程式。sleep 4會使程式阻塞4秒,以使得用戶有時間來退出,因為modem總是會回叫用戶已經使用的電話號碼。
下面一行使用spawn命令來啟動tip程式,以便使得tip的輸出能夠被expect所讀取,使得tip能從send讀輸入。一旦tip說它已經連接上,modem就會要求去撥打大哥電話號碼。(假定modem都是賀氏相容的,但是本腳本可以很容易的修改成能適應別的類型的modem)。不論發生了什麼,expect都會終止。如果呼叫失敗,expect腳本可以設計成進行重試,但這裡沒有。如果呼叫成功,getty會在expect退出後檢測到DTR,並且向用戶提示loging:。(實用的腳本往往提供更多的錯誤檢測)。
這個腳本展示了命令列參數的使用,命令列參數存貯在一個叫做argv的表裡面(這和C語言的風格很象)。在這種情況下,第一個元素就是電話號碼。方括號使得被括起來的部分當作命令來執行,結果就替換被括起來的部分。這也和C Shell的風格很象。
這個腳本和一個大約60K的C語言程式實現的功能相似。
6.[passwd和一致性檢查]
在前面,我們提到passwd程式在缺乏使用者交互的情況下,不能運行,passwd會忽略I/O重定向,也不能嵌入到管道裡邊以便能從別的程式或者檔裡讀取輸入。這個程式堅持要求真正的與用戶進行交互。因為安全的原因,passwd被設計成這樣,但結果導致沒有非互動式的方法來檢驗passwd。這樣一個對系統安全至關重要的程式竟然沒有辦法進行可靠的檢驗,真實具有諷刺意味。
passwd以一個用戶名作為參數,互動式的提示輸入密碼。下面的expect腳本以用戶名和密碼作為參數而非互動式的運行。
spawn oasswd [lindex $argv 1]
set password [lindex $argv 2]
expect "*password:"
send "$password "
expect "*password:"
send "$password "
expect eof
第一行以用戶名做參數啟動passwd程式,為方便起見,第二行把密碼存到一個變數裡面。和shell類似,變數的使用也不需要提前聲明。
在第三行,expect搜索模式"*password:",其中*允許匹配任意輸入,所以對於避免指定所有細節而言是非常有效的。 上面的程式裡沒有action,所以expect檢測到該模式後就繼續運行。
一旦接收到提示後,下一行就就把密碼送給當前進程。表明回車。(實際上,所有的C的關於字元的約定都支援)。上面的程式中有兩個expect-send序列,因為passwd為了對輸入進行確認,要求進行兩次輸入。在非互動式程式裡面,這是毫無必要的,但由於假定passwd是在和用戶進行交互,所以我們的腳本還是這樣做了。
最後,"expect eof"這一行的作用是在passwd的輸出中搜索檔結束符,這一行語句還展示了關鍵字的匹配。另外一個關鍵字匹配就是timeout了,timeout被用於表示所有匹配的失敗而和一段特定長度的時間相匹配。在這裡eof是非常有必要的,因為passwd被設計成會檢查它的所有I/O是否都成功了,包括第二次輸入密碼時產生的最後一個新行。
這個腳本已經足夠展示passwd命令的基本交互性。另外一個更加完備的例子回檢查別的一些行為。比如說,下面的這個腳本就能檢查passwd程式的別的幾個方面。所有的提示都進行了檢查。對垃圾輸入的檢查也進行了適當的處理。進程死亡,超乎尋常的慢回應,或者別的非預期的行為都進行了處理。
spawn passwd [lindex $argv 1]
expect eof {exit 1}
timeout {exit 2}
"*No such user.*" {exit 3}
"*New password:"
send "[lindex $argv 2 "
expect eof {exit 4}
timeout {exit 2}
"*Password too long*" {exit 5}
"*Password too short*" {exit 5}
"*Retype ew password:"
send "[lindex $argv 3] "
expect timeout {exit 2}
"*Mismatch*" {exit 6}
"*Password unchanged*" {exit 7}
" "
expect timeout {exit 2}
"*" {exit 6}
eof
這個腳本退出時用一個數字來表示所發生的情況。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 [lindex $argv 1]
expect "*Name*"
send "anonymous "
expect "*Password:*"
send [exec whoami]
expect "*ok*ftp>*"
send "get [lindex $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
}
}
這個腳本是這樣工作的。首先連接到一個login進程和終端。缺省的,所有的對話都記錄到標準輸出上(通過send_user)。因為我們對此並不感興趣,所以,我們通過命令"log_user 0"來禁止這個功能。(有很多的命令來控制可以看見或者可以記錄的東西)。
在迴圈裡面,select等待終端或者login進程上的動作,並且返回一個等待輸入的spawn_id表。如果在表裡面找到了一個值的話,case就執行一個action。比如說,如果字串"login"出現在login進程的輸出中,提示就會被記錄到標準輸出上,並且有一個標誌被設置以便通知腳本開始記錄使用者的擊鍵,直至用戶按下了回車鍵。無論收到什麼,都會回顯到終端上,一個相應的action會在腳本的終端那一部分執行。
這些例子顯示了expect的作業控制方式。通過把自己插入到對話裡面,expect可以在進程之間創建複雜的I/O流。可以創建多扇出,複用扇入的,動態的資料相關的進程圖。
相比之下,shell使得它自己一次一行的讀取一個檔顯的很困難。shell強迫用戶按下控制鍵(比如,^C,^Z)和關鍵字(比如fg和bg)來實現作業的切換。這些都無法從腳本裡面利用。相似的是:以非對話模式運行的shell並不處理“歷史記錄”和其他一些僅僅為互動式使用設計的特徵。這也出現了和前面哪個passwd程式的相似問題。相似的,也無法編寫能夠回歸的測試shell的某些動作的shell腳本。結果導致shell的這些方面無法進行徹底的測試。
如果使用expect的話,可以使用它的互動式的作業控制來驅動shell。一個派生的shell認為它是在交互的運行著,所以會正常的處理作業控制。它不僅能夠解決檢驗處理作業控制的shell和其他一些程式的問題。還能夠在必要的時候,讓shell代替expect來處理作業。可以支援使用shell風格的作業控制來支援進程的運行。這意味著:首先派生一個shell,然後把命令送給shell來啟動進程。如果進程被掛起,比如說,發送了一個^Z,進程就會停下來,並把控制返回給shell。對於expect而言,它還在處理同一個進程(原來那個shell)。
expect的解決方法不僅具有很大的靈活性,它還避免了重複已經存在於shell中的作業控制軟體。通過使用shell,由於你可以選擇你想派生的shell,所以你可以根據需要獲得作業控制權。而且,一旦你需要(比如說檢驗的時候),你就可以驅動一個shell來讓這個shell以為它正在互動式的運行。這一點對於在檢測到它們是否在互動式的運行之後會改變輸出的緩衝的程式來說也是很重要的。
為了進一步的控制,在interact執行期間,expect把控制終端(是啟動expect的那個終端,而不是偽終端)設置成生模式以便字元能夠正確的傳送給派生的進程。當expect在沒有執行interact的時候,終端處於熟模式下,這時候作業控制就可以作用於expect本身。
11.[互動式的使用expect]
在前面,我們提到可以通過interact命令來互動式的使用腳本。基本上來說,interact命令提供了對對話的自由訪問,但我們需要一些更精細的控制。這一點,我們也可以使用expect來達到,因為expect從標準輸入中讀取輸入和從進程中讀取輸入一樣的簡單。 但是,我們要使用expect_user和send_user來進行標準I/O,同時不改變spawn_id。
下面的這個腳本在一定的時間內從標準輸入裡面讀取一行。這個腳本叫做timed_read,可以從csh裡面調用,比如說,set answer="timed_read 30"就能調用它。
#!/usr/local/bin/expect -f
set timeout [lindex $argv 1]
expect_user "* "
send_user $expect_match
第三行從用戶那裡接收任何以新行符結束的任何一行。最後一行把它返回給標準輸出。如果在特定的時間內沒有得到任何鍵入,則返回也為空。
第一行支持"#!"的系統直接的啟動腳本。(如果把腳本的屬性加上可執行屬性則不要在腳本前面加上expect)。當然了腳本總是可以顯式的用"expect scripot"來啟動。在-c後面的選項在任何腳本語句執行前就被執行。比如說,不要修改腳本本身,僅僅在命令列上加上-c "trace...",該腳本可以加上trace功能了(省略號表示trace的選項)。
在命令列裡實際上可以加上多個命令,只要中間以";"分開就可以了。比如說,下面這個命令列:
expect -c "set timeout 20;spawn foo;expect"
一旦你把超時時限設置好而且程式啟動之後,expect就開始等待檔結束符或者20秒的超時時限。 如果遇到了檔結束符(EOF),該程式就會停下來,然後expect返回。如果是遇到了超時的情況,expect就返回。在這兩中情況裡面,都隱式的殺死了當前進程。
如果我們不使用expect而來實現以上兩個例子的功能的話,我們還是可以學習到很多的東西的。在這兩中情況裡面,通常的解決方案都是fork另一個睡眠的子進程並且用signal通知原來的shell。如果這個過程或者讀先發生的話,shell就會殺司那個睡眠的進程。 傳遞pid和防止後臺進程產生啟動資訊是一個讓除了高手級shell程式師之外的人頭痛的事情。提供一個通用的方法來象這樣啟動多個進程會使shell腳本非常的複雜。 所以幾乎可以肯定的是,程式師一般都用一個專門C程式來解決這樣一個問題。
expect_user,send_user,send_error(向標準錯誤終端輸出)在比較長的,用來把從進程來的複雜交互翻譯成簡單交互的expect腳本裡面使用的比較頻繁。在參考[7]裡面,Libs描述怎樣用腳本來安全的包裹(wrap)adb,怎樣把系統管理員從需要掌握adb的細節裡面解脫出來,同時大大的降低了由於錯誤的擊鍵而導致的系統崩潰。
一個簡單的例子能夠讓ftp自動的從一個私人的帳號裡面取檔。在這種情況裡,要求提供密碼。 即使檔的訪問是受限的,你也應該避免把密碼以明文的方式存儲在檔裡面。把密碼作為腳本運行時的參數也是不合適的,因為用ps命令能看到它們。有一個解決的方法就是在腳本運行的開始調用expect_user來讓用戶輸入以後可能使用的密碼。這個密碼必須只能讓這個腳本知道,即使你是每個小時都要重試ftp。
即使資訊是立即輸入進去的,這個技巧也是非常有用。比如說,你可以寫一個腳本,把你每一個主機上不同的帳號上的密碼都改掉,不管他們使用的是不是同一個密碼資料庫。如果你要手工達到這樣一個功能的話,你必須Telnet到每一個主機上,並且手工輸入新的密碼。而使用expect,你可以只輸入密碼一次而讓腳本來做其它的事情。
expect_user和interact也可以在一個腳本裡面混合的使用。考慮一下在調試一個程式的迴圈時,經過好多步之後才失敗的情況。一個expect腳本可以驅動哪個調試器,設置好中斷點,執行該程式迴圈的若干步,然後將控制返回給鍵盤。它也可以在返回控制之前,在循環體和條件測試之間來回的切換。
文件ftp-down
1. #!/usr/bin/expect -f
2. set ipaddress [lindex $argv 0]
3. set username [lindex $argv 1]
4. set password [lindex $argv 2]
5.
6. spawn ftp $ipaddress
7. expect "*Name*"
8. send "$username\n"
9. expect "*Password:*"
10. send "$password\n"
11. expect "*ok*ftp>*"
12. send "get teste\n"
13. expect "*ftp>*"
14. send "quit\n"
15. expect eof
複製代碼
執行時如下
1. ./ftp-down 192.168.1.1 temp temp
複製代碼
腳名名稱 FTP主機位址 用戶名 密碼
沒有留言:
張貼留言