最近調試程式,由于測試資料集過大,程式要跑幾天幾夜, 還好,測試集有一定的獨立性,這就使我有空子鑽了---把程式分到多台機子上運作,再把資料重定向到本機上,嘿嘿。 這樣搞過一次,用了8台機子,運作了10個多小時,還是偷偷的ssh到别人的機子上運作的---不要說我rp不好,反正晚上嘛,我偷偷的用一下也沒什麼影響:) 不過,其實發現這樣其實還是很麻煩的, 要分别ssh 上去,還要手工計算配置設定資料段,重定向。。。etc。。總之用的機子越多越麻煩,還容易出錯。。 于是乎,自然想到寫段 shell script 解決,不過很遺憾的發現,shell的話 ssh上去就停在那裡的,其實也不奇怪,shell 為了安全起見,對程式提供了最小限度的控制(想啟動的程式發送消息等),而把所有的互動都留給了使用者, 也就是說shell基本不能運作互動式的程式了---比如那些執行期間要你輸入選擇的程式。。 我這樣的需求,那用shell跟不用差不多了----到互動的時候都是要自己手動敲指令的。。 當然不甘心了, google之,發現有個專門幹這個事的東西: expect ---以前從來沒聽過這個 :) “ Expect是一個免費的程式設計工具語言,用來實作自動和互動式任務進行通信,而無需人的幹預”, 粗略看了下文法,感覺我的問題可以很輕易的解決,試了一下,果不其然^_^, expect真是個好東西。。 貼下我的expect script : 1 #! /usr/bin/expect 2 proc process { name disk step } { 3 spawn ssh $name 4 expect "*:)*" 5 puts "ssh $name ok" 6 send " cd **some dir** /n " 7 expect "*:)*" 8 puts "cd ok" 9 append file "data_" $disk "_" $step 10 puts $file 11 append param1 "--disk=" $disk 12 append param2 "--step=" $step 13 send " ./moon $param1 $param2 > $file & /n" 14 expect "*:)*" 15 puts " run moon ok" 16 send " exit /n" 17 expect "*:)*" 18 puts "exit ok" 19 } 20 21 set step [ expr 140000/$argc +1 ] 22 set disk 0 23 for { set i 0 } { $i < $argc } { incr i } { 24 puts $i 25 set name [ lindex $argv $i ] 26 puts $name 27 28 process $name $disk $step 29 30 set $disk [ expr $disk+$step ] 31 } 32 33 puts "done. :)" ============================ 附:網上看到的一些expect 有關的資料(我感覺比較好的): =============================== Expect是一個免費的程式設計工具語言,用來實作自動和互動式任務進行通信,而無需人的幹預。 Expect的作者Don Libes在1990年開始編寫Expect時對Expect做有如下定義: Expect是一個用來實作自動互動功能的軟體套件(Expect [is a] software suite for automating interactive tools)。 引用: Expect語言是基于Tcl的, 作為一種腳本語言,Tcl具有簡單的文法: cmd arg arg arg 一條Tcl指令由空格分割的單詞組成. 其中, 第一個單詞是指令名稱, 其餘的是指令參數 . $foo $符号代表變量的值. 在本例中, 變量名稱是foo. [cmd arg] 方括号執行了一個嵌套指令. 例如, 如果你想傳遞一個指令的結果作為另外一個指令的參數, 那麼你使用這個符号 . "some stuff" 雙引号把詞組标記為指令的一個參數. "$"符号和方括号在雙引号内仍被解釋 . {some stuff} 大括号也把詞組标記為指令的一個參數. 但是, 其他符号在大括号内不被解釋. 反斜線符号() 是用來引用特殊符号. 例如:n 代表換行. 反斜線符号也被用來關閉"$"符号 , 引号,方括号和大括号的特殊含義 . 最好的學習方法就是邊幹邊學,對于已經熟悉一種程式設計語言的人來說,用另一種新的語言來寫程式解決問題,是很容易的事。是以大概了解一下基本文法後,就一邊動手解決問題,一邊查手冊吧。 關于Tcl和Expect的文法,請參考Unix/Linux 平台任務的自動化相關部分。 引用: 例1:下面是一個telnet到指定的遠端機器上自動執行指令的Expect腳本,該腳本運作時的輸出如下: # /usr/bin/expect sample_login.exp root 111111 spawn telnet 10.13.32.30 7001 Trying 10.13.32.30... Connected to 10.13.32.30. Escape character is '^]'. accho console login: root Password: Last login: Sat Nov 13 17:01:37 on console Sun Microsystems Inc. SunOS 5.9 May 2004 # Login Successfully... # uname -p sparc # ifconfig -a lo0: flags=2001000849<UP,LOOPBACK,RUNNING,MULTICAST,IPv4,VIRTUAL> mtu 8232 index 1 inet 127.0.0.1 netmask ff000000 eri0: flags=1000843<UP,BROADCAST,RUNNING,MULTICAST,IPv4> mtu 1500 index 2 inet 10.13.22.23 netmask ffffff00 broadcast 10.13.22.255 ether 0:3:ba:4e:4a:aa # exit accho console login: Finished... 引用: 下面是該腳本的源代碼: # vi sample_login.exp: proc do_console_login {login pass} { set timeout 5 set done 1 set timeout_case 0 while ($done) { expect { "console login:" { send "$loginn" } "Password:" { send "$passn" } "#" { set done 0 send_user "nnLogin Successfully...nn" } timeout { switch -- $timeout_case { 0 { send "n" } 1 { send_user "Send a return...n" send "n" } 2 { puts stderr "Login time out...n" exit 1 } } incr timeout_case } } } } proc do_exec_cmd {} { set timeout 5 send "n" expect "#" send "uname -pn" expect "#" send "ifconfig -an" expect "#" send "exitn" expect "login:" send_user "nnFinished...nn" } if {$argc<2} { puts stderr "Usage: $argv0 login passwaord.n " exit 1 } set LOGIN [lindex $argv 0] set PASS [lindex $argv 1] spawn telnet 10.13.32.30 7001 do_console_login $LOGIN $PASS do_exec_cmd close exit 0 上面的腳本隻是一個示例,實際工作中,隻需要重新實作do_exec_cmd函數就可以解決類似問題了。 引用: 在例1中,還可以學習到以下Tcl的文法: 1. 指令行參數 $argc,$argv 0,$argv 1 ... $argv n if {$argc<2} { puts stderr "Usage: $argv0 login passwaord.n " exit 1 } 2. 輸入輸出 puts stderr "Usage: $argv0 login passwaord.n " 3. 嵌套指令 set LOGIN [lindex $argv 0] set PASS [lindex $argv 1] 4. 指令調用 spawn telnet 10.13.32.30 7001 5. 函數定義和調用 proc do_console_login {login pass} { .............. } 6. 變量指派 set done 1 7. 循環 while ($done) { ................ } 8. 條件分支Switch switch -- $timeout_case { 0 { ............... } 1 { ............... } 2 { ............... } } 9. 運算 incr timeout_case 此外,還可以看到 Expect的以下指令: send expect send_user 可以通過-d參數調試Expect腳本: # /usr/bin/expect -d sample_login.exp root 111111 EXPECT 互動式程式可程式設計對話,第5版 expect [ -dDinN ] [ -c cmds ] [ -[f|b] ] cmdfile ] [ args ]'F} 簡介 Expect是一種能利用腳本和其它互動式程式進行對話的程式。通過腳本,expect能夠獲知一個程式應該有怎樣的響應和怎樣是正确的響應。它采用翻譯式語言來控制流結構和高層結構使對話進行下去。而且,還允許使用者在想要控制的時候能夠直接控制程式,然後再将控制交回給腳本。 Expectk是expect和tk的混合體,它可以象expect和tk那樣使用。同樣,expect也能夠被C和C++直接調用(沒有Tcl存在的情況下)。可以參看libexpect(3)。 Expect這個名字來源于廣泛使用的uucp, Kermit和其它的modem控制程式等的send/expect序列的思想,但是它并不象 uucp那樣,expect對環境沒有很特殊的要求,是以可以作為使用者級的指令和任何程式互動,而且expect實際上可以同時和多個程式互動。 舉例來說,expect可以做這些事情: 使你的計算機可以回撥,這樣你就不必為你的上網而支付電話費啦。 一遍遍的開始一個遊戲程式(如rogue),直到那個随機産生的裝備設定達到最好,然後把控制交給你來玩遊戲。 運作fsck,對它的提問按預先設定的标準給出響應“yes”,“no”或者将控制交給你。 連到另外一個網絡或BBS(如MCI Mail, CompuServe),自動收下你的信件,就好象原來發到你的本地系統一樣。 攜帶rlogin, telnet, tip, su, chgrp等需要的環境變量,目前目錄或其它資訊 用普通腳本來執行一個任務存在着很多種理由使得是不可行的,(如果你試試就知道了)但用expect就都成為可能了。 總地說來,expect在運作那些需要在程式和使用者之間進行互動的程式時是很有用的。互動一旦被程式化地指定了,運作起來會很友善。如果需要,Expect也可以将控制交還給使用者(不停止正在運作的程式)。類似的,使用者也可以在任何時間把控制交還給腳本。| 注:老外可真夠羅嗦的,就這麼簡單的意思,讓我翻譯這麼半天,還是我來簡單說說吧?:expect是個腳本解釋程式,就好象/bin/sh, /bin/ksh一樣。所完成的功能呢,最簡單的就是自動對需要人工互動和程式進行自動互動,比如一個程式需要你不斷地輸入yes繼續,你懶得做,幹脆用寫個expect腳本自動輸入yes就行了。當然,expect可以做的事情遠不止這些,它實際上是tcl (Tool Command Language)的一個變種,格式和tcl程式也類似,寫expect腳本對懂tcl的人應該不難。用過 secureCRT的人應該知道有個自動登入的設定,那就是利用expect實作的。好了,我不羅嗦了,繼續幹活。 引用:用法 expect從cmdfile中讀取指令清單來執行,同樣它也可以在有執行權限的腳本的第一行中加上#!辨別來隐式地執行,如: #!/usr/local/bin/expect -f 當然,路徑應該準确地描述expect的位置,/usr/local/bin隻是一個例子。 -c參數訓示其後的指令在腳本的最先開始執行,指令應該用引号引起來以不被shell打散。這個選項可被多次使用。多個指令如果用一個-c訓示,則應用分号分隔。指令将按其書寫順序執行。(使用expectk時,這個參數用作-command) -d參數允許一些診斷輸出,報告主要的expect和互動指令行為。在expect腳本開始用exp_internal 1也可以起到一樣的作用,-d會多打出expect的版本。(strace指令在跟蹤狀态時很有用,trace指令在跟蹤變量時很有用)(expectk中此參數為- diag)。 -D參數打開互動debugger,後跟一個整數。如果這個整數是非零,或者^C被按下(或者碰到一個設定的斷點,或者腳本中設定的其它合适的 debugger指令)Debugger會在下一個tcl過程之前控制程式。關于debugger的資訊參看README或SEE ALSO。 (expectk中此參數為-Debug) -f參數指定從哪個檔案中讀取指令。當被用在#!訓示(見上)中時此參數是可選的,是以其它參數可在指令行中提供。(expectk中為-file)。 -b參數。預設地,指令檔案被整個地讀到記憶體中執行,但是有時需要一行行地讀取,比如,标準輸入stdin就是這樣。為了強制特定的檔案被這樣讀入,可以使用-b參數。(expectk中為-buffer)。如果檔案名是“-”,則表示從标準輸入stdin讀入。(用“./-”來表示一個叫作 “-”的檔案) -i參數使expect互動地提示輸入指令,而不是從檔案中讀指令。指令提示行通過exit指令或一個eof字元結束。參看interpreter(見下)。-i假設既沒有指令檔案,又沒有使用-c參數。(expectk中為-interactive)。 --用來對選項參數結束的劃界。在你想傳遞一個象選項參數樣的參數給你的腳本時,這個選項是很有用的,它使得expect不對其進行翻譯。也可以放在#!行來阻止expect對任何選項參數格式的參數的翻譯。比如,下面例子将保留原始參數(包括腳本名)到argv中: #!/usr/local/bin/expect 注意加參數到#!行時應該遵守getopt(3)和execve(2)的慣例。 -N選項。 $exp_library/expect.rc檔案如果存在的話将被自動的啟用,除非-N選項被使用。(expectk中為- NORC)這樣的話就會自動找~/.expect.rc,除非加了-n參數。如果定義了環境變量DOTDIR,那就會從那裡找.expect.rc。 (expectk中為-norc)。expect.rc的使用隻在執行完-c參數指定的指令後。 -v列印expect的版本号并退出。(expectk中為-version) 可選的args被結構化成一個清單存在argv中,argc被初始化成argv的長度。 Argv0被定義為腳本的名字。下面例子列印出腳本名和前三個參數: send_user "$argv0 [lrange $argv 0 2] % set i 1 1 字元串應該用引号括起來: % set str "test" 'test' 要輸出一個标量的内容,使用put語句: % puts $str test $用來說明str是一個變量。puts函數在标準輸出顯示變量的内容。 數組也可以用set語句定義,實際上,tcl中建立數組隻是單個建立數組的元素。例如 , % set arr(1) 0 % set arr(2) 1 1 這樣就建立了一個兩個元素的數組arr。在TCL中,不存在相當于數組邊界這樣的東西 ,例如 % set arr(100) to to 這時數組中實際隻存在arr(1),arr(2)和arr(100),這是和C語言不同的地方。用arr ay size指令可以傳回數組的大小: % array size arr 3 通路數組的方法和通路标兩實際是一樣的,例如: % puts $arr(100) to 可以用同樣的方法建立多元數組。 要使用數組中的所有元素,需要使用一種特殊的便利方式。首先要啟動startsearsh: % array startsearch arr s-1-arr 這裡傳回了一個搜尋id,你可以把它傳遞給某個變量,因為以後還要使用它進行進一 步的搜尋: % set my_id [array startsearch arr] s-1-arr 現在my_id的内容是s-1-arr,然後,就可以搜尋arr的内容了: % array nextelement arr $my_id whi 這裡的array nextelement傳回的是什麼?可能有點出乎你的意料,是arr數組的下标 ,再執行一次array nextelement指令又會找出另外一個下标: % array nextelement arr $my_id 4 這樣周遊下去,可以找出arr數組的所有下标,而知道下标之後,就可以用$arr(4)之 類的方式通路arr的内容了。當周遊完成之後,array nextelement指令将簡單地傳回: % array nextelement arr $my_id % 這時就可以停止周遊過程了,如果你想确認周遊是否完成,可以使用array anymore命 令: % array anymore arr $my_id 傳回0說明周遊已經完成。 串處理 TCL中可以進行一般的串處理過程,這可以使用string指令和append指令,append指令 将某個字元串加到另外一個字元串的後面: % set str1 "test " test % set str2 "cook it" cook it % append str1 $str2 " and other" test cook it and other string指令可以執行字元串的比較,删除和查詢,其格式是 string [參數] string1 [string2] 參數可以是下面的指令之一: compare 按照字典順序對字元串進行比較,根據相對關系傳回-1,0或者+1。 first 傳回string2中第一次出現string1的位置,如果失敗,傳回-1。 last 傳回string2中最後一次出現string1的位置,如果失敗,傳回-1 trim 從string1中删除開頭和結尾的出現在string2中的字元 trimleft 從string1中删除開頭的出現在string2中的字元。 trimright 從string1中删除結尾的出現在string2中的字元 下面幾個用在string中的參數不需要string2變量: length 傳回tring1的長度 tolower 傳回将string1全部小寫化的串 toupper 傳回将string1全部大寫化的串 運算 TCL的運算方式比較别扭,它使用expr指令作為計算符号,其用法類似C語言的+=和/= ,例如, % set j [expr $i/5] 1 注意TCL會自動選擇整數或者浮點計算: % set l [ expr $i /4.0] 1.25 % set l [ expr $i /4] 1 在TCL裡面可以使用+ - * /和%作為基本運算符,另外通常還包括一些數學函數,如a bs,sin,cos,exp和power(乘方)等等。 另外,還有一個起運算符作用的指令incr,它用來對變量加一: % set i 1 1 % incr i 2 流程控制 tcl支援分支和循環。分支語句可以使用if和switch實作。if語句的和C語言類似,如 if { $ x < 0 } { set y 10; } 注意判斷子句也需要使用花括号。 與C語言一樣,tcl的if語句也可以使用else和elseif。 switch語句的用法有點類似這樣: switch $x { 0 { set y 10;} 10 { set y 100;} 20 { set y 400;} } 與C的switch語句不同,每次隻有符合分支值的子句才被執行。 循環指令主要由for,foreach和while構成,而且每一個都可以使用break和continue 子句。 for語句的格式有點類似這樣: for { set i 0} {$i < 10} { incr i} {puts $i} 将會輸出從1到9的整數。 如果用while循環,這個句子可以寫成 while {$i < 10 } { puts $i; incr i; } foreach是對于集合中的每一個元素執行一次指令,大緻的指令格式是 foreach [變量] { 集合 } { 語句; } 例如 % foreach j { 1 3 5} { put $j; } 1 3 5 函數 如同在一般的程式設計語言裡面一樣,在tcl裡面也可以定義函數,這是通過proc指令實作 的: proc my_proc {i}{ puts $i; } 這樣就定義了一個名字叫proc的函數,它隻是在終端顯示輸入變元的内容。 要使用這個函數,簡單地輸入它的名字: % my_proc { 5 } 5 如果變元的數目是0,隻要使用空的變元清單,例如 proc my_proc {} {語句;} 盡管tcl還可以處理更複雜的過程,但是我們不再介紹了,例如檔案的讀寫以及tk圖形 語言,因為我們處理tcl的主要目标就是了解expect,對于更複雜的程式設計工作,我們建議 你使用perl。 11.1.2 expect expect是建立在tcl基礎上的一個工具,它用來讓一些需要互動的任務自動化地完成。 我們首先從一個簡單的例子開始,如同在這一節一開始就提到的,我們想設定一個自動 的檔案下載下傳程式。 我們看一看這樣的一個例子腳本: #! /usr/bin/expect spawn ftp 202.199.248.11 expect "Name" send "ftpr" expect "Password:" send "nothingr" expect "apply" send "cd /pub/UNIX/Linux/remoteXr" expect "successful." send "binr" expect "set to I" send "get exceed5.zipr" expect "complete." send "quitr" 這個是什麼意思?呵呵,就是個自動下載下傳程式。第一行說明這個程式應該調用/usr/b in/expect去執行,然後的就是expect指令。 察看expect的手冊頁面(man expect)可以得到一個很長的expect說明,可惜其中關于 expect的文法仍然介紹的不夠。一般來說,expect主要用在需要自動執行人機互動的過 程中,例如fsck程式,這個程式會不斷地提問"yes/no",像這樣的指令就可以用expect 來完成。 spawn語句在expect腳本中用于啟動一個新的程序,在我們的程式中,spawn ftp 202 .199.248.11就是去執行ftp程式,接下來,就是expect和send的指令對了。 每一對expect和send指令代表一個資訊/回應。如果這樣說不好了解的話,那麼可以看 一看ftp的具體執行過程: ftp 202.199.248.11 Connected to 202.199.248.11. 220 mail.asnc.edu.cn FTP server (BeroFTPD 1.3.3(3) Sun Feb 20 15:52:49 CST 2000. Name (202.199.248.11:wanghy): 顯然,一旦連接配接成功,伺服器會傳回一個Name(202.199.248.11:wanghy):的字元串來 要求客戶給出使用者名。expect語句簡單地在傳回資訊中查詢你給出的字元串,一旦成功 就執行下面的指令,現在,expect " Name"已經成功地找到了Name字元串,接下來可以 執行send指令了。 send指令比expect指令更簡單,它簡單地向标準輸入送出你設定的字元串,現在設定 為send "ftpr"表示等到登入資訊之後就給出一個輸入ftp回車,也就是标準的登入過 程。 下面的行與這些行完全一樣,隻是機械地等待伺服器的回應,并且送出自己的輸入。 要使用這個expect腳本,你隻需要将它設定為可執行的屬性,然後執行它,expect就 會執行你需要的服務。 由于expect是tcl的擴充,是以你在expect檔案中可以象tcl腳本一樣設定變量和程式 流程。 現在我們看一看我們還能夠如何改進我們的expect腳本。ftp指令可能會失敗,比如遠 端的機器可能會無法提供服務,或者在啟動ftp指令時本地機器發生問題。為了處理這一 類的問題,我們可以使用expect的timeout選項來設定逾時的話expect腳本自動退出: #! /usr/bin/expect spawn ftp 202.199.248.11 expect { timeout exit Connect } ……………… 注意這裡面使用的花括号。它的含義是使用一組并清單達式。使用并清單達式的主要 原因是這樣:如果使用下面的指令對: expect timeout exit 那麼由于expect腳本是順序執行的,那麼當程式執行到這個expect的時候就會阻塞, 是以程式會一直等待到timeout然後退出。并清單達式則是相當于switch的行為,隻要列 出的幾項内容有一項得到滿足,expect指令就得到滿足,于是程式可以正常執行。上面 的腳本表示,如果連接配接ftp的時候發生了逾時,那麼就退出,否則,一旦發現Connect應 答,說明伺服器已經正常了,那麼就可以繼續運作了。 我們可以看看用tcl能夠對我們的expect腳本提供什麼幫助。我們可以設定讓expect腳 本不斷地連接配接遠端伺服器的服務,直到正常建立連接配接開始,為此,我們可以把建立連接配接 的指令放在一個循環裡面,并且根據回應的不同自動選擇重新輸入指令還是繼續執行: spawn ftp while {1} { expect "ftp>" send "o 202.199.248.11r" expect { "Connected" break "refused" { sleep 10} ; } } 這裡使用了我們在tcl語言中講到的while和break指令,熟悉C的讀者應該很容易看出 它的行為:不斷地等待ftp>提示符,在提示符下面發送連接配接遠端伺服器的指令,如果服 務器回應是refused(連接配接失敗),就等待10秒鐘,然後開始下一次循環;如果是Conne cted,那麼就跳出循環執行下面的指令。sleep是expect的一個标準指令,表示暫停若幹 秒鐘。 expect還支援許多更複雜的程序控制方式,如fork,disconnect等等,你可以從手冊 頁面中得到詳細的資訊。另外,各種tcl運算符和流程控制指令,包括tcl函數也可以使 用。 有些讀者可能會問,如果expect執行的話是否控制台輸入不能使用了,答案是否定的 。expect指令運作時,如果某個等待的資訊沒有得到,那麼程式會阻塞在相應的expect 語句處,這時,你在鍵盤上輸入的東西仍然可以正常地傳遞到程式中去,其實對于那些 expect處理的資訊,原則上你輸入的内容仍然有效,隻是expect的反映太快,總是搶在 你的前面“輸入”就是了。知道了這一點之後,你就可能寫一個expect腳本,讓expect 自動處理來自fscki的那些惡心的yes/no選項(我們介紹過,這些yes/no其實完全是多餘 的,正常情況下你除了選擇yes之外什麼也幹不了)。 預設下,expect在标準輸出(你的終端上)輸出所有來自應用程式的回應資訊,你可 以用下面的兩個指令重定向這些資訊: log_file [檔案名] 這個指令讓expect在你設定的檔案中記錄輸出資訊。必須注意,這個選項并不影響控 制台輸出資訊,不過如果你通過crond設定expect腳本在半夜運作的話,你就确實可能需 要這個指令來記錄各種資訊了。例如: log_file expect.log log_user 0/1 這個選項設定是否顯示輸出資訊,設定為1時是預設值,為0 的話,expect将不産生任 何輸出資訊,或者說簡單地過濾掉控制台輸出。必須記住,如果你用log_user 0關閉了 控制台輸出,那麼你同時也就關閉了對記錄檔案的輸出。 這一點很讓人困擾,如果你确實想要記錄expect的輸出卻不想讓它在控制台上制造垃 圾的話,你可以簡單地把expect的輸出重定向到/dev/null: ./test.exp > /dev/null 你可以象下面這樣使用一對fork和disconnect指令。expect的disconnect指令将使得 相應的程序到背景執行,輸入和輸出被重定向到/dev/null: if [fork]!=0 exit disconnect fork指令會産生出一個子程序,而且它産生傳回值,如果傳回的是0,說明這是一個子 程序,如果不為0,那麼是父程序。是以,執行了fork指令之後,父程序死亡而子程序被 disconnect指令放到背景執行。注意disconnect指令隻能對子程序使用。 |