python Pexpect

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 的使用

本例實現瞭如下功能: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 (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)

測試子程序是否還在運行。這個方法是非阻塞的,如果子程序被終止了,那麼該方法會去讀取子程序的 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 # 單位秒



參考資料


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章