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