轉自:http://15103850.blog.hexun.com/61380230_d.html
一、概述
我們通過Shell可以實現簡單的控制流程功能,如:迴圈、判斷等。但是對於需要互動的場合則必須通過人工來幹預,有時候我們可能會需要實現和互動程式如telnet伺服器等進行互動的功能。而Expect就使用來實現這種功能的工具。
Ubuntu 論壇
Expect是一個免費的編程工具語言,用來實現自動和互動式任務進行通訊,而無需人的幹預。Expect的作者Don Libes在1990年 開始編寫Expect時對Expect做有如下定義:Expect是一個用來實現自動互動功能的軟體套件 (Expect [is a] software suite for automating interactive tools)。使用它系統管理員 的可以建立指令碼用來實現對命令或程式提供輸入,而這些命令和程式是期望從終端(terminal)得到輸入,一般來說這些輸入都需要手工輸入進行的。 Expect則可以根據程式的提示類比標準輸入提供給程式需要的輸入來實現互動程式執行。甚至可以實現實現簡單的BBS聊天機器人。 :)
Expect是不斷髮展的,隨著時間的流逝,其功能越來越強大,已經成為系統管理員的的一個強大助手。Expect需要Tcl程式設計語言的支援,要在系統上運行Expect必須首先安裝Tcl。
二、Expect工作原理
從最簡單的層次來說,Expect的工作方式象一個通用化的Chat指令碼工具。Chat指令碼最早用於UUCP網路內,以用來實現電腦之間需要建立串連時進行特定的登入工作階段的自動化。
Chat指令碼由一系列expect-send對組成:expect等待輸出中輸出特定的字元,通常是一個提示符,然後發送特定的響應。例如下面的 Chat指令碼實現等待標準輸出出現Login:字串,然後發送somebody作為使用者名稱;然後等待Password:提示符,並發出響應 sillyme。
引用:Login: somebody Password: sillyme
這個指令碼用來實現一個登入過程,並用特定的使用者名稱和密碼實現登入。
Expect最簡單的指令碼操作模式本質上和Chat指令碼工作模式是一樣的。
例子: BT無線網路破解教程
1、實現功能
下面我們分析一個響應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。
2、下面是一個能用來實現自動執行該命令的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指令碼也就退出結束。
3、決定如何響應
管理員往往有這樣的需求,希望根據當前的具體情況來以不同的方式對一個命令進行響應。我們可以通過後面的例子看到expect可以實現非常複雜的條件響 應,而僅僅通過簡單的修改預先處理指令碼就可以實現。下面的例子是一個更複雜的expect-send例子:
expect -re "\[(.*)]:"
if {$expect_out(1,string)!="/bin/tcsh"} {
send "/bin/tcsh" }
send " "
expect eof
在這個例子中,第一個expect命令現在使用了-re參數,這個參數表示指定的的字串是一個Regex,而不是一個普通的字串。對於上面這 個例子裡是尋找一個左方括弧字元(其必須進行三次逃逸(escape),因此有三個符號,因為它對於expect和正則表達時來說都是特殊字元)後面跟有 零個或多個字元,最後是一個右方括弧字元。這裡.*表示表示一個或多個任一字元,將其存放在()中是因為將匹配結果存放在一個變數中以實現隨後的對匹配結 果的訪問。
當發現一個匹配則檢查包含在[]中的字串,查看是否為/bin/tcsh。如果不是則發送/bin/tcsh給chsh命令作為輸入,如果是則僅僅發送一個斷行符號符。這個簡單的針對具體情況發出不同相響應的小例子說明了expect的強大功能。
在一個正則表達時中,可以在()中包含若干個部分並通過expect_out數組訪問它們。各個部分在運算式中從左至右進行編碼,從1開始(0包含有整個匹配輸出)。()可能會出現嵌套情況,這這種情況下編碼從最內層到最外層來進行的。
4、使用逾時
下一個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
Cisco IOS On Unix 專題
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"
5、一個更複雜的例子
下面我們將討論一個更加複雜的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 {!=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指令碼結束。
簡單例子:
#!/usr/bin/expect
set timeout 30
spawn ssh -l user x.x.x.x
expect {
"password: " {send "mypasswd\r"}
timeout {
send_user "timeout"
exit 1
}
eof {
send_user "endofsession"
exit 1
}
}
set timeout 1
expect "localhost ~]$"
send "cd test; nohup ./a.out &\r"
#interact
expect eof
該指令碼自動登入xxxx機器,並執行了相應命令後返回當前終端。其中user和mypasswd分別替換為真實的使用者和密碼即可
注意:expect的本地檔案要用絕對路徑