2012年9月24日 星期一

expect的基本用法

一、概述

我們通過Shell可以實現簡單的控制流功能,如:迴圈、判斷等。但是對於需要交互的場合則必須通過人工來干預,有時候我們可能會需要實現和交互程式如telnet伺服器等進行交互的功能。而Expect就使用來實現這種功能的工具。

Expect
是一個免費的程式設計工具語言,用來實現自動和互動式任務進行通信,而無需人的干預。Expect的作者Don Libes1990年開始編寫Expect時對Expect做有如下定義:Expect是一個用來實現自動交互功能的軟體套件(Expect [is a] software suite for automating interactive tools)。使用它系統管理員的可以創建腳本用來實現對命令或程式提供輸入,而這些命令和程式是期望從終端(terminal)得到輸入,一般來說這些輸入都需要手工輸入進行的。Expect則可以根據程式的提示類比標準輸入提供給程式需要的輸入來實現交互程式執行。甚至可以實現實現簡單的BBS聊天機器人。 http://bbs.chinaunix.net/static/image/smiley/default/icon_smile.gif

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腳本工作模式是一樣的。

例子:
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腳本: 

1.  #!/usr/bin/expect
2.  # Change a login shell to tcsh
3.   
4.  set user [lindex $argv 0]
5.  spawn chsh $user
6.  expect "]:"
7.  send "/bin/tcsh " 
8.  expect eof
9.  exit
複製代碼


這個簡單的腳本可以解釋很多Expect程式的特性。和其他腳本一樣首行指定用來執行該腳本的命令程式,這裡是/usr/bin/expect。程式第一行用來獲得腳本的執行參數(其保存在陣列$argv中,從0號開始是參數),並將其保存到變數user中。

第二個參數使用Expectspawn命令來啟動腳本和命令的會話,這裡啟動的是chsh命令,實際上命令是以衍生子進程的方式來運行的。

隨後的expectsend命令用來實現交互過程。腳本首先等待輸出中出現]:字串,一旦在輸出中出現chsh輸出到的特徵字串(一般特徵字串往往是等待輸入的最後的提示符的特徵資訊)。對於其他不匹配的資訊則會完全忽略。當腳本得到特徵字串時,expect將發送/bin/tcsh和一個回車符給chsh命令。最後腳本等待命令退出(chsh結束),一旦接收到標識子進程已經結束的eof字元,expect腳本也就退出結束。

3
、決定如何回應

管理員往往有這樣的需求,希望根據當前的具體情況來以不同的方式對一個命令進行回應。我們可以通過後面的例子看到expect可以實現非常複雜的條件回應,而僅僅通過簡單的修改預處理腳本就可以實現。下面的例子是一個更複雜的expect-send例子: 

1.  expect -re "\[(.*)]:"
2.  if {$expect_out(1,string)!="/bin/tcsh"} {
3.  send "/bin/tcsh" }
4.  send " "
5.  expect eof
複製代碼


在這個例子中,第一個expect命令現在使用了-re參數,這個參數表示指定的的字串是一個規則運算式,而不是一個普通的字串。對於上面這個例子裡是查找一個左方括號字元(其必須進行三次逃逸(escape),因此有三個符號,因為它對於expect和正則表達時來說都是特殊字元)後面跟有零個或多個字元,最後是一個右方括號字元。這裡.*表示表示一個或多個任意字元,將其存放在()中是因為將匹配結果存放在一個變數中以實現隨後的對匹配結果的訪問。

當發現一個匹配則檢查包含在[]中的字串,查看是否為/bin/tcsh。如果不是則發送/bin/tcshchsh命令作為輸入,如果是則僅僅發送一個回車符。這個簡單的針對具體情況發出不同相回應的小例子說明了expect的強大功能。

在一個正則表達時中,可以在()中包含若干個部分並通過expect_out陣列訪問它們。各個部分在運算式中從左到右進行編碼,從1開始(0包含有整個匹配輸出)()可能會出現嵌套情況,這這種情況下編碼從最內層到最外層來進行的。

4
、使用超時

下一個expect例子中將闡述具有超時功能的提示符函數。這個腳本提示用戶輸入,如果在給定的時間內沒有輸入,則會超時並返回一個默認的響應。這個腳本接收三個參數:提示符字串,默認回應和超時時間() 

1.  #!/usr/bin/expect
2.  # Prompt function with timeout and default.
3.  set prompt [lindex $argv 0]
4.  set def [lindex $argv 1] 
5.  set response $def
6.  set tout [lindex $argv 2]
複製代碼

腳本的第一部分首先是得到運行參數並將其保存到內部變數中。 

1.  send_tty "$prompt: "
2.  set timeout $tout
3.  expect " " {
4.  set raw $expect_out(buffer)
5.  # remove final carriage return
6.  set response [string trimright "$raw" " "]
7.  }
8.  if {"$response" == "} {set response $def}
9.  send "$response "
10. # Prompt function with timeout and default.
11. set prompt [lindex $argv 0]
12. set def [lindex $argv 1] 
13. set response $def
14. 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命令給任意的使用者,發送的消息來自於一個檔或者來自於鍵盤輸入。 

1.  #!/usr/bin/expect
2.  # Write to multiple users from a prepared file
3.  # or a message input interactively
4.   
5.  if {$argc<2} {
6.  send_user "usage: $argv0 file user1 user2 ... "
7.  exit
8.  }
複製代碼

send_user
命令用來顯示使用説明資訊到父進程(一般為使用者的shell)的標準輸出。 

1.  set nofile 0
2.  # get filename via the Tcl lindex function
3.  set file [lindex $argv 0]
4.  if {$file=="i"} { 
5.  set nofile 1 
6.  } else { 
7.  # make sure message file exists
8.  if {[file isfile $file]!=1} { 
9.  send_user "$argv0: file $file not found. "
10. exit }}
複製代碼


這部分實現處理腳本啟動參數,其必須是一個儲存要發送的消息的檔案名或表示使用交互輸入得到發送消的內容的"i"命令。

變數file被設置為腳本的第一個參數的值,是通過一個Tcl函數lindex來實現的,該函數從清單/陣列得到一個特定的元素。[]用來實現將函數lindex的返回值作為set命令的參數。

如果腳本的第一個參數是小寫的"i",那麼變數nofile被設置為1,否則通過調用Tcl的函數isfile來驗證參數指定的檔存在,如果不存在就報錯退出。

可以看到這裡使用了if命令來實現邏輯判斷功能。該命令後面直接跟判斷條件,並且執行在判斷條件後的{}內的命令。if條件為false時則運行else後的區塊。 

1.  set procs {}
2.  # start write processes
3.  for {set i 1} {$i<$argc}
4.  {incr i} {
5.  spawn -noecho write 
6.  [lindex $argv $i] 
7.  lappend procs $spawn_id
8.  }
複製代碼

最後一部分使用spawn命令來啟動write進程實現向使用者發送消息。這裡使用了for命令來實現迴圈控制功能,迴圈變數首先設置為1,然後因此遞增。循環體是最後的{}的內容。這裡我們是用腳本的第二個和隨後的參數來spawn一個write命令,並將每個參數作為發送消息的用戶名。lappend命令使用保存每個spawn的進程的進程ID號的內部變數$spawn_id在變數procs中構造了一個進程ID號清單。 

1.  if {$nofile==0} {
2.  setmesg [open "$file" "r"]
3.  } else {
4.  send_user "enter message,
5.  ending with ^D: " }
複製代碼


最後腳本根據變數nofile的值實現打開消息檔或者提示使用者輸入要發送的消息。 

1.  set timeout -1
2.  while 1 {
3.  if {$nofile==0} {
4.  if {[gets $mesg chars] == -1} break
5.  set line "$chars " 
6.  } else {
7.  expect_user {
8.  -re " " {}
9.  eof break }
10. set line $expect_out(buffer) }
11.  
12. foreach spawn_id $procs { 
13. send $line }
14. sleep 1}
15. exit
複製代碼

上面這段代碼說明了實際的消息文本是如何通過無限迴圈while被發送的。while迴圈中的 if判斷消息是如何得到的。在非交互模式下,下一行內容從消息檔中讀出,當檔內容結束時while迴圈也就結束了。(break命令實現終止迴圈)  

在交互模式下,expect_user命令從使用者接收消息,當使用者輸入ctrl+D時結束輸入,迴圈同時結束。 兩種情況下變數$line都被用來保存下一行消息內容。當是消息檔時,回車會被附加到消息的尾部。

foreach
迴圈遍歷spawn的所有進程,這些進程的ID號都保存在清單變數$procs中,實現分別和各個進程通信。send命令組成了foreach的循環體,發送一行消息到當前的write進程。while迴圈的最後是一個sleep命令,主要是用於處理非交互模式情況下,以確保消息不會太快的發送給各個write進程。當while迴圈退出時,expect腳本結束。


三、參考資源

Expect
軟體版本深帶有很多例子腳本,不但可以用於學習和理解expect腳本,而且是非常使用的工具。一般可以在/usr/doc/packages/expect/example看到它們,在某些linux發佈中有些expect腳本保存在/usr/bin目錄下。 

Don Libes, Exploring Expect, O'Reilly & Associates, 1995.
 

John Ousterhout, Tcl and the Tk Toolkit, Addison-Wesley, 1994.
 

一些有用的expect腳本

autoexpect:
這個腳本將根據自身在運行時使用者的操作而生成一個expect腳本。它的功能某種程度上類似于在Emacs編輯器的鍵盤巨集工具。一個自動創建的腳本可能是創建自己定制腳本的好的開始。

kibitz:
這是一個非常有用的工具。通過它兩個或更多的用戶可以連接到同一個shell進程。

tkpasswd:
這個腳本提供了修改使用者密碼的GUI工具,包括可以檢查密碼是否是基於字典模式。這個工具同時是一個學習expecttk的好實例。

沒有留言: