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 的使用
本例實現瞭如下功能:ftp 登入到 develperWorks.ibm.com 主機上,並用二進位制傳輸模式下載一個名叫 rmall
的檔案。
清單 1. ftp 的例子程式碼
#!/usr/bin/env python
import 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 = " index
child.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.log
script_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 檔案的 title
fout.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 0
if __name__ == "__main__":
try:
main()
except SystemExit, e:
raise e
except 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) |
只能通過 spawn 類的建構函式指定。在 spawn 類的建構函式通過引數指定 logfile 時,表示開啟或關閉 logging 。所有的子程式的 input 和 output 都會被 copy 到指定的 logfile 中。設定 logfile 為 None 表示停止 logging,預設就是停止 logging 。設定 logfile 為 sys.stdout,會將所有東西 echo 到標準輸出。
logfile_read
和
logfile_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 the
pxssh 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 to
connect to a new host and ssh asks you if you want to accept the public key
fingerprint 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: # Timeout
print 'ERROR!'
print 'SSH could not login. Here is what SSH said:'
print child.before, child.after
return 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: # Timeout
print 'ERROR!'
print 'SSH could not login. Here is what SSH said:'
print child.before, child.after
return None
# 輸入密碼.
child.sendline(password)
return child
def 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.EOF
child.expect(pexpect.EOF)
# 輸出命令結果.
print child.before
if __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 -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 |
- 使用了 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')
# 匹配 prompt
s.prompt()
# 將 prompt 前所有內容列印出,即命令 'uptime' 的執行結果.
print s.before
# 傳送命令 ' ls -l '
s.sendline ('ls -l')
# 匹配 prompt
s.prompt()
# 將 prompt 前所有內容列印出,即命令 ' ls -l ' 的執行結果.
print s.before
# 退出 ssh session
s.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.01
ls -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 (self, timeout=20): |
匹配新提示符(不是 original_prompt)。注:這只是匹配提示符,不能匹配別的 string,如果要匹配特殊 string,需直接使用父類 spawn 的 expect 方法。 prompt 方法相當於是 expect 方法的一個快捷方法。如果auto_prompt_reset 為 False,這時需要手動設定 PROMPT 域為某個正規表示式來 match 接下來要出現的 prompt,因為 prompt() 函式預設是 expect 被重置過的 PROMPT
的。
logout
方法
傳送’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.before
print "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(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 異常
如果子程式沒有在指定的時間內生成任何 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 異常被丟擲,但是他們除了顯示的資訊不同,其實本質上是相同的。為了實用的目的,不需要區分它們,他們只是給了些關於你的 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 [email protected]:.')
child.expect ('(?i)password')
child.sendline (mypassword)
# 以上功能,相當於以下 run 函式:
from pexpect import *
run ('scp foo [email protected]:.', events={'(?i)password': mypassword}) |
- run (command, timeout=-1, withexitstatus=False, events=None, extra_args=None, logfile=None, cwd=None, env=None):
- 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 在命令的結果中出現了,會出現以下動作:
- 傳送相應的 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 來傳)
清單 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 [email protected] '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
如果你只是想跳過一個新行,直接 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
測試子程式是否還在執行。這個方法是非阻塞的,如果子程式被終止了,那麼該方法會去讀取子程式的 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 # 單位秒 |
參考資料
原文連結:
http://www.ibm.com/developerwork…
写评论
很抱歉,必須登入網站才能發佈留言。