【Python】使用cmd模塊構造一個帶有後臺線程的交互命令行界面

最近寫一些測試工具,實在懶得搞GUI,而後意識到python有一個自帶模塊叫cmd,用了用發現簡直是救星。python

1. 基本用法windows

cmd模塊很容易學到,基本的用法比較簡單,繼承模塊下的Cmd類,添加須要的功能入口就行了。異步

Cmd類有個prompt屬性,修改它能夠把默認提示符((cmd))替換成自定義的;socket

爲本身的Cmd類添加名爲「do_xxx()」的方法,則運行時,在提示符下能夠接受xxx指令。但對應的參數解析貌似是要本身搞定的,不然在指令名以後輸入的全部東西,只要不回車,它都是一個大參數;async

有指令的實現方法,也有對應幫助信息的實現方法。只要添加「help_xxx()」方法,就能夠爲xxx方法在運行時提供一個幫助信息了。固然你也能夠實現一個「do_help()」來打印幫助彙總什麼的;tornado

2. 簡單實例工具

閒話少說,先上代碼oop

 1 from cmd import Cmd
 2 from sys import exit
 3 
 4 
 5 class MyCmd(Cmd):
 6     def __init__(self):
 7         super(MyCmd, self).__init__()
 8         self.prompt = "->"
 9 
10     def do_hello(self, args):
11         print("Hello.")
12 
13     def help_hello(self, args):
14         print("hello - print hello and do nothing more.")
15 
16     def do_exit(self, args):
17         exit(0)
18 
19 
20 def main():
21     mycmd = MyCmd()
22     mycmd.cmdloop(intro="My Cmd Demo.")
23 
24 
25 if __name__ == "__main__":
26   main()

 

3. 問題來了測試

最初的想法其實是要用cmd構造一個工具,鏈接到某個服務端,沒動做的時候就接收和顯示那邊發來的數據,某些時候還要按照交互輸入的指令,往服務端那邊發送一些數據。爲了交互方便,才選擇cmd來構造界面。spa

那麼起碼來講,工具要有個do_exit()方法用來退出,還要有個do_send()方法用來發數據給服務端。

只有這麼簡單就行了,實際須要考慮等着收數據的同時還要等着操做者從界面輸入指令,指令輸入完畢回車之後,就要把數據發出去。顯然這是個異步場景。說到異步,想到了tornado和twisted,但試了試twisted發現用來搞這種客戶端並不怎麼方便;後來想起python自帶的asyncore,看了下文檔發現很簡單就能夠作到既一直接收又能隨時發送,就改用asyncore了。

但asyncore的dispatcher要跑起來的話,得運行asyncore.loop(),但這個若是在主線程裏運行起來,後面就沒有Cmd.cmdloop()什麼事了。

對此,我想到的是,把asyncore.loop()放到一個後臺線程裏去,而把cmdloop()放在主線程裏。而要讓cmd界面可以經過dispatcher發送數據,仍是得先讓它知道dispatcher實例的存在。

好在python自帶了很多各類電池,調用Threading提供的功能,沒費多少功夫就搞好了。

大概是這樣的:

from cmd import Cmd
from sys import exit, argv
import threading
import socket
import asyncore



class BackgroundRunner(asyncore.dispatcher, object):
    def __init__(self, host, port):
        super(BackgroundRunner, self).__init__()
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect((host, port))
        self.buffer = b''

    def handle_connect(self):
        pass

    def handle_close(self):
        self.close()

    def handle_read(self):
        recvdata = self.recv(4096)
        # TO-DO with the data received

    def writable(self):
        return len(self.buffer) > 0

    def handle_write(self):
        sent = self.send(self.buffer)
        self.buffer = self.buffer[sent:]


class MyCmd(Cmd, object)
    def __init__(self):
        super(MyCmd, self).__init__()
        self.prompt = "->"
        self.bgrunner = BackgroundRunner(host, port)

        # 調用setDaemon()將線程轉到後臺,不然它執行ssyncore.loop()的時候會佔住你的標準輸入和輸出直到強行退出
        nthd = threading.Thread(target=asyncore.loop)
        nthd.setDaemon(True)
        nthd.start()

  # Cmd.emptyline()方法應該視須要進行重載,不然回車輸入空指令的默認處理會是執行上一條指令
def emptyline(self): pass def do_send(self, args): # bla bla bla, 對參數args(其實就是個字符串)作些事,獲得你要發送的數據data sendresult = self.bgrunner.send(data) # bla bla bla def do_exit(self, args): self.bgrunner.close() exit(0) def main(): # 其實你徹底能夠用argparse什麼的來處理命令行參數,我這只是給本身額寫個示例才直接搞argv的,別太認真 host, port = argv[1:3] c = MyCmd(host, int(port)) c.cmdloop() if __name__ == "__main__": main()

 

4. 各類小麻煩

默認cmd模塊中的Cmd類會使用rawinput來處理提示符顯示和輸入信息獲取的工做,可是特定狀況下會有個問題:

當交互線程等待用戶輸入指令的時候,若是但願後臺線程能夠打印信息到前臺顯示的話……

打印隨便用print或者sys.stdout.write什麼的,固然是打印出來了,但只要開始輸入新的指令,這些打印信息就都被清除掉了,只剩下提示符和新的輸入。若是想實時看什麼東西的話……

反覆嘗試和閱讀cmd模塊源碼之後發現這麼一件事:

Cmd類在實例化的時候,默認會有個use_rawinput屬性是爲1的,若是重載__init__()的時候把它設置爲0,那麼會改成經過readline來處理提示符和輸入(固然你若是在windows上玩這一手的話,最好先把pyreadline裝上,windows上我沒弄過gnu readline,不知道有沒搞成了的),而後打印信息被擦除的問題就得以解決了。

其實記錄信息徹底可讓logging模塊去搞,但此次的任務只是個即時小工具而已……

相關文章
相關標籤/搜索