代碼:
$exp = new Expect;
#new一個Expect.
$exp->spawn($command,@params);
#fork和exec一個新進程用來執行$command命令.
$exp->expect($timeout,$content);
#'-re'.在$timeout內,期待出現包含$content內容的正則表達式,如果不加re默認是精確匹配,也可以使用正則表達式,expect ($timeout,re=>'').
$exp->send($string);
#向終端發送字符
$exp->debug(0|1|2);
#打印debug 信息,不同的數字表示不同的bug級別
$exp->interact();
#和用戶進行交互,把控制權轉交給用戶
$exp->soft_close();
#軟關閉,直到$timeout時間到達,才關掉該進程
$exp->hard_close();
#硬關閉,立刻關閉該進程
$exp->match();
#返回匹配的結果
$exp->match_number();
#返回匹配的個數
$exp->send_slow();
# 就是讓command慢慢敲進去..
$exp->stty();
#它和外部命令 stty 類似,設定 terminal 的模式,-raw 還是 -cooked。開關回顯 echo 和 -echo。
exp_continue 一般在 expect 命令中使用,它可以避免寫第二次 expect 命令,即如果匹配上某個模式,然後需要做一些事情(如輸入密碼什麼的),之後需要繼續匹配,因此需要第二次 expect 命令。exp_continue 可以重複本次 expect 命令,因此也就不需要再次寫一個 expect 命令,而可以將兩個寫在一起。
我們想讓程序只在後臺跑就行了,不會跑到前面來影響我們.所以我們需要expect的關掉終端顯示的功能.代碼:
$Expect::Log_Stdout = 0;
$exp->log_stdout(0);
注:0爲關掉顯示,1爲打開
希望expect一直等到某個字符出現需要怎麼樣做
代碼:
$exp->expect(undef,sub {})
如果不希望expect等,就使用$exp->expect(0),這樣當前如果有這個字符就成功,沒有就跳過.
取得expect的命令行中有輸出,使用before來取得數據來進行處理
代碼:
my $read = $exp->before();
print $read;
注意,這個得到的內容,是從expect中匹配到的內容.
在使用expect時,日誌中有很多特別的符號,使用下面這個terminal type的設定,就會好了. 不知爲什麼,日誌文件中會有很多^[ ,^G之類的字符,如果用了下面的參數就行了,只有一個^M,這個用dos2unix就行了
代碼:
$ENV{TERM} = "xterm";
打開內部調試,這樣可以讓後續命令的診斷信息發送到 Expect 所控制的 stderr,這樣可以顯示出比如$exp->expect()中的內容是否匹配.這樣可以顯示看到顯示的字符,方便調試匹配規則,
代碼:
$exp->exp_internal(1);
注意:internal這個必須寫到spawn後面 打開debug模塊,可以顯示連接的過程,進程的退出之類的狀態
代碼:
$exp->debug(3);
記錄expect的日誌到一個文件
代碼:
$exp->log_stdout(1);
$exp->log_file("output.log");
#注意,這句一定要寫在Expect->spawn之後,不然expect會沒有日誌輸出
中間發送和匹配之類的過程.......
代碼:
$exp->log_file(undef);
在expect時,追加日誌到一個文件
代碼:
$exp->log_file("filename");
默認是追加,如果要修改就用
代碼:
$exp->log_file("filename", "w");
在perl的expect中怎麼發送控制字符,
比如
代碼:
ctrl+c control-G $exp->send(”\cG”);
control-c $exp->send(”\cC”;);
我設置了一個終端大小,但是修改窗口大小後,我的Expect不能發現窗口改變. 主要是遠程不能抓到本地的WINCH”window size changed”的信號,當我修改我本地終端大小時需要傳送信息給spawn.
代碼:
my $exp = new Expect;
$exp->slave->clone_winsize_from(\*STDIN);
$exp->spawn("ssh somehost);
$SIG{WINCH} = \&winch;
sub winch {
$exp->slave->clone_winsize_from(\*STDIN);
kill WINCH => $exp->pid if $exp->pid;
$SIG{WINCH} = \&winch;
}
$exp->interact();
一個簡單的Perl使用Expect自動登陸服務器的實例
代碼:
#!/usr/bin/perl
use Expect;
$Expect::Log_Stdout = 1;
$ENV{TERM} = "vt100";
my ($host,$pass) = ("host","passwd");
my $exp = Expect->new;
$exp = Expect->spawn("ssh -l root $host");
$exp->log_file("output.log", "w");
$exp->expect(2,[
qr/password:/i,
sub {
my $self = shift ;
$self->send("$pass\n");
exp_continue;
}
],
[
'connecting (yes/no)',
sub {
my $self = shift ;
$self->send("yes\n");
}
]
);
#$exp->interact() if ($exp->expect(undef,'#'));
$exp->send("uptime\n") if ($exp->expect(undef,'#'));
$exp->send("exit\n") if ($exp->expect(undef,'#'));
$exp->log_file(undef);
Expect 和 Expect::Simple 都可以從 CPAN 網站上直接下載,最新版本爲 Expect-1.21 和 Expect::Simple-0.04。比較方便的安裝方法是使用 Perl 自帶的包管理工具 cpan:
perl -MCPAN -e 'install Expect::Simple' |
請確保有足夠的執行權限,cpan 將自動解決模塊間依賴關係。由於 Expect::Simple 依賴於 Expect,安裝 Expect::Simple 的過程中,Expect 模塊以及另外的依賴模塊都會自動安裝完畢。
Expect 模塊詳解
與最初的 Expect 語言類似,Expect 模塊的主要功能也通過 spawn,expect,send 三個方法實現。
spawn:啓動目標程序
$obj = Expect->spawn( $command, @parameters ); |
spawn 是 Expect 類的主要方法之一,通過 fork 和 exec 啓動目標程序,若成功則返回 Expect 對象,否則返回 undef。參數 $command 指定目標程序,@parameters 爲可選參數。下面是一個簡單的例子:
$obj1 = Expect->spawn( "ftp 9.9.9.9" ); # 啓動 ftp 進程 $obj2 = Expect->spawn( "ftp", "9.9.9.9" ); # 與上一行等效 |
上述兩行執行結果相同,但其實際處理過程存在細微差別。一般情況下我們可以把完整的命令行 ( 甚至可以是複合命令,包括命令、參數、管道符、重定向符等 ) 都寫入 $command 而不指定 @parameters。
注:在 spawn 的實現中,$command 和 @parameters 都原封不動地傳遞給了 Perl 的 exec 函數。根據 exec 函數的說明文檔,如果傳遞進來的是多元列表參數,exec 直接將其傳遞給 execvp 系統調用;如果傳遞進來的是標量參數或者單元列表參數,exec 函數將檢查是否存在 shell 元字符 ( 如 | & ; ( ) < > 等 ),若存在,則將此參數交給系統 shell 進行解析,否則將其分詞後傳遞給 execvp 系統調用。因此如果 spawn 的是一個含有 shell 元字符的複合命令,我們一般只能將其完整寫入 $command。
expect:等待特定輸出
$obj->expect( $timeout, @match_patterns ); |
使用 Expect 對象的 expect 方法等待目標程序的特定輸出。參數列表中 $timeout 設定超時 ( 以秒爲單位 ),@match_patterns 提供一個或多個匹配模式,如果在設定時間內目標程序輸出結果和 @match_patterns 中某元素匹配則成功返回。缺省情況下 expect 使用精確匹配,若想使用正則表達式,可以在該模式元素前加 '-re' 前綴 :
$obj->expect( 10, 'match me exactly', '-re'=>'match\s+me\s+exactly' ); |
標量上下文中 expect 返回匹配模式在 @match_patterns 中的位置 ( 注意下標從 1 開始 ),若不成功則返回 undef。而列表上下文中 expect 返回一個包含詳細匹配信息的列表:
( $pos, $err, $match, $before, $after ) = $obj->expect( $timeout, @patterns ); |
其中 $pos 就是在標量環境中的返回值,$err 是出錯信息,$match 爲成功匹配的字串,$before 爲匹配字串之前的輸出部分,$after 爲匹配字串之後的輸出部分。下面這個例子 (matchinfo.pl 及其輸出結果 ) 具體說明了這些值的含義:
#! /usr/bin/perl # 9.125.13.44 是一臺 ftp 服務器,使用 ftp 命令登錄時的輸出爲: # Connected to 9.125.13.44. # 220 AIX6144 FTP server (Version 4.2 Tue Dec 22 14:13:26 CST 2009) ready. # 502 authentication type cannot be set to GSSAPI # 502 authentication type cannot be set to KERBEROS_V4 # KERBEROS_V4 rejected as an authentication type # Name (9.125.13.44:root):
use Expect; $obj = Expect->spawn( "ftp 9.125.13.44" ) or die "Couldn't spawn ftp, $!"; # 關閉目標程序的輸出顯示 $obj->log_stdout( 0 ); # 使用 slice 獲取列表環境下的返回值 @result{ "position", "error", "match", "before", "after" } = $obj->expect( 10, 'Name' ); # 查看匹配結果 print "$_ = $result{$_}\n" foreach ( keys %result ); # 關閉目標程序 $obj->soft_close( ); |
爲了使運行結果更加清楚,示例中使用$obj->log_stdout(0)關閉目標程序的輸出顯示。該程序的運行結果爲:
after = (9.125.13.44:root): match = Name error = position = 1 before = Connected to 9.125.13.44. 220 AIX6144 FTP server (Version 4.2 Tue Dec 22 14:13:26 CST 2009) ready. 502 authentication type cannot be set to GSSAPI 502 authentication type cannot be set to KERBEROS_V4 KERBEROS_V4 rejected as an authentication type |
Expect 對象也提供了單獨的方法來獲得這些匹配信息,如$obj->before( ),$obj->after( ),$obj->match( ),$obj->error( ) 等。
使用 -i 選項可以對多個或多組 Expect 對象同時執行 expect 操作:
$obj1 = Expect->spawn( "ftp 9.125.13.44" ) or die $!; $obj2 = Expect->spawn( "telnet 9.9.9.9" ) or die $!; $obj3 = Expect->spawn( "ssh 9.181.59.64" ) or die $!; expect ( $timeout, '-i', $obj1, '-re', qr/name/i, '-i', [$obj2, $obj3], '-re', qr/login/i, '-re', qr/password/i, ) |
本例使用函數風格的 expect 方法 ( 區別於通過對象調用 ),構造匿名數組的引用 [...] 來傳遞多個 Expect 對象。
此外在 $obj->expect($timeout, @match_patterns)中,@match_patterns 還可採用 [ $regexp, sub{},@opt_params ]的形式,根據不同模式來執行不同後續操作。邏輯流程較爲簡單時,利用此特點可以使代碼組織更加緊湊。下面的 telnet 登錄腳本 (exptelnet.pl) 採用了這種形式:
#! /usr/bin/perl use Expect;
my $PROMPT = '[\]\$\>\#]\s*$'; # 遠程系統的命令提示符模式 $obj = Expect->spawn( "telnet 9.125.13.44" ) or die "Couldn't spawn telnet, $!"; $obj->expect( 10, [ qr/login:\s*$/i, sub{ my $self = shift; $self->send( "root\r" ); exp_continue;} ], [ qr/password:\s*$/i, sub{ my $self = shift; $self->send( "zu88jie\r" ); exp_continue;} ], [ qr/$PROMPT/, sub{my $self=shift; $self->send( "logout\r" ); exp_continue_timeout;} ], ); |
示例中匿名函數返回 exp_continue 符號將重置等待時間並繼續執行 expect,使得一次 expect 調用可以完成多次匹配動作。與之相對的是返回exp_continue_timeout 符號,在繼續執行 expect 時不重置等待時間。
send:發送數據
$obj->send( @strings ); |
當交互式程序等待用戶輸入時,可以使用 send 方法向其提供輸入數據。需要注意,send 送出的數據可能會回顯在終端上 ( 具體與終端設置有關 ),此數據會進入 Expect 對象的匹配緩衝區,被下一個 expect 動作接收。爲了避免 send 數據對 expect 匹配造成混亂,一般可以使用 $obj->stty("-echo" )方法關閉終端回顯,或者在 spawn 前使用 $obj->raw_pty(1)將終端設定成 raw 模式 (raw 模式將關閉回顯,禁止回車 - 換行符翻譯 ),或者爲expect 提供更加精確的匹配模式。
log_file:設置日誌記錄
$obj->log_file( $filename | $filehandle | undef ); |
將交互過程的內容輸出到日誌文件能便於自動化腳本的追蹤和調試。通常使用文件名或者文件句柄作爲參數來指定具體日誌文件,例如:
$obj = Expect->spawn( "ftp 9.9.9.9" ); # 使用"w"選項截斷舊日誌 $obj->log_file( "./out.log", "w" ); |
debug:設置調試信息
$obj->debug( 0 | 1 | 2 | 3 ); |
參數 0 爲禁止調試信息 ( 缺省值 ),1 ~ 3 級詳細程度遞增。另一個相關的方法是 $object->exp_internal( 1 | 0 ),用以打開或關閉expect 對象的內部調試信息。將此標誌設置爲 1 可以看到expect 對象對輸出內容嘗試匹配的完整過程 (expdebug.pl):
#! /usr/bin/Perl use Expect; $obj = Expect->spawn( "ftp 9.125.13.44" ) or die $!; $obj->exp_internal( 1 ); # 打開對象內部調試信息 $obj->expect( 10, "Name" ); $obj->soft_close( ); |
運行結果爲:
Starting EXPECT pattern matching... ... 省略中間輸出 ... spawn id(3): Does `' match: pattern #1: -ex `Name'? No. ... 省略中間輸出 ... match: pattern #1: -ex `Name'? YES!! Before match string: `Connected to 9.125.13.44.\r\n220 AIX6144 FTP server ...' Match string: `Name' After match string: ` (9.125.13.44:root): ' Matchlist: () |
從結果中可以監視目標程序每一條輸出與 expect 模式的匹配情況,若匹配成功還能查看 before\match\after字串,這對於調試程序大有幫助。
interact:返回交互模式
$obj->interact( ); |
爲了適應某些特殊場合,我們可能需要將控制權交還給目標程序,此時只需使用 $obj->interact( )方法。
clear_accum 與 set_accum:操縱匹配緩衝區
清單 18.clear_accum 與 set_accum 方法原型
$obj->clear_accum( ); $obj->set_accum( $value ); |
expect 方法針對 Expect 對象的匹配緩衝區 (accumulator) 進行匹配嘗試,默認情況下每次匹配成功後,accumulator中 before 和 match 部分將被清除,下次匹配從 after 開始。但是 Expect 對象提供了 clear_accum 與 set_accum 方法改變這種行爲:使用 $obj>set_accum( $value )將緩衝區內容設置成 $value,使用 $obj>clear_accum( )清空緩衝區內容。具體用法參見如下代碼片段:
$obj->notransfer( 1 ); $obj->expect( $timeout, # 1 保留 accumulator 內容 , pattern1 將被再次匹配 [ "pattern1", sub { my $self = shift; ... } ], # 2 將 accumulator 內容設置爲 after string,即截斷 before 和 match string [ "pattern2", sub { my $self = shift; ...; $self->set_accum( $self->after( ) );} ], # 3 將 accumulator 內容清空 [ "pattern3", sub { my $self = shift; ...; $self->clear_accum( );} ], ); |
示例中 $obj->notransfer( 0 | 1 )方法用於設置是否保留匹配緩衝區內容。0 是默認行爲 ( 清除 before 和 match);1 爲保留所有內容。保留緩衝區內所有內容會導致原先匹配過的模式被再次匹配 (#1)。我們也可手動設置 accumulator 內容來影響下一次匹配 (#2 #3)。
Expect::Simple 模塊詳解
Expect::Simple 對 Expect 模塊進行了封裝,隱藏其內部複雜機制。此模塊處理一些簡單的應用已經足夠。
new;構造方法
$obj = Expect::Simple->new( \%attr ); |
創建 Expect::Simple 對象的同時啓動目標程序,需傳遞關聯數組 %attr 的引用作爲參數。通過該關聯數組設置目標程序的相關信息 ( 如命令、超時、提示符等 ),因此該數組必須包含 Prompt,DisconnectCmd,Cmd 等鍵值。
Cmd 鍵指定目標程序及其參數。它的值可以是標量或者是數組引用:Cmd =>$command或 Cmd => [ $command, $arg1, $arg2, ...]。在 Expect::Simple 的實現中 $command 或 $command, $arg1, $arg2, ...都被直接傳給 Expect 的 spawn 方法,因此前面對 spawn 方法的分析在這裏同樣適用:可以把完整的命令行寫入 $command;對不含 shell 元字符的簡單命令,也可以使用分拆形式,傳遞數組引用。
Prompt 鍵指定一個或一組預期的輸入提示 ( 支持正則表達式 ):Prompt => ['ftp>', 'telnet>',-re => 'prompt\d+>\s+']。Prompt 鍵值相當於 Expect 模塊中 expect 方法的匹配模式參數。
DisconnectCmd 鍵指定退出目標程序所用的指令:DisconnectCmd=> 'exit'。
Timeout 鍵設定超時 ( 缺省值爲1000 秒 ),如果目標程序在設定時間內未響應則返回。Expect::Simple在目標程序啓動之前設置超時且使用全局設定,因此無法區分處理目標程序執行過程中立即響應部分和有明顯延時的部分。在設定超時值時需要考慮響應最慢的階 段。
RawPty 鍵用於設置終端,對應於 Expect 模塊的 raw_pty 方法,默認值爲 0。
Verbose 鍵用於設定輸出內容的詳細程度:Verbose => 3。
send:順序發送數據
使用 $obj->send( $cmd | @cmds)向目標程序發送一條或依次發送多條數據。每條數據送達後等待下一個輸入提示以發送下一條數據。以 ssh 自動登錄爲例,serialsend.pl 演示如何順序發送多條數據:
#! /usr/bin/perl use Expect::Simple; my %attr = ( Prompt => [ -re => qr/password:\s*$/i, -re => qr/[\]\$\>\#]\s*$/ ], Cmd => 'ssh [email protected]', DisconnectCmd => 'exit', Verbose => 3, Timeout => 10, ); my $obj = Expect::Simple->new( \%attr ); # 自動輸入密碼,執行 ls 命令,退出。由於已設定退出命令,此處不必再發送 exit $obj->send( "zhu88jie", "ls" ); |
輸出結果:
Running command...done. Sending `zhu88jie' Sending `ls' Disconnecting. |
若想看到 ls 命令的執行輸出,只需將 Verbose 值設爲 4。
before,after,match_str,error:查看匹配信息
這些方法和 Expect 模塊中的 before,after,match,error 一一對應,不再詳細闡述。需要注意由於 Expect 對象的緩衝區在匹配過程中會不斷更新,在依次發送多條數據的情況下,使用這些方法只能查看最近一次匹配結果。
expect_handle:使用內部對象
Expect::Simple 封裝了 Expect 模塊的細節,提供簡單易用的接口,但是當 Expect::Simple 提供的功能無法以滿足需求時,還可以使用這個方法直接操作內部 Expect 對象。
應用示例
Expect 模塊使用示例
本例 (ftpdemo.pl) 演示如何使用 Expect 模塊從 ftp 上自動下載文件。
#!/usr/bin/perl # Usage: ftpdemo.pl [-u username] [-p password] host file1 [file2 file3 ...] use Expect; use Getopt::Std;
# 設置缺省用戶名和密碼 my %opts = ( u=>'anonymous', p=>'[email protected]' ); # 解析 -u 和 -p 選項 getopt( 'up', \%opts ); $host = shift @ARGV; # 下一個參數是 ftp 服務器地址 @files = @ARGV; # 餘下的參數爲需要下載的文件
# 啓動 ftp 進程 print "Starting ftp session with server $host ...\n"; $ftp = Expect->spawn( "ftp $host" ) or die "Couldn't spawn ftp, $!";
# 屏蔽多餘輸出 $ftp->log_stdout( 0 );
# 等待用戶名輸入提示 unless ( $ftp->expect(30, -re=>qr/name \(.*?\):\s*$/i) ) { die "Never got username prompt on $host, ".$ftp->error( )."\n"; }
# 發送用戶名數據 print "Sending username ($opts{u}) ... \n"; $ftp->send( "$opts{u}\r" );
# 等待密碼輸入提示 unless ( $ftp->expect( 30, -re=>qr/password:\s*$/i ) ) { die "Never got password prompt on $hostname, ".$ftp->error( )."\n"; }
# 發送密碼 print "Sending password ( $opts{p} ) ... \n"; $ftp->send( "$opts{p}\r" );
# 等待 ftp 命令行提示 unless ( $ftp->expect(30,"ftp>") ) { die "Never got ftp prompt after sending username, ".$ftp->error( )."\n"; }
# 下載文件 foreach my $file ( @files ) { print "Getting the $file ... \n"; $ftp->send( "get $file\r" ); unless ( $ftp->expect( 30,"ftp> " ) ) { die "Never got ftp prompt after attempting to get $file, ".$ftp->error( )."\n"; } }
# 斷開 ftp 連接 print "Download finished. Disconnecting ... \n"; $ftp->send( "bye\r" ); $ftp->soft_close( ); print "Done.\n"; |
使用此腳本從 9.125.13.44 的 ftp 根目錄下載 diskusage.log 文件,運行結果:
# ./ftpdemo.pl -u root -p zhu88jie 9.125.13.44 diskusage.log Starting ftp session with server 9.125.13.44 ... Sending username (root) ... Sending password (zhu88jie) ... Getting the diskusage.log ... Download finished. Disconnecting ... Done. # |
Expect::Simple 模塊使用示例
本例演示如何使用 Expect::Simple 對目標程序的輸入輸出進行測試。腳本 target.pl 作爲此例中的目標程序。target.pl 循環讀取用戶輸入,直到用戶輸入 quit 時退出。
#! /usr/bin/perl my $p = 'tpt %d> '; # 輸入提示符 printf( $p, 0 ); while( <> ){ do { print "byebye\n"; last } if /quit/; # 如果用戶輸入 quit 則輸出"byebye",跳出循環 print uc( $_ ); # 否則以大寫的形式輸出用戶本次輸入 printf( $p, $. ); # 更新輸入提示符,$. 爲讀入行數計數器 } print "quit> \n"; # 打印結束輸入提示符 |
某次運行結果如下:
# ./target.pl tpt 0> 1 1 tpt 1> 2 2 tpt 2> quit byebye quit> # |
exptest.pl 腳本用來模擬用戶輸入不同數據以測試目標程序的功能:
#! /usr/bin/perl use Test::More tests => 4; # 計劃執行的測試用例總數 use Expect::Simple;
my %attr = ( Cmd => "./target.pl" DisconnectCmd => 'quit', Prompt => [ -re => 'tpt\s\d+> ', 'quit> '], RawPty => 1, Verbose =>3, ); my $res; # 實際輸出結果 my $obj = Expect::Simple->new( \%attr );
# 測試用例 1,用戶輸入 a,預期目標程序輸出 A $obj->send( 'a' ); chomp( $res = $obj->before ); is( $res, 'A', 'Test case 1: Input "a" -> Output "A"' );
# 測試用例 2,用戶輸入 b,預期目標程序輸出 B $obj->send( 'b' ); chomp( $res = $obj->before ); is( $res, 'B', 'Test case 2: Input "b" -> Output "B"' );
# 測試用例 3,用戶輸入 quit,預期目標程序輸出 byebye $obj->send( 'quit' ); chomp( $res = $obj->before ); is( $res, 'byebye', 'Test case 3: Input "quit" -> Output "byebye"' );
# 測試用例 4,目標程序輸出 byebye 後應直接輸出 quit> is ( $obj->match_str, 'quit> ', 'Test case 4: Output "byebye" -> Output "quit>" ' ); |
程序引用了 Test::More 模塊,Test::More 是一個編寫測試腳本的框架,這裏只介紹程序中涉及的部分,更多內容請查閱 CPAN 網站上相關信息。整個測試過程使用 is 函數來判斷用例的執行結果:is ( $got, $expected, $comments )。該函數判斷 $got與 $expected 是否相等,相等則通過,輸出“ok”,否則輸出“not ok”。
在target.pl 循環執行的過程中,如果用戶未輸入 quit,則每次 expect 都匹配到輸入提示符“tpt\s\d+>”上,而終端回顯又被關閉,因此匹配緩衝區的 before 部分即爲目標程序上次輸出的結果。程序中使用 $res 變量獲得目標程序的實際輸出,與預期結果比較判斷測試用例是否通過。exptest.pl 實際執行的結果爲:
# ./exptest.pl 1..4 Running command...done. Sending `a' ok 1 - Test case 1: Input "a" -> Output "A" Sending `b' ok 2 - Test case 2: Input "b" -> Output "B" Sending `quit' ok 3 - Test case 3: Input "quit" -> Output "byebye" ok 4 - Test case 4: Output "byebye" -> Output "quit>" Disconnecting. # |