http://www.cnblogs.com/dkblog/archive/2013/03/20/2970738.html

http://www.ibm.com/developerworks/cn/linux/l-cn-pexpect2/index.html

http://www.cnblogs.com/dkblog/archive/2013/03/20/2970738.html



python Pexpect

Pexpect 是一個用來啟動子程序并對其進行自動控制的純 Python 模塊。 Pexpect 可以用來和像 ssh、ftp、passwd、telnet 等命令行程序進行自動交互。繼第一部分《探索 Pexpect,第 1 部分:剖析 Pexpect 》介紹了 Pexpect 的基礎和如何使用后,本文將結合具體實例入手,詳細介紹 Pexpect 的用法和在實際應用中的注意點。

概述

通過本系列第一部分《探索 Pexpect,第 1 部分:剖析 Pexpect 》(請參閱參考資料)的介紹,相信大家已經對 Pexpect 的用法已經有了比較全面的了解,知道 Pexpect 是個純 Python 語言實現的模塊,使用其可以輕松方便的實現與 ssh、ftp、passwd 和 telnet 等程序的自動交互,但是讀者的理解還可能只是停留在理論基礎上,本文將從實際例子入手具體介紹 Pexpect 的使用場景和使用心得體驗,實例中的代碼讀者都可以直接拿來使用,相信會對大家產生比較大的幫助。以下是本文所要介紹的所有 Pexpect 例子標題:

  • 例 1:ftp 的使用(注:spawn、expect 和 sendline 的使用)

  • 例 2:記錄 log(注:logfile、logfile_sendlogfile_read的使用)

  • 例 3:ssh 的使用

  • 例 4:pxssh 的使用

  • 例 5:telnet 的使用(注:interact 的使用)

  • pexpect 使用 tips

    • 調試 pexpect 程序的 tips

    • pexpect 不會解釋 shell 中的元字符

    • EOF 異常和 TIMEOUT 異常

    • 使用 run() 來替代某些的 spawn 的使用

    • expect_exact() 的使用

    • expect() 中正則表達式的使用 tips

    • isalive() 的使用 tips

    • delaybeforesend 的使用 tips




回頁首



例 1:ftp 的使用

本例實現了如下功能:ftp 登錄到 develperWorks.ibm.com 主機上,并用二進制傳輸模式下載一個名叫 rmall的文件。


清單 1. ftp 的例子代碼

#!/usr/bin/env?pythonimport?pexpect
#?即將?ftp?所要登錄的遠程主機的域名
ipAddress?=?'develperWorks.ibm.com'
#?登錄用戶名
loginName?=?'root'
#?用戶名密碼
loginPassword?=?'passw0rd'#?拼湊?ftp?命令
cmd?=?'ftp?'?+?ipAddress
#?利用?ftp?命令作為?spawn?類構造函數的參數,生成一個?spawn?類的對象
child?=?pexpect.spawn(cmd)
#?期望具有提示輸入用戶名的字符出現
index?=?child.expect(["(?i)name",?"(?i)Unknown?host",?pexpect.EOF,?pexpect.TIMEOUT])
#?匹配到了?"(?i)name",表明接下來要輸入用戶名
if?(?index?==?0?):#?發送登錄用戶名?+?換行符給子程序.child.sendline(loginName)#?期望?"(?i)password"?具有提示輸入密碼的字符出現.index?=?child.expect(["(?i)password",?pexpect.EOF,?pexpect.TIMEOUT])#?匹配到了?pexpect.EOF?或?pexpect.TIMEOUT,表示超時或者?EOF,程序打印提示信息并退出.if?(index?!=?0):print?"ftp?login?failed"child.close(force=True)#?匹配到了密碼提示符,發送密碼?+?換行符給子程序.child.sendline(loginPassword)#?期望登錄成功后,提示符?"ftp>"?字符出現.index?=?child.expect(?['ftp>',?'Login?incorrect',?'Service?not?available',pexpect.EOF,?pexpect.TIMEOUT])#?匹配到了?'ftp>',登錄成功.if?(index?==?0):print?'Congratulations!?ftp?login?correct!'#?發送?'bin'+?換行符給子程序,表示接下來使用二進制模式來傳輸文件.child.sendline("bin")print?'getting?a?file...'#?向子程序發送下載文件?rmall?的命令.child.sendline("get?rmall")#?期望下載成功后,出現?'Transfer?complete.*ftp>',其實下載成功后,#?會出現以下類似于以下的提示信息:#????200?PORT?command?successful.#????150?Opening?data?connection?for?rmall?(548?bytes).#????226?Transfer?complete.#????548?bytes?received?in?0.00019?seconds?(2.8e+03?Kbytes/s)#?所以直接用正則表達式?'.*'?將?'Transfer?complete'?和提示符?'ftp>'?之間的字符全省去.index?=?child.expect(?['Transfer?complete.*ftp>',?pexpect.EOF,?pexpect.TIMEOUT]?)#?匹配到了?pexpect.EOF?或?pexpect.TIMEOUT,表示超時或者?EOF,程序打印提示信息并退出.if?(index?!=?0):print?"failed?to?get?the?file"child.close(force=True)#?匹配到了?'Transfer?complete.*ftp>',表明下載文件成功,打印成功信息,并輸入?'bye',結束?ftp?session.print?'successfully?received?the?file'child.sendline("bye")#?用戶名或密碼不對,會先出現?'Login?incorrect',然后仍會出現?'ftp>',但是?pexpect?是最小匹配,不是貪婪匹配,#?所以如果用戶名或密碼不對,會匹配到?'Login?incorrect',而不是?'ftp>',然后程序打印提示信息并退出.elif?(index?==?1):print?"You?entered?an?invalid?login?name?or?password.?Program?quits!"child.close(force=True)#?匹配到了?'Service?not?available',一般表明?421?Service?not?available,?remote?server?has#?closed?connection,程序打印提示信息并退出.#?匹配到了?pexpect.EOF?或?pexpect.TIMEOUT,表示超時或者?EOF,程序打印提示信息并退出.else:print?"ftp?login?failed!?index?=?"?+?indexchild.close(force=True)#?匹配到了?"(?i)Unknown?host",表示?server?地址不對,程序打印提示信息并退出
elif?index?==?1?:print?"ftp?login?failed,?due?to?unknown?host"child.close(force=True)
#?匹配到了?pexpect.EOF?或?pexpect.TIMEOUT,表示超時或者?EOF,程序打印提示信息并退出
else:print?"ftp?login?failed,?due?to?TIMEOUT?or?EOF"child.close(force=True)

?

注:

  • 運行后,輸出結果為:

Congratulations!?ftp?login?correct!
getting?a?file...
successfully?received?the?file

?

  • 本例 expect 函數中的 pattern 使用了 List,并包含了 pexpect.EOF和pexpect.TIMEOUT,這樣出現了超時或者 EOF,不會拋出 expection 。(關于 expect() 函數的具體使用,請參閱參考資料)

  • 如 果程序運行中間出現了錯誤,如用戶名密碼錯誤,超時或者 EOF,遠程 server 連接不上,都會使用 c hild.close(force=True) 關掉 ftp 子程序。調用 close 可以用來關閉與子程序的 connection 連接,如果你不僅想關閉與子程序的連接,還想確保子程序是真的被 terminate 終止了,設置參數 force=True,其最終會調用 c hild.kill(signal.SIGKILL) 來殺掉子程序。

?



回頁首


例 2:記錄 log

本例實現了如下功能:運行一個命令,并將該命令的運行輸出結果記錄到 log 文件中 ./command.py [-a] [-c command] {logfilename} -c 后接的是要運行的命令的名字,默認是“ls -l”; logfilename 是記錄命令運行結果的 log 文件名,默認是“command.log”;指定 -a 表示命令的輸出結果會附加在 logfilename 后,如果 logfilename 之前已經存在的話。


清單 2. 記錄 log 的例子代碼

#!/usr/bin/env?python
"""
This?run?a?user?specified?command?and?log?its?result../command.py?[-a]?[-c?command]?{logfilename}logfilename?:?This?is?the?name?of?the?log?file.?Default?is?command.log.
-a?:?Append?to?log?file.?Default?is?to?overwrite?log?file.
-c?:?spawn?command.?Default?is?the?command?'ls?-l'.Example:This?will?execute?the?command?'pwd'?and?append?to?the?log?named?my_session.log:./command.py?-a?-c?'pwd'?my_session.log"""
import?os,?sys,?getopt
import?traceback
import?pexpect#?如果程序中間出錯,打印提示信息后退出
def?exit_with_usage():print?globals()['__doc__']os._exit(1)def?main():#######################################################################?Parse?the?options,?arguments,?get?ready,?etc.######################################################################try:optlist,?args?=?getopt.getopt(sys.argv[1:],?'h?ac:',?['help','h','?'])#?如果指定的參數不是’?-a?’?,?‘?-h?’?,?‘?-c?’?,?‘?-??’?,?‘?--help?’?,#‘?--h?’或’?--??’時,會拋出?exception,#?這里?catch?住,然后打印出?exception?的信息,并輸出?usage?提示信息.except?Exception,?e:print?str(e)exit_with_usage()options?=?dict(optlist)#?最多只能指定一個?logfile,否則出錯.if?len(args)?>?1:exit_with_usage()#?如果指定的是?'-h','--h','-?','--?'?或?'--help',只輸出?usage?提示信息.if?[elem?for?elem?in?options?if?elem?in?['-h','--h','-?','--?','--help']]:print?"Help:"exit_with_usage()#?獲取?logfile?的名字.if?len(args)?==?1:script_filename?=?args[0]else:#?如果用戶沒指定,默認?logfile?的名字是?command.logscript_filename?=?"command.log"#?如果用戶指定了參數?-a,如果之前該?logfile?存在,那么接下來的內容會附加在原先內容之后,#?如果之前沒有該??logfile,新建一個文件,并且接下來將內容寫入到該文件中.if?'-a'?in?options:fout?=?open?(script_filename,?"ab")else:#?如果用戶沒指定參數?-a,默認按照用戶指定?logfile?文件名新建一個文件,然后將接下來將內容寫入到該文件中.fout?=?open?(script_filename,?"wb")#?如果用戶指定了?-c?參數,那么運行用戶指定的命令.if?'-c'?in?options:command?=?options['-c']#?如果用戶沒有指定?-c?參數,那么默認運行命令'ls?–?l'else:command?=?"ls?-l"#?logfile?文件的?titlefout.write?('==========Log?Tile:?IBM?developerWorks?China==========\n')#?為接下來的運行命令生成一個?pexpect?的?spawn?類子程序的對象.p?=?pexpect.spawn(command)#?將之前?open?的?file?對象指定為?spawn?類子程序對象的?log?文件.p.logfile?=?fout#?命令運行完后,expect?EOF?出現,這時會將?spawn?類子程序對象的輸出寫入到?log?文件.p.expect(pexpect.EOF)#open?完文件,使用完畢后,需關閉該文件.fout.close()return?0if?__name__?==?"__main__":try:main()except?SystemExit,?e:raise?eexcept?Exception,?e:print?"ERROR"print?str(e)traceback.print_exc()os._exit(1)

?

注:

  • 運行:./command.py -a -c who cmd.log

運行結束后,cmd.log?的內容為:
IBM?developerWorks?China
Root?	?:0?		?2009-05-12?22:40
Root?	?pts/1?		?2009-05-12?22:40?(:0.0)
Root?	?pts/2?		?2009-07-05?18:55?(9.77.180.94)

?

  • logfile

只能通過 spawn 類的構造函數指定。在 spawn 類的構造函數通過參數指定 logfile 時,表示開啟或關閉 logging 。所有的子程序的 input 和 output 都會被 copy 到指定的 logfile 中。設置 logfile 為 None 表示停止 logging,默認就是停止 logging 。設置 logfile 為 sys.stdout,會將所有東西 echo 到標準輸出。

  • logfile_readlogfile_send:

logfile_read:只用來記錄 python 主程序接收到 child 子程序的輸出,有的時候你不想看到寫給 child 的所有東西,只希望看到 child 發回來的東西。 logfile_send:只用來記錄 python 主程序發送給 child 子程序的輸入 logfile、logfile_read 和 logfile_send 何時被寫入呢? logfile、logfile_read 和 logfile_send 會在每次寫 write 和 send 操作后被 flush 。

    • 調用 send 后,才會往 logfile 和 logfile_send 中寫入,sendline/sendcontrol/sendoff/write/writeline 最終都會調用 send,所以 sendline 后 logfile 中一定有內容了,只要此時 logfile 沒有被 close 。

    • 調用 read_nonblocking 后,才會往 logfile 和 logfile_read 中寫入,expect_loop 會調用 read_nonblocking,而 expect_exact 和 expect_list 都會調用 expect_loop,expect 會調用 expect_list,所以 expect 后 logfile 中一定有內容了,只要此時 logfile 沒有被 close 。

  • 如果調用的函數最終都沒有調用 send 或 read_nonblocking,那么 logfile 雖然被分配指定了一個 file,但其最終結果是:內容為空。見下例:


清單 3. log 內容為空的例子代碼

import?pexpect
p?=?pexpect.spawn(?‘?ls?-l?’?)
fout?=?open?('log.txt',?"w")
p.logfile?=?fout
fout.close()

?

運行該腳本后,你會發現其實 log.txt 是空的,沒有記錄 ls -l 命令的內容,原因是沒有調用 send 或 read_nonblocking,真正的內容沒有被 flush 到 log 中。如果在 fout.close() 之前加上 p.expect(pexpect.EOF),log.txt 才會有 ls -l 命令的內容。

?



回頁首


例 3:ssh 的使用

本例實現了如下功能:ssh 登錄到某個用戶指定的主機上,運行某個用戶指定的命令,并輸出該命令的結果。


清單 4. ssh 的例子代碼

#!/usr/bin/env?python"""
This?runs?a?command?on?a?remote?host?using?SSH.?At?the?prompts?enter?hostname,
user,?password?and?the?command.
"""import?pexpect
import?getpass,?os#user:?ssh?主機的用戶名
#host:ssh?主機的域名
#password:ssh?主機的密碼
#command:即將在遠端?ssh?主機上運行的命令
def?ssh_command?(user,?host,?password,?command):"""This?runs?a?command?on?the?remote?host.?This?could?also?be?done?with?thepxssh?class,?but?this?demonstrates?what?that?class?does?at?a?simpler?level.This?returns?a?pexpect.spawn?object.?This?handles?the?case?when?you?try?toconnect?to?a?new?host?and?ssh?asks?you?if?you?want?to?accept?the?public?keyfingerprint?and?continue?connecting."""ssh_newkey?=?'Are?you?sure?you?want?to?continue?connecting'#?為?ssh?命令生成一個?spawn?類的子程序對象.child?=?pexpect.spawn('ssh?-l?%s?%s?%s'%(user,?host,?command))i?=?child.expect([pexpect.TIMEOUT,?ssh_newkey,?'password:?'])#?如果登錄超時,打印出錯信息,并退出.if?i?==?0:?#?Timeoutprint?'ERROR!'print?'SSH?could?not?login.?Here?is?what?SSH?said:'print?child.before,?child.afterreturn?None#?如果?ssh?沒有?public?key,接受它.if?i?==?1:?#?SSH?does?not?have?the?public?key.?Just?accept?it.child.sendline?('yes')child.expect?('password:?')i?=?child.expect([pexpect.TIMEOUT,?'password:?'])if?i?==?0:?#?Timeoutprint?'ERROR!'print?'SSH?could?not?login.?Here?is?what?SSH?said:'print?child.before,?child.afterreturn?None#?輸入密碼.child.sendline(password)return?childdef?main?():#?獲得用戶指定?ssh?主機域名.host?=?raw_input('Hostname:?')#?獲得用戶指定?ssh?主機用戶名.user?=?raw_input('User:?')#?獲得用戶指定?ssh?主機密碼.password?=?getpass.getpass()#?獲得用戶指定?ssh?主機上即將運行的命令.command?=?raw_input('Enter?the?command:?')child?=?ssh_command?(user,?host,?password,?command)#?匹配?pexpect.EOFchild.expect(pexpect.EOF)#?輸出命令結果.print?child.beforeif?__name__?==?'__main__':try:main()except?Exception,?e:print?str(e)traceback.print_exc()os._exit(1)

?

注:

  • 運行后,輸出結果為:

Hostname:?develperWorks.ibm.com
User:?root
Password:
Enter?the?command:?ls?-ltotal?60
drwxr-xr-x?	?2?root?	?system?	?512?Jun?14?2006??.dt
drwxrwxr-x?	?3?root?	?system?	?512?Sep?23?2008??.java
-rwx------?	?1?root?	?system?	?1855?Jun?14?2006??.kshrc
-rwx------?	?1?root?	?system?	?806?Sep?16?2008??.profile
-rwx------?	?1?root?	?system?	?60?Jun?14?2006??.rhosts
drwx------?	?2?root?	?system?	?512?Jan?18?2007??.ssh
drwxr-x---?	?2?root?	?system?	?512?Apr?15?00:04?223002
-rwxr-xr-x?	?1?root?	?system?	?120?Jan?16?2007??drcron.sh
-rwx------?	?1?root?	?system?	?10419?Jun?14?2006??firewall
drwxr-x---?	?2?root?	?system?	?512?Oct?25?2007??jre
-rw-------?	?1?root?	?system?	?3203?Apr?04?2008??mbox
-rw-r--r--?	?1?root?	?system?	?0?Jun?14?2006??pt1
-rw-r--r--?	?1?root?	?system?	?0?Jun?14?2006??pt2

?

  • 使用了 getpass.getpass() 來獲得用戶輸入的密碼,與 raw_input 不同的是,getpass.getpass() 不會將用戶輸入的密碼字符串 echo 回顯到 stdout 上。(更多 python 相關技術,請參閱參考資料)

?



回頁首


例 4:pxssh 的使用

本例實現了如下功能:使用 pexpect 自帶的 pxssh 模塊實現 ssh 登錄到某個用戶指定的主機上,運行命令’ uptime ’和’ ls -l ’,并輸出該命令的結果。


清單 5. 使用 pxssh 的例子代碼

#!/usr/bin/env?python
import?pxssh
import?getpass
try:#?調用構造函數,創建一個?pxssh?類的對象.s?=?pxssh.pxssh()#?獲得用戶指定?ssh?主機域名.hostname?=?raw_input('hostname:?')#?獲得用戶指定?ssh?主機用戶名.username?=?raw_input('username:?')#?獲得用戶指定?ssh?主機密碼.password?=?getpass.getpass('password:?')#?利用?pxssh?類的?login?方法進行?ssh?登錄,原始?prompt?為'$'?,?'#'或'>'s.login?(hostname,?username,?password,?original_prompt='[$#>]')#?發送命令?'uptime's.sendline?('uptime')#?匹配?prompts.prompt()#?將?prompt?前所有內容打印出,即命令?'uptime'?的執行結果.print?s.before#?發送命令?'?ls?-l?'s.sendline?('ls?-l')#?匹配?prompts.prompt()#?將?prompt?前所有內容打印出,即命令?'?ls?-l?'?的執行結果.print?s.before#?退出?ssh?sessions.logout()
except?pxssh.ExceptionPxssh,?e:print?"pxssh?failed?on?login."print?str(e)

?

  • 運行后,輸出結果為:

hostname:?develperWorks.ibm.com
username:?root
password:
uptime
02:19AM???up?292?days,??12:16,??2?users,??load?average:?0.01,?0.02,?0.01ls?-l
total?60
drwxr-xr-x?	?2?root?	?system?	?512?Jun?14?2006??.dt
drwxrwxr-x?	?3?root?	?system?	?512?Sep?23?2008??.java
-rwx------?	?1?root?	?system?	?1855?Jun?14?2006??.kshrc
-rwx------?	?1?root?	?system?	?806?Sep?16?2008??.profile
-rwx------?	?1?root?	?system?	?60?Jun?14?2006??.rhosts
drwx------?	?2?root?	?system?	?512?Jan?18?2007??.ssh
drwxr-x---?	?2?root?	?system?	?512?Apr?15?00:04?223002
-rwxr-xr-x?	?1?root?	?system?	?120?Jan?16?2007??drcron.sh
-rwx------?	?1?root?	?system?	?10419?Jun?14?2006??firewall
drwxr-x---?	?2?root?	?system?	?512?Oct?25?2007??jre
-rw-------?	?1?root?	?system?	?3203?Apr?04?2008??mbox
-rw-r--r--?	?1?root?	?system?	?0?Jun?14?2006??pt1
-rw-r--r--?	?1?root?	?system?	?0?Jun?14?2006??pt2

?

  • pxssh 是 pexpect 中 spawn 類的子類,增加了 login, logout 和 prompt 幾個方法,使用其可以輕松實現 ssh 連接,而不用自己調用相對復雜的 pexpect 的方法來實現。 pxssh 做了很多 tricky 的東西來處理 ssh login 過程中所可能遇到的各種情況。比如:如果這個 session 是第一次 login,pxssh 會自動接受遠程整數 remote certificate ;如果你已經設置了公鑰認證,pxssh 將不會再等待 password 的提示符。(更多 ssh 相關知識,請參閱參考資料) pxssh 使用 shell 的提示符來同步遠程主機的輸出,為了使程序更加穩定,pxssh 還可以設置 prompt 為更加唯一的字符串,而不僅僅是“ $ ”和“ # ”。

  • login 方法

	login?(self,server,username,password='',terminal_type='ansi',?iginal_prompt=r"[#$]",login_timeout=10,port=None,auto_prompt_reset=True):

?

使用原始 original_prompt 來找到 login 后的提示符(這里默認 original_prompt 是“$”或“#”,但是有時候可能也是別的 prompt,這時就需要在 login 時手動指定這個特殊的 prompt,見上例,有可能是“ > ”),如果找到了,立馬使用更容易匹配的字符串來重置該原始提示符(這是由 pxssh 自己自動做的,通過命令 "PS1='[PEXPECT]\$ '" 重置原始提示符,然后每次 expect 匹配 \[PEXPECT\][\$\#])。原始提示符是很容易被混淆和胡弄的,為了阻止錯誤匹配,最好根據特定的系統,指定更加精確的原始提示符,例如 "Message Of The Day" 。有些情況是不允許重置原始提示符的,這時就要設置 auto_prompt_reset 為 False 。而且此時需要手動設置 PROMPT 域為某個正則表達式來 match 接下來要出現的新提示符,因為 prompt() 函數默認是 expect 被重置過的 PROMPT 的。

  • prompt方法

prompt?(self,?timeout=20):

?

匹配新提示符(不是 original_prompt)。注:這只是匹配提示符,不能匹配別的 string,如果要匹配特殊 string,需直接使用父類 spawn 的 expect 方法。 prompt 方法相當于是 expect 方法的一個快捷方法。如果auto_prompt_reset 為 False,這時需要手動設置 PROMPT 域為某個正則表達式來 match 接下來要出現的 prompt,因為 prompt() 函數默認是 expect 被重置過的 PROMPT 的。

  • logout方法

logout?(self):


發送'exit'給遠程 ssh 主機,如果有 stopped jobs,會發送'exit'兩次。




回頁首



例 5:telnet 的使用

本例實現了如下功能:telnet 登錄到某遠程主機上,輸入命令“ls -l”后,將子程序的執行權交還給用戶,用戶可以與生成的 telnet 子程序進行交互。


清單 6. telnet 的例子代碼

#!/usr/bin/env?python
import?pexpect#?即將?telnet?所要登錄的遠程主機的域名
ipAddress?=?'develperWorks.ibm.com'
#?登錄用戶名
loginName?=?'root'
#?用戶名密碼
loginPassword?=?'passw0rd'
#?提示符,可能是’?$?’?,?‘?#?’或’?>?’
loginprompt?=?'[$#>]'#?拼湊?telnet?命令
cmd?=?'telnet?'?+?ipAddress
#?為?telnet?生成?spawn?類子程序
child?=?pexpect.spawn(cmd)
#?期待'login'字符串出現,從而接下來可以輸入用戶名
index?=?child.expect(["login",?"(?i)Unknown?host",?pexpect.EOF,?pexpect.TIMEOUT])
if?(?index?==?0?):#?匹配'login'字符串成功,輸入用戶名.child.sendline(loginName)#?期待?"[pP]assword"?出現.index?=?child.expect(["[pP]assword",?pexpect.EOF,?pexpect.TIMEOUT])#?匹配?"[pP]assword"?字符串成功,輸入密碼.child.sendline(loginPassword)#?期待提示符出現.child.expect(loginprompt)if?(index?==?0):#?匹配提示符成功,輸入執行命令?'ls?-l'child.sendline('ls?-l')#?立馬匹配?'ls?-l',目的是為了清除剛剛被?echo?回顯的命令.child.expect('ls?-l')#?期待提示符出現.child.expect(loginprompt)#?將?'ls?-l'?的命令結果輸出.print?child.beforeprint?"Script?recording?started.?Type?^]?(ASCII?29)?to?escape?from?the?script?shell."#?將?telnet?子程序的執行權交給用戶.child.interact()print?'Left?interactve?mode.'else:#?匹配到了?pexpect.EOF?或?pexpect.TIMEOUT,表示超時或者?EOF,程序打印提示信息并退出.print?"telnet?login?failed,?due?to?TIMEOUT?or?EOF"child.close(force=True)
else:#?匹配到了?pexpect.EOF?或?pexpect.TIMEOUT,表示超時或者?EOF,程序打印提示信息并退出.print?"telnet?login?failed,?due?to?TIMEOUT?or?EOF"child.close(force=True)

?

  • 運行后,輸出結果為:

total?60
drwxr-xr-x???2?root?????system??????????512?Jun?14?2006??.dt
drwxrwxr-x???3?root?????system??????????512?Sep?23?2008??.java
-rwx------???1?root?????system?????????1855?Jun?14?2006??.kshrc
-rwx------???1?root?????system??????????806?Sep?16?2008??.profile
-rwx------???1?root?????system???????????60?Jun?14?2006??.rhosts
drwx------???2?root?????system??????????512?Jan?18?2007??.ssh
drwxr-x---???2?root?????system??????????512?Apr?15?00:04?223002
-rwxr-xr-x???1?root?????system??????????120?Jan?16?2007??drcron.sh
-rwx------???1?root?????system????????10419?Jun?14?2006??firewall
drwxr-x---???2?root?????system??????????512?Oct?25?2007??jre
-rw-------???1?root?????system?????????3203?Apr?04?2008??mbox
-rw-r--r--???1?root?????system????????????0?Jun?14?2006??pt1
-rw-r--r--???1?root?????system????????????0?Jun?14?2006??pt2
essni2
Script?recording?started.?Type?^]?(ASCII?29)?to?escape?from?the?script?shell.
此時程序會?block?住,等待用戶的輸入,比如用戶輸入’?pwd?’,輸出/home/root
接下來用戶敲入?ctrl+]?結束子程序

?

  • interact方法

interact(self,?escape_character?=?chr(29),?input_filter?=?None,?output_filter?=?None)

?

通常一個 python 主程序通過 pexpect.spawn 啟動一個子程序,一旦該子程序啟動后,python 主程序就可以通過 child.expect 和 child.send/child.sendline 來和子程序通話,python 主程序運行結束后,子程序也就死了。比如 python 主程序通過 pexpect.spawn 啟動了一個 telnet 子程序,在進行完一系列的 telnet 上的命令操作后,python 主程序運行結束了,那么該 telnet session(telnet 子程序)也會自動退出。但是如果調用 child.interact,那么該子程序(python 主程序通過 pexpect.spawn 衍生成的)就可以在運行到 child.interact 時,將子程序的控制權交給了終端用戶(the human at the keyboard),用戶可以通過鍵盤的輸入來和子程序進行命令交互,管理子程序的生殺大權,用戶的鍵盤輸入 stdin 會被傳給子程序,而且子程序的 stdout 和 stderr 輸出也會被打印出來到終端。默認 ctrl + ] 退出 interact() 模式,把子程序的執行權重新交給 python 主程序。參數 escape_character 指定了交互模式的退出字符,例如 child.interact(chr(26)) 接下來就會變成 ctrl + z 退出 interact() 模式。

?



回頁首


pexpect 使用 tips

調試 pexpect 程序的 tips

  • 獲得 pexpect.spawn 對象的字符串 value值,將會給 debug 提供很多有用信息。


清單 7. 打印 pexpect.spawn 對象的字符串 value 值的例子代碼

try:i?=?child.expect?([pattern1,?pattern2,?pattern3,?etc])
except:print?"Exception?was?thrown"print?"debug?information:"print?str(child)

?

  • 將子程序的 input 和 output 打 log 到文件中或直接打 log 到屏幕上也非常有用


清單 8. 記錄 log 的例子代碼

#?打開?loggging?功能并將結果輸出到屏幕上
child?=?pexpect.spawn?(foo)
child.logfile?=?sys.stdout

?

pexpect 不會解釋 shell 中的元字符

  • pexpect 不會解釋 shell 的元字符,如重定向 redirect,管道 pipe,和通配符 wildcards( “ > ” , “ | ”和“ * ”等 ) 如果想用的話,必須得重新啟動一個新 shell(在 spawn 的參數 command 中是不會解釋他們的,視其為 command string 的一個普通字符)


清單 9. 重新啟動一個 shell 來規避 pexpect 對元字符的不解釋

child?=?pexpect.spawn('/bin/bash?-c?"ls?-l?|?grep?LOG?>?log_list.txt"')
child.expect(pexpect.EOF)

?

如果想在 spawn 出來的新子程序中使用重定向 redirect,管道 pipe,和通配符 wildcards( “ > ” , “ | ”和“ * ”等 ),好像沒有好的方法,只能不使用這些字符,先利用 expect 匹配命令提示符,從而在 before 中可以拿到之前命令的結果,然后在分析 before 的內容達到使用重定向 redirect, 管道 pipe, 和通配符 wildcards 的目的。

EOF 異常和 TIMEOUT 異常

  • TIMEOUT 異常

如果子程序沒有在指定的時間內生成任何 output,那么 expect() 和 read() 都會產生 TIMEOUT 異常。超時默認是 30s,可以在 expect() 和 spawn 構造函數初始化時指定為其它時間,如:

child.expect('password:',?timeout=120)?#?等待?120s

?

如果你想讓 expect() 和 read() 忽略超時限制,即無限期阻塞住直到有 output 產生,設置 timeout 參數為 None。


清單 10. 忽略 timeout 超時限制的例子代碼

child?=?pexpect.spawn(?"telnet?develperWorks.ibm.com"?)
child.expect(?"login",?timeout=None?)

?

  • EOF 異常

可能會有兩種 EOF 異常被拋出,但是他們除了顯示的信息不同,其實本質上是相同的。為了實用的目的,不需要區分它們,他們只是給了些關于你的 python 程序到底運行在哪個平臺上的額外信息,這兩個顯示信息是:

End?Of?File?(EOF)?in?read().?Exception?style?platform.
End?Of?File?(EOF)?in?read().?Empty?string?style?platform.

?

有些 UNIX 平臺,當你讀取一個處于 EOF 狀態的文件描述符時,會拋出異常,其他 UNIX 平臺,卻只會靜靜地返回一個空字符串來表明該文件已經達到了狀態。

使用 run() 來替代某些的 spawn 的使用

pexpect 模塊除了提供 spawn 類以外,還提供了 run() 函數,使用其可以取代一些 spawn 的使用,而且更加簡單明了。


清單 11. 使用 run() 來替代 spawn 的使用的例子代碼

#?使用?spawn?的例子
from?pexpect?import?*
child?=?spawn('scp?foo?myname@host.example.com:.')
child.expect?('(?i)password')
child.sendline?(mypassword)
#?以上功能,相當于以下?run?函數:
from?pexpect?import?*
run?('scp?foo?myname@host.example.com:.',?events={'(?i)password':?mypassword})

?

  • run (command, timeout=-1, withexitstatus=False, events=None, extra_args=None, logfile=None, cwd=None, env=None):

    • 發送相應的 response String 。如果需要回車符“ Enter ”的話,“ \\n ”也必須得出現在 response 字符串中。

    • response 同樣也可以是個回調函數,不過該回調函數有特殊要求,即它的參數必須是個 dictionary,該 dictionary 的內容是:包含所有在 run() 中定義的局部變量,從而提供了方法可以訪問 run() 函數中 spawn 生成的子程序和 run() 中定義的其他局部變量,其中 event_count, child, 和 extra_args 最有用。回調函數可能返回 True,從而阻止當前 run() 繼續執行,否則 run() 會繼續執行直到下一個 event 。回調函數也可能返回一個字符串,然后被發送給子程序。 'extra_args' 不是直接被 run() 使用,它只是提供了一個方法可以通過 run() 來將數據傳入到回調函數中(其實是通過 run() 定義的局部變量 dictionary 來傳)

    • command:執行一個命令,然后返回結果,run() 可以替換 os.system()(更多 os.system() 知識,請參閱參考資料),因為 os.system() 得不到命令輸出的結果

    • 返回的 output 是個字符串,STDERR 也會包括在 output 中,如果全路徑沒有被指定,那么 path 會被 search

    • timeout:單位 s 秒,每隔 timeout 生成一個 pexpect.TIMEOUT 異常

    • 每行之間被 CR/LF (\\r\\n) 相隔,即使在 Unix 平臺上也是 CR/LF,因為 Pexpect 子程序是偽 tty 設備

    • withexitstatus:設置為 True,則返回一個 tuple,里面包括 (command_output, exitstatus),如果其為 False,那么只是僅僅返回 command_output

    • events:是個 dictionary,里面存放 {pattern:response} 。無論什么時候 pattern 在命令的結果中出現了,會出現以下動作:


清單 12. 其它一些使用 run() 的例子代碼

#?在?local?機器上啟動?apache?的?daemon
from?pexpect?import?*
run?("/usr/local/apache/bin/apachectl?start")
#?使用?SVN?check?in?文件
from?pexpect?import?*
run?("svn?ci?-m?'automatic?commit'?my_file.py")
#?運行一個命令并捕獲?exit?status
from?pexpect?import?*
command_output,?exitstatus)?=?run?('ls?-l?/bin',?withexitstatus=1)
#?運行?SSH,并在遠程機器上執行’?ls?-l?’,如果?pattern?'(?i)password'?被匹配住,密碼?'secret'?
#?將會被發送出去
run?("ssh?username@machine.example.com?'ls?-l'",?events={'(?i)password':'secret\\n'})
#?啟動?mencoder?來?rip?一個?video,同樣每?5s?鐘顯示進度記號
from?pexpect?import?*
def?print_ticks(d):print?d['event_count']
run?("mencoder?dvd://1?-o?video.avi?-oac?copy?-ovc?copy",?events={TIMEOUT:print_ticks})

?

expect_exact() 的使用

expect_exact(self, pattern_list, timeout = -1, searchwindowsize = -1); expect_exact() 與 expect() 類似,但是 pattern_list 只能是字符串或者是一個字符串的 list,不能是正則表達式,其匹配速度會快于 expect(),原因有兩個:一是字符串的 search 比正則表達式的匹配要快,另一個則是可以限制只從輸入緩沖的結尾來尋找匹配的字符串。還有當你覺得每次要 escape 正則表達式中的特殊字符為普通字符時很煩,那么你也可以使用 expect_exact() 來取代 expect()。


清單 13. expect_exact() 的例子代碼

import?pexpect
child?=?pexpect.spawn('ls?-l')
child.expect_exact('pexpect.txt')
print?child.after

?

expect() 中正則表達式的使用 tips

expect() 中的正則表達式不是貪婪匹配 greedy match,而是最小匹配,即只匹配緩沖區中最早出現的第一個字符串。因為是依次讀取一個字符的 stream 流來判斷是否和正則表達式所表達的模式匹配,所以如果參數 pattern 是個 list,而且不止一次匹配,那么緩沖區中最早出現的第一個匹配的字符串才算數。


清單 14. expect() 的最小匹配例子代碼

#?如果輸入是?'foobar'
index?=?p.expect?(['bar',?'foo',?'foobar'])
#index?返回是?1?('foo')?而不是?2?('foobar'),即使?'foobar'?是個更好的匹配。原因是輸入是個流?stream,
#?當收到?foo?時,第?1?個?pattern?('foo')?就被匹配了,不會等到’?bar?’的出現了,所以返回?1

?

  • “$”不起任何作用,匹配一行的結束 (end of line),必須得匹配 CR/LF

正則表達式中,'$'可以匹配一行的結束(具體'$'正則表達式的使用,請參閱參考資料),但是 pexpect 從子程序中一次只讀取一個字符,而且每個字符都好像是一行的結束一樣,pexpect 不能在子程序的輸出流去預測。匹配一行結束的方法必須是匹配 "\r\n" (CR/LF) 。即使是 Unix 系統,也是匹配 "\r\n" (CR/LF),因為 pexpect 使用一個 Pseudo-TTY 設備與子程序通話,所以當子程序輸出 "\n" 你仍然會在 python 主程序中看到 "\r\n" 。原因是 TTY 設備更像 windows 操作系統,每一行結束都有個 "\r\n" (CR/LF) 的組合,當你從 TTY 設備去解釋一個 Unix 的命令時,你會發現真正的輸出是 "\r\n" (CR/LF),一個 Unix 命令只會寫入一個 linefeed (\n),但是 TTY 設備驅動會將其轉換成 "\r\n" (CR/LF) 。


清單 15. 匹配一行結束 1

child.expect?('\r\n')

?

如果你只是想跳過一個新行,直接 expect('\n') 就可以了,但是如果你想在一行的結束匹配一個具體的 pattern 時,就必須精確的尋找 (\r),見下例:


清單 16. 匹配一行結束 2

#?成功在一行結束前匹配一個單詞
child.expect?('\w+\r\n')
#?以下兩種情況都會失敗
child.expect?('\w+\n')
child.expect?('\w+$')

?

這個問題其實不只是 pexpect 會有,如果你在一個 stream 流上實施正則表達式匹配時,都會遇到此問題。正則表達式需要預測,stream 流中很難預測,因為生成這個流的進程可能還沒有結束,所以你很難知道是否該進程是暫時性的暫停還是已經徹底結束。

  • 當 '.' 和 '*' 出現在最后時

child.expect ('.+'); 因為是最小匹配,所以只會返回一個字符,而不是一個整個一行(雖然 pexpect 設置了 re.DOTALL,會匹配一個新行。 child.expect ('.*'); 每次匹配都會成功,但是總是沒有字符返回,因為 '*' 表明前面的字符可以出現 0 次 , 在 pexpect 中,一般來說,任何 '*' 都會盡量少的匹配。

isalive() 的使用 tips

  • isalive(self)

測試子程序是否還在運行。這個方法是非阻塞的,如果子程序被終止了,那么該方法會去讀取子程序的 exitstatus 或 signalstatus 這兩個域。返回 True 表明子程序好像是在運行,返回 False 表示不再運行。當平臺是 Solaris 時,可能需要幾秒鐘才能得到正確的狀態。當子程序退出后立馬執行 isalive() 有時可能會返回 1 (True),這是一個 race condition,原因是子程序已經關閉了其文件描述符,但是在 isalive() 執行前還沒有完全的退出。增加一個小小的延時會對 isalive() 的結果有效性有幫助。


清單 17. isalive() 的例子代碼

#?以下程序有時會返回?1?(True)
child?=?pexpect.spawn('ls')
child.expect(pexpect.EOF)
print?child.isalive()
#?但是如果在?isalive()?之前加個小延時,就會一直返回?0?(False)
child?=?pexpect.spawn('ls')
child.expect(pexpect.EOF)
time.sleep(0.1)????#?之前要?import?time,單位是秒?s
print?child.isalive()

?

delaybeforesend 的使用 tips

spawn 類的域 delaybeforesend 可以幫助克服一些古怪的行為。比如,經典的是,當一個用戶使用 expect() 期待 "Password:" 提示符時,如果匹配,立馬 sendline() 發送密碼給子程序,但是這個用戶會看到他們的密碼被 echo back 回顯回來了。這是因為,通常許多應用程序都會在打印出 "Password:" 提示符后,立馬關掉 stdin 的 echo,但是如果你發送密碼過快,在程序關掉 stdin 的 echo 之前就發送密碼出去了,那么該密碼就會被 echo 出來。


清單 18. delaybeforesend 的例子代碼

child.expect?('[pP]assword:')
child.sendline?(my_password)
#?在?expect?之后,某些應用程序,如?SSH,會做如下動作:
#1.?SSH?打印?"password:"?提示符給用戶
#2.?SSH?關閉?echo.
#3.?SSH?等待用戶輸入密碼
#?但是現在第二條語句?sendline?可能會發生在?1?和?2?之間,即在?SSH?關掉?echo?之前輸入了?password?給子程序?,?從?
#?而在?stdout,該?password?被?echo?回顯出來,出現了?security?的問題
#?所以此時可以通過設置?delaybeforesend?來在將數據寫(發送)給子程序之前增加一點點的小延時,因為該問題經?
#?常出現,所以默認就?sleep?50ms.?許多?linux?機器必須需要?0.03s?以上的?delay
self.delaybeforesend?=?0.05?#?單位秒



參考資料

  • 訪問 《探索 Pexpect,第 1 部分:剖析 Pexpect 》,了解 pexpect 基礎知識。

  • 訪問 Expect 的主頁,了解更多 Expect 的內容。

  • 訪問 Python 官方網頁,了解更多 Python 相關知識。

  • 有關正則表達式方面相關知識,請訪問正則表達式。

  • 更多 SSH 相關知識,請訪問 SSH。

  • 更多 os.system() 的介紹,請訪問 os.system()。

  • 在 developerWorks Linux 專區 尋找為 Linux 開發人員(包括Linux 新手入門)準備的更多參考資料,查閱我們最受歡迎的文章和教程。

  • 在 developerWorks 上查閱所有 Linux 技巧 和 Linux 教程。