參考:http://www.ibm.com/developerworks/cn/linux/l-cn-pexpect2/
概述
通過本系列第一部分《探索 Pexpect,第 1 部分:剖析 Pexpect 》(請參閱參考資料)的介紹,相信大家已經對 Pexpect 的用法已經有了比較全面的了解,知道 Pexpect 是個純 Python 語言實作的子產品,使用其可以輕松友善的實作與 ssh、ftp、passwd 和 telnet 等程式的自動互動,但是讀者的了解還可能隻是停留在理論基礎上,本文将從實際例子入手具體介紹 Pexpect 的使用場景和使用心得體驗,執行個體中的代碼讀者都可以直接拿來使用,相信會對大家産生比較大的幫助。 以下是本文所要介紹的所有 Pexpect 例子标題:
-
例 1:ftp 的使用(注:spawn、expect 和 sendline 的使用)
-
例 2:記錄 log(注:logfile、
logfile_send
和
logfile_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 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)
-
logfile
:
隻能通過 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 或 read_nonblocking,那麼 logfile 雖然被配置設定指定了一個 file,但其最終結果是:内容為空。見下例:
-
調用 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 。
-
清單 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
方法
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.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
方法
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 [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):
- 發送相應的 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 [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
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)
清單 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
清單 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 # 機關秒