Linux shell 交互式編程、TCL/TK 和 Expect 編譯與安裝、expect 編程

以下文章資源都來源於網絡,保留原作者的一切權利:但是不知道原作者是誰了……



Expect  被用來進行一些需要進行交互是shell 編程的,比如完成ssh 自動登錄,就可以使用 expect 編程來實現


        1,獲取原始的tcl源碼包和expect源碼包,名稱根據版本不同而有所區別。我的分別是tcl8.4.14.tar 和 expect-5.43.tar。

下載Expect的地址:http://expect.nist.gov/src/

下載TCL/TK的地址: http://www.tcl.tk/software/tcltk/downloadnow84.tml

                                http://download.chinaunix.net/download/0001000/22.shtml

需要注意的是不能直接拷貝編譯過的源代碼目錄,否則好像會編譯出錯。在我的實際工作經驗中曾經這麼做:在型號爲9000/800/SD32A 的hp-unix主機上拷貝了這兩個目錄到型號爲ia64 hp superdome server SD32B的hp-unix主機上,結果編譯的時候提示 socklen_t 和 fd_mask  這兩個變量分別與/usr/include/sys/socket.h 和 /usr/include/sys/types.h文件裏的重定義了。



      2,編譯expect工具之前,需要先編譯tcl的動態庫。解壓源碼包,tar -xvf tcl8.4.14.tar; 切換到解壓之後的tcl源代碼路徑,該目錄下根據不同的操作系統有不同的編譯路徑,支持mac, win ,unix。由於我的是hp-unix,所以我進入tcl8.4.14/unix目錄,運行configure工具,./configure --prefix="Install dir"(安裝目錄)。 這裏—prefix=指定了tcl下一步的安裝目錄。



     3,配置腳本執行之後將會產生Makefile文件,這時候只需gmake 即可, gmake install可以完成安裝。(如果操作系統不支持gmake,可以make代替)



      4,到安裝目錄,可以檢查tcl編譯是否安裝正確。本軟件包自帶測試套件,能執行一些測試,以確定它是否編譯正確。如果你想運行測試套件,執行下面的命令: TZ=UTC make test



     5,編譯expect工具。解壓源碼包: tar -xvf expect-5.43.tar,進入解壓之後的expect源代碼路徑,運行配置命令:



./configure -prefix=/app/expect --with-tcl=/app/expect/lib --with-x=no --with-tclinclude=/app/util/tcl8.4.14/generic

配置參數的意思是:

·    -prefix=/app/expect 安裝目錄,同tcl的編譯

·         --with-tcl=/app/expect/lib: 確保配置腳本找到臨時工具目錄中的Tcl,我們不希望它使用主系統中可能存在的tcl. 一般使用上一步tcl編譯之後的安裝目錄下的lib路徑

·         --with-x=no: 告訴配置腳本,不要查找 Tk (Tcl 的 GUI 組件) 或 X 窗口系統庫,這兩個都有可能存在於主系統中。

·         -with-tclinclude:幫助腳本找到所需要的tcl頭文件。一般設置成 /tcl源代碼路徑/generic 目錄



   6,編譯expect : gmake


    7,本軟件包自帶測試套件,可以執行一系列測試,以確定是否正常編譯: gmake test。 網上有資料說有的版本test的時候會出現一些錯誤,我沒實際遇到過,實際上這一步我很少執行,哈哈。



   8,安裝expect : gmake install 。



   9,環境變量的path路徑加上expect安裝路徑,就可以隨時隨地執行expect了。


交互式shell編程利器expect

手裏有幾臺Linux服務器需要經常添加用戶,每次都要登錄到相應的機器上去添加,特別麻煩。於是想,可不可以在一臺機器上寫一個腳本來遠程管理其它服務器呢?



目標首先瞄準了我熟悉的PHP-CLI,它有一個開發中的模塊ssh2,可以完成相應的功能。這個不想說了,因爲用了半天都不行,Bug還太多,建議大家如非必要還是不要用這個模塊的好。



沒了PHP,很迷茫,然後很幸運地發現了expect。expect是交互式shell編程的利器,可以根據返回值來確定下面發送什麼命令,特別好用。我把自己編寫的遠程增加用戶的shell跟大家分享下(需要機器裝有expect,沒有的自己裝吧),腳本如下:



#!/usr/bin/expect

#腳本第一個參數是遠程服務器IP

set IP     [lindex $argv 0]

#遠程服務器用戶名(通常用root)

set USER [lindex $argv 1]

#遠程服務器用戶名的密碼

set PASSWD [lindex $argv 2]

#添加的新用戶

set Nuser [lindex $argv 3]

#新用戶的密碼

set Npasswd [lindex $argv 4]

#用spawn啓動一個ssh客戶端

spawn ssh -l $USER $IP

#如果是第一次連接,要保存密鑰再輸入密碼,如果不是第一次連接則輸入密碼

expect {

"yes/no" { send "yes\r"; exp_continue }

"password:" { send "$PASSWD\r" }

}

#如果不是root,要expect "$",下面不講了,很簡單

expect "*#"

send "useradd -s /bin/sh -d /home/$Nuser $Nuser\r"

expect "*#"

send "passwd $Nuser\r"

expect "*password:"

send "$Npasswd\r"

expect "*password:"

send "$Npasswd\r"

expect "*#"

send "exit\r


一、概述



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



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



例子:

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參數,這個參數表示指定的的字符串是一個正則表達式,而不是一個普通的字符串。對於上面這個例子裏是查找一個左方括號字符(其必須進行三次逃逸(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

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 {[file isfile $file]!=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腳本結束。





三、參考資源



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工具,包括可以檢查密碼是否是基於字典模式。這個工具同時是一個學習expect和tk的好實例。





另附:

創建時間:2001-04-29

文章屬性:轉載

文章來源:中國科大BBS站

文章提交:quack (quack_at_xfocus.org)



[版權聲明]


  Copyright(c) 1999


  本教程由*葫蘆娃*翻譯,並做了適當的修改,可以自由的用於非商業目的。

  但Redistribution時必須拷貝本[版權聲明]。      



[BUG]



  有不少部分,翻譯的時候不能作到“信,達”。當然了,任何時候都沒有做到“雅”,希望各位諒解。



[原著]


  Don Libes: National Institute of Standards and Technology

    [email protected]



[目錄]


  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

#!/usr/bin/expect -f

set ipaddress [lindex $argv 0]

set username [lindex $argv 1]

set password [lindex $argv 2]



spawn ftp $ipaddress

expect "*Name*"

send "$username\n"

expect "*Password:*"

send "$password\n"

expect "*ok*ftp>*"

send "get teste\n"

expect "*ftp>*"

send "quit\n"

expect eof

複製代碼





執行時如下

./ftp-down 192.168.1.1 temp temp

複製代碼


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章