//http://jiangzhixiang123.blog.163.com/blog/static/27802062201010102422192/
Pexpect 是一個用來啓動子程序並對其進行自動控制的 Python 模塊,它可以用來和像 ssh、ftp、passwd、telnet 等命令行程序進行自動交互。本文介紹 Pexpect 的主要用法和在實際應用中的注意點。 Python 語言的愛好者,系統管理人員,部署及測試人員都能使用 Pexpect 在自己的工作中實現與命令行交互的自動化。
概述
Pexpect 是 Don Libes 的 Expect 語言的一個 Python 實現,是一個用來啓動子程序,並使用正則表達式對程序輸出做出特定響應,以此實現與其自動交互的 Python 模塊。 Pexpect 的使用範圍很廣,可以用來實現與 ssh, ftp , telnet 等程序的自動交互;可以用來自動複製軟件安裝包並在不同機器自動安裝;還可以用來實現軟件測試中與命令行交互的自動化。
下載
Pexpect 可以從 SourceForge 網站下載。本文介紹的示例使用的是 2.3 版本,如不說明測試環境,默認運行操作系統爲 fedora 9 並使用 Python 2.5 。
安裝
download pexpect-2.3.tar.gz
tar zxvf pexpect-2.3.tar.gz
cd pexpect-2.3
python setup.py install (do this as root)
依賴
Python 版本 2.4 , 2.5
pty module ,pty 是任何 Posix 系統標準庫的一部分。
由於其依賴 pty module ,所以 Pexpect 還不能在 Windows 的標準 python 環境中執行,如果想在 Windows 平臺使用,可以使用在 Windows 中運行 Cygwin 做爲替代方案。
遵循 MIT 許可證
根據 Wiki 對 MIT License 的介紹該模塊被授權人有權利使用、複製、修改、合併、出版發行、散佈、再授權及販售軟件及軟件的副本。被授權人可根據程序的需要修改授權條款爲適當的內容。在軟件和軟件的所有副本中都必須包含版權聲明和許可聲明。
Pexpect 提供的 run() 函數:
清單 1. run() 的定義
run(command,timeout=-1,withexitstatus=False,events=None,extra_args=None,\
logfile=None, cwd=None, env=None)
函數run可以用來運行命令,其作用與 Python os 模塊中system()函數相似。run()是通過Pexpect類實現的。
如果命令的路徑沒有完全給出,則run會使用which命令嘗試搜索命令的路徑。
清單 2. 使用 run() 執行 svn 命令
from pexpect import *
run (“svn ci -m ‘automatic commit’ my_file.py”)
與os.system()不同的是,使用run()可以方便地同時獲得命令的輸出結果與命令的退出狀態。
清單 3. run() 的返回值
from pexpect import *
(command_output, exitstatus) = run (‘ls -l /bin’, withexitstatus=1)
command_out中保存的就是 /bin 目錄下的內容。
Pexpect 提供的 spawn() 類:
使用 Pexpect 啓動子程序
清單 4. spawn 的構造函數
class spawn
__init__(self,command,args=[],timeout=30,maxread=2000,searchwindowsize=None,
logfile=None, cwd=None, env=None)
spawn 是 Pexpect 模塊主要的類,用以實現啓動子程序,它有豐富的方法與子程序交互從而實現用戶對子程序的控制。它主要使用pty.fork()生成子進程,並調用exec()系列函數執行 command 參數的內容。
可以這樣使用:
清單 5. spawn() 使用示例
child=pexpect.spawn(‘/usr/bin/ftp’) # 執行 ftp 客戶端命令
child=pexpect.spawn(‘/usr/bin/[email protected]’) # 使用 ssh 登錄目標機器
child=pexpect.spawn(‘ls-latr/tmp’)# 顯示 /tmp 目錄內容
當子程序需要參數時,還可以使用一個參數的列表:
清單 6. 參數列表示例
child=pexpect.spawn(‘/usr/bin/ftp’,[])
child=pexpect.spawn(‘/usr/bin/ssh’,['[email protected]'])
child=pexpect.spawn(‘ls’,['-latr','/tmp'])
在構造函數中,maxread 屬性指定了 Pexpect 對象試圖從 tty 一次讀取的最大字節數,它的默認值是 2000 字節。
由於需要實現不斷匹配子程序輸出, searchwindowsize 指定了從輸入緩衝區中進行模式匹配的位置,默認從開始匹配。
logfile 參數指定了 Pexpect 產生的日誌的記錄位置,
例如:
清單 7. 記錄日誌
child=pexpect.spawn(‘some_command’)
fout=file(‘mylog.txt’,'w’)
child.logfile=fout
還可以將日誌指向標準輸出:
清單 8. 將日誌指向標準輸出
child=pexpect.spawn(‘some_command’)
child.logfile=sys.stdout
如果不需要記錄向子程序輸入的日誌,只記錄子程序的輸出,可以使用:
清單 9. 記錄輸出日誌
child=pexpect.spawn(‘some_command’)
child.logfile_send=sys.stdout
使用 Pexpect 控制子程序
爲了控制子程序,等待子程序產生特定輸出,做出特定的響應,可以使用 expect 方法
清單 10. expect() 定義
expect(self, pattern, timeout=-1, searchwindowsize=None)
在參數中: pattern 可以是正則表達式, pexpect.EOF , pexpect.TIMEOUT ,或者由這些元素組成的列表。
需要注意的是,當 pattern 的類型是一個列表時,且子程序輸出結果中不止一個被匹配成功,則匹配返回的結果是緩衝區中最先出現的那個元素,或者是列表中最左邊的元素。使用 timeout 可以指定等待結果的超時時間,該時間以秒爲單位。當超過預訂時間時, expect 匹配到 pexpect.TIMEOUT 。
如果難以估算程序運行的時間,可以使用循環使其多次等待直至等待運行結束:
清單 11. 使用循環
while True:
index = child.expect(["suc","fail",pexpect.TIMEOUT])
if index == 0:
break
elif index == 1:
return False
elif index == 2:
pass #continue to wait
expect()在執行中可能會拋出兩種類型的異常分別是 EOF and TIMEOUF , 其中 EOF 通常代表子程序的退出, TIMEOUT 代表在等待目標正則表達式中出現了超時。
清單 12. 使用並捕獲異常
try:
index=pexpect(['good','bad'])
ifindex==0:
do_something()
elifindex==1:
do_something_else()
exceptEOF:
do_some_other_thing()
exceptTIMEOUT:
do_something_completely_different()
此時可以將這兩種異常放入 expect 等待的目標列表中:
清單 13. 避免異常
index=p.expect(['good','bad',pexpect.EOF,pexpect.TIMEOUT])
ifindex==0:
do_something()
elifindex==1:
do_something_else()
elifindex==2:
do_some_other_thing()
elifindex==3:
do_something_completely_different()
expect 不斷從讀入緩衝區中匹配目標正則表達式,當匹配結束時 pexpect 的 before 成員中保存了緩衝區中匹配成功處之前的內容 pexpect 的 after 成員保存的是緩衝區中與目標正則表達式相匹配的內容。
清單 14. 打印 before 成員的內容
child = pexpect.spawn(‘/bin/ls /’)
child.expect (pexpect.EOF)
print child.before
此時child.before保存的就是在根目錄下執行 ls 命令的結果
清單 15. send 系列函數
send(self, s)
sendline(self, s=”)
sendcontrol(self, char)
這些方法用來向子程序發送命令,模擬輸入命令的行爲。
與send()不同的是sendline()會額外輸入一個回車符,更加適合用來模擬對子程序進行輸入命令的操作。
當需要模擬發送 ” Ctrl+c ” 的行爲時,還可以使用sendcontrol()發送控制字符。
清單 16. 發送 ctrl+c
child.sendcontrol(‘c’)
由於send()系列函數向子程序發送的命令會在終端顯示,所以也會在子程序的輸入緩衝區中出現,因此不建議使用 expect 匹配最近一次sendline()中包含的字符。否則可能會在造成不希望的匹配結果。
清單 17. interact() 定義
interact(self, escape_character = chr(29), input_filter = None, output_filter = None)
Pexpect 還可以調用interact()讓出控制權,用戶可以繼續當前的會話控制子程序。用戶可以敲入特定的退出字符跳出,其默認值爲“ ^] ”。
下面展示一個使用 Pexpect 和 ftp 交互的實例
清單 18. ftp 交互的實例:
# This connects to the openbsd ftp site and
# downloads the README file.
import pexpect
child = pexpect.spawn (‘ftp ftp.openbsd.org’)
child.expect (‘Name .*: ‘)
child.sendline (‘anonymous’)
child.expect (‘Password:’)
child.sendline (‘[email protected]’)
child.expect (‘ftp> ‘)
child.sendline (‘cd pub/OpenBSD’)
child.expect(‘ftp> ‘)
child.sendline (‘get README’)
child.expect(‘ftp> ‘)
child.sendline (‘bye’)
該程序與 ftp 做交互,登錄到 ftp.openbsd.org ,當提述輸入登錄名稱和密碼時輸入默認用戶名和密碼,當出現 ” ftp> ” 這一提示符時切換到 pub/OpenBSD 目錄並下載 README 這一文件。
以下實例是上述方法的綜合應用,用來建立一個到遠程服務器的 telnet 連接,並返回保存該連接的 pexpect 對象。
清單 19. 登錄函數:
import re,sys,os
from pexpect import *
def telnet_login(server,user, passwd,shell_prompt= “ #|-> ” ):
”"”
@summary: This logs the user into the given server. It uses the ‘shell_prompt’
to try to find the prompt right after login. When it finds the prompt
it immediately tries to reset the prompt to ‘#UNIQUEPROMPT#’ more easily matched.
@return: If Login successfully ,It will return a pexpect object
@raise exception: RuntimeError will be raised when the cmd telnet
failed or the user and passwd do not match
@attention:1. shell_prompt should not include ‘$’,on some server,
after sendline(passwd) the pexpect object will read a ‘$’.
2.sometimes the server’s output before its shell prompt will contain ‘#’
or ‘->’ So the caller should kindly assign the shell prompt
”"”
if not server or not user \
or not passwd or not shell_prompt:
raise RuntimeError, “You entered empty parameter for telnet_login ”
child = pexpect.spawn(‘telnet %s’ % server)
child.logfile_read = sys.stdout
index = child.expect (['(?i)login:', '(?i)username', '(?i)Unknown host'])
if index == 2:
raise RuntimeError, ‘unknown machine_name’ + server
child.sendline (user)
child.expect (‘(?i)password:’)
child.logfile_read = None # To turn off log
child.sendline (passwd)
while True:
index = child.expect([pexpect.TIMEOUT,shell_prompt])
child.logfile_read = sys.stdout
if index == 0:
if re.search(‘an invalid login’, child.before):
raise RuntimeError, ‘You entered an invalid login name or password.’
elif index == 1:
break
child.logfile_read = sys.stdout # To tun on log again
child.sendline( “ PS1=#UNIQUEPROMPT# ” )
#This is very crucial to wait for PS1 has been modified successfully
#child.expect( “ #UNIQUEPROMPT# ” )
child.expect(“%s.+%s” % ( “ #UNIQUEPROMPT# ” , “ #UNIQUEPROMPT# ” ))
return child
Pxssh 類的使用:
Pxssh 做爲 pexpect 的派生類可以用來建立一個 ssh 連接,它相比其基類增加了如下方法:
login()建立到目標機器的 ssh 連接
logout()釋放該連接
prompt()等待提示符,通常用於等待命令執行結束
下面的示例連接到一個遠程服務器,執行命令並打印命令執行結果。
該程序首先接受用戶輸入用戶名和密碼,login 函數返回一個 pxssh 對象的鏈接,然後調用sendline()分別輸入 ” uptime ” , ” ls ” 等命令並打印命令輸出結果。
清單 20. pxssh 示例
import pxssh
import getpass
try:
s = pxssh.pxssh()
hostname = raw_input(‘hostname: ‘)
username = raw_input(‘username: ‘)
password = getpass.getpass(‘password: ‘)
s.login (hostname, username, password)
s.sendline (‘uptime’) # run a command
s.prompt() # match the prompt
print s.before # print everything before the propt.
s.sendline (‘ls -l’)
s.prompt()
print s.before
s.sendline (‘df’)
s.prompt()
print s.before
s.logout()
except pxssh.ExceptionPxssh, e:
print “pxssh failed on login.”
print str(e)
Pexpect 使用中需要注意的問題:
spawn() 參數的限制
在使用 spawn 執行命令時應該注意,Pexpect 並不與 shell 的元字符例如重定向符號 > ,>>, 管道 | ,還有通配符 * 等做交互,所以當想運行一個帶有管道的命令時必須另外啓動一個 shell ,爲了使代碼清晰,以下示例使用了參數列表例如:
清單 21. 啓動新的 shell 執行命令
shell_cmd=’ls-l|grepLOG>log_list.txt’
child=pexpect.spawn(‘/bin/bash’,['-c',shell_cmd])
child.expect(pexpect.EOF)
與線程共同工作
Perl 也有 expect 的模塊 Expect-1.21,但是 perl 的該模塊在某些操作系統例如 fedora 9 或者 AIX 5 中不支持在線程中啓動程序執行 , 以下實例試圖利用多線同時程登錄到兩臺機器進行操作,不使用線程直接調用時 sub1() 函數可以正常工作,但是使用線程時在 fedora9 和 AIX 5 中都不能正常運行。
清單 22. perl 使用 expect 由於線程和 expect 共同使用導致不能正常工作的程序
use threads;
use Expect;
$timeout = 5;
my $thr = threads->create(\&sub1(first_server));
my $thr2 = threads->create(\&sub1(second_server));
sub sub1
{
my $exp = new Expect;
$exp -> raw_pty(1);
$exp -> spawn (“telnet”,$_[0]) or die “cannot access telnet”;
$exp -> expect ( $timeout, -re=>’[Ll]ogin:’ );
$exp -> send ( “user\n”);
$exp -> expect ( $timeout, -re=>’[Pp]assword:’ );
$exp -> send ( “password\n” );
$exp -> expect ( $timeout, -re=>” #” );
$exp -> send ( “date\n” );
$exp -> expect ( $timeout, -re=>’\w\w\w \w\w\w \d{1,2} \d\d:\d\d:\d\d \w\w\w \d\d\d\d’);
$localtime=$exp->match();
print “\tThe first server ’ s time is : $localtime\n”;
$exp -> soft_close ();
}
print “This is the main thread!”;
$thr->join();
$thr2->join();
Pexpect 則沒有這樣的問題,可以使用多線程並在線程中啓動程序運行。但是在某些操作系統如 fedora9 中不可以在線程之間傳遞 Pexpect 對象。
對正則表達式的支持
在使用expect()時 , 由於 Pexpect 是不斷從緩衝區中匹配,如果想匹配行尾不能使用 “ $ ” ,只能使用 “ \r\n ”代表一行的結束。另外其只能得到最小匹配的結果,而不是進行貪婪匹配,例如 child.expect (‘.+’) 只能匹配到一個字符。
應用實例:
在實際系統管理員的任務中,有時需要同時管理多臺機器,這個示例程序被用來自動編譯並安裝新的內核版本,並重啓。它使用多線程,每個線程都建立一個到遠程機器的 telnet 連接並執行相關命令。 該示例會使用上文中的登錄函數。
清單 23. 管理多臺機器示例
03 |
PROMPT = “ #UNIQUEPROMPT# ” |
04 |
class RefreshKernelThreadClass(threading.Thread): |
05 |
"""The thread to downLoad the kernel and install it on a new server """ |
06 |
def __init__( self ,server_name,user,passwd): |
07 |
threading.Thread.__init__( self ) |
08 |
self .server_name_ = server_name |
11 |
self .result_ = [] # the result information of the thread |
14 |
self .setName( self .server_name_) # set the name of thread |
17 |
#call the telnet_login to access the server through telnet |
18 |
child = telnet_login( self .server_name_, self .user_, self .passwd_) |
20 |
except RuntimeError,ex: |
21 |
info = "telnet to machine %s failed with reason %s" % ( self .server_name_, ex) |
22 |
self .result_. = ( False , self .server_name_ + info) |
25 |
child.sendline(' cd ~ / Download / dw_test && \ |
27 |
tar zxvf linux - 2.6 . 28.tar .gz && \ |
29 |
&& make mrproper && make allyesconfig and |
30 |
make - j 4 && make modules && \ |
31 |
make modules install && make install') |
32 |
# wail these commands finish |
34 |
index = child.expect([PROMPT,pexpect.TIMEOUT,pexpect.EOF]) |
40 |
self .result_ = ( False , 'Sub process exit abnormally ' ) |
44 |
child.sendline( 'shutdown -Fr' ) |
47 |
while retry_times > 0 : |
48 |
index_shutdown = child.expect([ "Unmounting the file systems" , |
51 |
if index_shutdown = = 0 or index_shutdown = = 1 : |
53 |
elif index_shutdown = = 2 : |
54 |
retry_times = retry_times - 1 |
56 |
self .result_ = ( False , 'Cannot shutdown ' ) |
60 |
def refresh_kernel(linux_server_list,same_user,same_passwd): |
62 |
@summary: The function is used to work on different linux servers to download |
63 |
the same version linux kernel, conpile them and reboot all these servers |
64 |
To keep it simple we use the same user id and password on these servers |
66 |
if not type (linux_server_list) = = list : |
67 |
return ( False , "Param %s Error!" % linux_server_list) |
69 |
if same_user is None or same_passwd is None or not |
70 |
type (same_user) = = str or not type (same_passwd) = = str : |
71 |
return ( False , "Param Error!" ) |
74 |
# start threads to execute command on the remote servers |
75 |
for i in range ( len (linux_server_list)): |
76 |
thread_list[i] = RefreshKernelThreadClass(linux_server_list[i], same_user,same_passwd) |
77 |
thread_list[i].start() |
79 |
# wait the threads finish |
80 |
for i in range ( len (linux_server_list)): |
83 |
for i in range ( len (linux_server_list)): |
84 |
if thread_list[ 0 ].result_[ 0 ] = = False : |
89 |
if __name__ = = "__main__" : |
90 |
refresh_kernel(server_list, "test_user" , "test_passwd" ) |