python控制windows命令列程式

顺其自然,道法自然發表於2024-03-06

有一些現成的庫, 比如WExpect, 是開源的, 在github上可以搜尋到. 但是, 不知道為什麼, 在我自己的筆記本上不能正常工作. 而其原始碼也比較多, 懶得定位了.
於是自己實現了一個, 用法如下.

  • 啟動和停止命令列
import my_cmd as cmd
cmd.start()
cmd.stop()
  • prompt命令列提示符匹配字串
import my_cmd as cmd
print(cmd.get_prompt())  # 輸出: `[A-Za-z]:\\.*>$`
# cmd.set_prompt('>')  # 設定提示符匹配字串, 需要是正規表示式
  • 輪詢等待expect預期輸出
    功能: 輪詢等待expect預期輸出, 如果沒有預期輸出, 則一直等待下去.
    原型: expect(*regex_infos:str)->int:
    expect(): 如果不用引數, 則預設等待prompt提示符出現.
    如果引數有一個或者多個正規表示式, 則返回第一個匹配的正規表示式在列表中的索引. 如果匹配到prompt, 則返回0.
import my_cmd as cmd
cmd.start()
cmd.expect()
cmd.stop()

輸出:

Microsoft Windows [版本 10.0.22621.3155]
(c) Microsoft Corporation。保留所有權利。

d:\MyCode\Libs\Python\Test\my_cmd>
cmd.exe process exited.
  • 檢視和清除輸出
import my_cmd as cmd
cmd.start()
cmd.expect()
print(cmd.get_output())  # 獲取所有的輸出
print(cmd.get_last_line())  # 獲取最後一行的輸出
print(cmd.clear_output())  # 清除輸出資訊
cmd.stop()
  • 輸入命令
    write_line(cmd:str): 輸入命令, 自動新增換行符. 為防止輸出資訊干擾, 會先清除之前的輸出資訊.
import my_cmd as cmd
cmd.start()
cmd.expect()
cmd.write_line('pwd')
cmd.expect()
cmd.stop()

輸出:

Microsoft Windows [版本 10.0.22621.3155]
(c) Microsoft Corporation。保留所有權利。

d:\MyCode\Libs\Python\Test\my_cmd>pwd
/d/MyCode/Libs/Python/Test/my_cmd

d:\MyCode\Libs\Python\Test\my_cmd>
cmd.exe process exited.

原始碼

import Common
import io
import re
import time
import subprocess

# region 全域性變數
_buffer = io.StringIO()
'''存放輸出字串的緩衝區'''
_last_line_buffer = io.StringIO()
'''最後一行的輸出緩衝區. 主要是為了提高expect函式的匹配效率.'''
_proc:subprocess.Popen = None
'''cmd.exe程序物件'''
_prompt:str = r'[A-Za-z]:\\.*>$'  # 有時候磁碟機代號也可能是小寫
r'''預設提示資訊. 形如: C:\Users\huzho>'''
# endregion

def clear_output() -> None:
    '''清除輸出'''
    global _buffer,_last_line_buffer
    _buffer.close()
    _buffer = io.StringIO()
    _last_line_buffer.close()
    _last_line_buffer = io.StringIO()

def expect(*regex_infos:str)->int:
    '''
    等待預期的字串出現. 為提高效率, 預設只匹配輸出的最後一行.
    返回值: 匹配到的預期字串的索引, 如果為0, 表示prompt, 其餘的按順序為1,2,3...
    '''
    regex_infos = list(regex_infos)
    regex_infos.insert(0, _prompt)  # 增加上提示符
    while True:  # 迴圈匹配, 直到匹配成功為止
        for i,reg_info in enumerate(regex_infos):
            if re.search(reg_info, get_last_line()):
                return i
        time.sleep(0.1)  # 防止死迴圈

def get_last_line() -> str:
    '''獲取輸出的最後一行字串'''
    return _last_line_buffer.getvalue()

def get_output() -> str:
    '''獲取當前的輸出'''
    return _buffer.getvalue()

def get_prompt() -> str:
    '''獲取當前的提示符'''
    return _prompt

@Common.run_in_thread
def _output():
    '''輪詢輸出'''
    global _last_line_buffer
    while True:
        output = _proc.stdout.read(1)  # 讀取一個字元, 保證了只要有輸出, 就會及時響應
        if output == '' and _proc.poll() is not None:   # poll函式有值, 表示程序已退出
            _buffer.close()  # 清空緩衝區
            _last_line_buffer.close()
            print('cmd.exe process exited.')
            break
        print(output,end='')
        _buffer.write(output)  # 寫入緩衝區中
        # 處理最後一行的緩衝區
        if output == '\n':  # 如果換行, 則重置最後一行
            _last_line_buffer.close()
            _last_line_buffer = io.StringIO()
        else:
            _last_line_buffer.write(output)

def set_prompt(prompt:str) -> None:
    '''設定提示符'''
    global _prompt
    _prompt = prompt

def start():
    '''啟動cmd程序並輪詢輸出'''
    global _proc,_buffer,_last_line_buffer
    _buffer = io.StringIO()
    _last_line_buffer = io.StringIO()
    
    _proc = subprocess.Popen('cmd.exe', 
                            stdin=subprocess.PIPE, 
                            stdout=subprocess.PIPE, 
                            #stderr=subprocess.PIPE, 
                            stderr=subprocess.STDOUT,  # 錯誤資訊也同時輸出
                            #encoding='gbk',  # windows下的編碼, 可以列印漢字資訊, 是不是`text=True`就可以了?
                            cwd=None,  # 工作目錄, 後續可以增加設定工作目錄的功能
                            bufsize=1,
                            text=True)
    _output()  # 在另一個執行緒中輪詢輸出

def stop():
    '''停止cmd程序'''
    #send('exit')
    _proc.terminate()
    

def write_line(line:str) -> None:
    '''寫入命令. 寫入命令之前先清除之前的輸出. 以免有干擾.'''
    clear_output()  # 先清除之前的輸出
    _proc.stdin.write(line + '\n')
    _proc.stdin.flush()

BTW: 雖然主要是針對windows系統的, 但是改造一下, 應該也可以用於linux系統, 因為是純python語言開發的.

相關文章