NO IMAGE

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)

  • 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 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):
    • 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

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 # 單位秒

參考資料

原文連結:
http://www.ibm.com/developerwork…