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


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

1. 基本用法

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

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

為自己的Cmd類添加名為“do_xxx()”的方法,則運行時,在提示符下可以接受xxx指令。但對應的參數解析貌似是要自己搞定的,否則在指令名之后輸入的所有東西,只要不回車,它都是一個大參數;

有指令的實現方法,也有對應幫助信息的實現方法。只要添加“help_xxx()”方法,就可以為xxx方法在運行時提供一個幫助信息了。當然你也可以實現一個“do_help()”來打印幫助匯總什么的;

2. 簡單實例

閑話少說,先上代碼

 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來構造界面。

那么起碼來說,工具要有個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模塊去搞,但這次的任務只是個即時小工具而已……


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM