最近寫一些測試工具,實在懶得搞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模塊去搞,但這次的任務只是個即時小工具而已……