# `cmd` --- 以列為導向的命令直譯器支援

原始碼：Lib/cmd.py

`Cmd` 類別提供了一個簡單的架構，用於撰寫列導向的命令直譯器。這類直譯器常用於測試控制工具、管理工具以及日後將包裝於更高階介面的原型中。

class cmd.Cmd(completekey='tab', stdin=None, stdout=None)

  `Cmd` 實例或其子類別實例是一種列導向的直譯器架構。通常沒有必要直接實例化 `Cmd` 本身；它更適合作為你自定義的直譯器類別的父類別，讓你能繼承 `Cmd` 的方法，並封裝動作方法。

  可選引數 completekey 是 `readline` 模組中用於自動完成的按鍵名稱；預設為 `Tab`。若 completekey 不為 `None` 且 `readline` 可用，則會自動啟用命令自動完成功能。

  預設值 `'tab'` 會被特殊處理，使其在所有的 `readline.backend` 中皆代表 `Tab` 鍵。具體來說，若 `readline.backend` 為 `editline`，則 `Cmd` 會改用 `'^I'` 取代 `'tab'`。請注意，其他值不會有此處理方式，且可能僅能在特定的後端中適用。

  可選引數 stdin 與 stdout 用來指定 Cmd 實例或其子類別實例所使用的輸入與輸出檔案物件。若未指定，預設為 `sys.stdin` 與 `sys.stdout`。

  若你希望使用指定的 stdin，請務必將該實例的 `use_rawinput` 屬性設為 `False`，否則 stdin 會被忽略。

  在 3.13 版的變更: 對於 `editline`，`completekey='tab'` 會被替換為 `'^I'`。

## Cmd 物件

`Cmd` 實例具有以下方法：

Cmd.cmdloop(intro=None)

  重複顯示提示字元、接收輸入、剖析接收到的輸入字串前綴，並派發給動作方法，將其餘部分作為引數傳遞給它們

  此可選引數為橫幅或導言字串，會在首次顯示提示字元前輸出（此值會覆寫 `intro` 類別屬性）。

  如果已載入 `readline` 模組，輸入將自動繼承類似 bash 的歷史紀錄編輯功能（例如 `Control`-`P` 可向上捲動至上一個命令，`Control`-`N` 向下捲動至下一個命令，`Control`-`F` 非破壞性地將游標向右移動，`Control`-`B` 非破壞性地將游標向左移動等）。

  當輸入為檔案結尾（EOF）時，會傳回字串 `'EOF'`。

  直譯器實例僅當存在 `do_foo()` 方法時，才會識別命令名稱 `foo`。作為特殊情況，以字元 `'?'` 開頭的列會被派發至 `do_help()` 方法；另一個特殊情況是，以字元 `'!'` 開頭的列會被派發至 `do_shell()` 方法（若該方法已定義）。

  當 `postcmd()` 方法回傳真值時，此方法將會結束。傳遞給 `postcmd()` 的 stop 引數是該命令對應的 `do_*()` 方法的回傳值。

  如果啟用了自動完成，命令的自動完成將會自動執行，而命令引數的自動完成則是透過呼叫 `complete_foo()` 方法並傳入 text、line、begidx 和 endidx 引數來處理。text 是要比對的字串前綴：所有回傳的符合項都必須以此字串開頭。line 是目前的輸入列（前置空白會被移除），begidx 和 endidx 則分別是前綴字串的起始與結束索引，可用來根據引數所在的位置提供不同的自動完成結果。

Cmd.do_help(arg)

  所有 `Cmd` 的子類別都會繼承預先定義的 `do_help()` 方法。當此方法接收到引數 `'bar'` 時，會呼叫對應的 `help_bar()` 方法；若該方法不存在，則會列印 `do_bar()` 的說明字串（若有的話）。若未提供任何引數，`do_help()` 會列出所有可用的說明主題（也就是所有具有對應 `help_*()` 方法或有說明字串的命令），並且也會列出所有尚未記錄的命令。

Cmd.onecmd(str)

  將引數視為在回應提示字元時所輸入的內容來直譯。這個方法可以被覆寫，但通常不需要這麼做；參見 `precmd()` 與 `postcmd()` 方法，它們提供實用的執行勾點（hook）。此方法的回傳值是一個旗標，用來指出是否應該停止直譯器對命令的直譯。若有對應 str 命令的 `do_*()` 方法，則會回傳該方法的回傳值；否則，回傳值將來自 `default()` 方法。

Cmd.emptyline()

  在回應提示字元時輸入空白列，會呼叫此方法。若此方法未被覆寫，則會重復上一次輸入的非空命令。

Cmd.default(line)

  當輸入列中的命令前綴無法辨識時，會呼叫此方法。若此方法未被覆寫，則會輸出並回傳錯誤訊息。

Cmd.completedefault(text, line, begidx, endidx)

  當沒有對應特定命令的 `complete_*()` 方法時，會呼叫此方法以完成輸入列。預設會回傳空串列。

Cmd.columnize(list, displaywidth=80)

  此方法用來將字串串列顯示為緊湊的欄集合。每一欄的寬度僅足以容納其內容，各欄之間以兩個空格分隔，以提高可讀性。

Cmd.precmd(line)

  勾點方法會在直譯命令列 line 前執行，但會在提示字元產生並顯示後才觸發。這個方法在 `Cmd` 類別中為 stub，預期由子類別覆寫。回傳值會作為 `onecmd()` 方法所執行的命令；`precmd()` 的實作可以重寫該命令，或直接回傳未變更的 line。

Cmd.postcmd(stop, line)

  勾點方法會在命令派發完成後執行。這個方法在 `Cmd` 類別中為 stub，預期由子類別覆寫。line 是剛剛執行的命令列，而 stop 是一個旗標，用來指出在呼叫 `postcmd()` 後是否應終止執行；該值即為 `onecmd()` 方法的回傳值。本方法的回傳值將會更新內部的 stop 旗標；若回傳 false，則會繼續進行直譯。

Cmd.preloop()

  當 `cmdloop()` 被呼叫時，此勾點方法會執行一次。這個方法在 `Cmd` 類別中為 stub，預期由子類別覆寫。

Cmd.postloop()

  當 `cmdloop()` 即將回傳時，此勾點方法會執行一次。這個方法在 `Cmd` 類別中為 stub，預期由子類別覆寫。

`Cmd` 子類別的實例包含一些公開的實例變數：

Cmd.prompt

  用來請求輸入的提示字元。

Cmd.identchars

  可作為命令前綴的字元字串

Cmd.lastcmd

  最後一個遇到的非空命令前綴

Cmd.cmdqueue

  排入佇列的輸入列串列。當 `cmdloop()` 需要新輸入時，會檢查 cmdqueue 串列；若不為空，其元素將依序處理，就如同它們是在提示字元中輸入的一樣。

Cmd.intro

  作為簡介或橫幅的字串。可透過為 `cmdloop()` 方法提供引數來覆寫此內容。

Cmd.doc_header

  若說明輸出包含已記錄命令的區段，則會顯示的標頭字串。

Cmd.misc_header

  若說明輸出包含雜項說明主題的區段（也就是存在 `help_*()` 方法但沒有對應的 `do_*()` 方法），則會顯示的標頭字串。

Cmd.undoc_header

  若說明輸出包含未記錄命令的區段（也就是存在 `do_*()` 方法但沒有對應的 `help_*()` 方法），則會顯示的標頭字串。

Cmd.ruler

  用於在說明訊息的標頭下方繪製分隔線的字元。若為空，則不會繪製分隔線。預設為 `'='`。

Cmd.use_rawinput

  一個旗標，預設為 true。若為 true，`cmdloop()` 會使用 `input()` 來顯示提示字元並讀取下一個命令；若為 false，則會改用 `sys.stdout.write()` 和 `sys.stdin.readline()`。（這表示在支援的系統中，透過 import `readline` module，直譯器將自動支援類似 Emacs 的列編輯與命令歷史快捷鍵。）

## Cmd 範例

`cmd` module 主要用於建構自訂 shell，讓使用者能以互動方式操作程式。

本節將示範如何以 `turtle` module 中的幾個命令為基礎，建立一個簡單的 shell。

像是 `forward()` 這樣的基本 turtle 命令，可透過新增名為 `do_forward()` 的方法加入至 `Cmd` 子類別中。傳入的引數會轉換為數值，並傳送給 turtle 模組。該方法的說明字串會用於 shell 所提供的說明功能中。

此範例同時包含一個簡單的錄製與重播功能，其實作方式是透過 `precmd()` 方法，負責將輸入轉為小寫並寫入檔案。`do_playback()` 方法則會讀取該檔案，並將錄製的命令加入 `cmdqueue` 中以供立即重播:

    import cmd, sys
    from turtle import *

    class TurtleShell(cmd.Cmd):
        intro = '歡迎來到 turtle shell。輸入 help 或 ? 來列出命令。\n'
        prompt = '(turtle) '
        file = None

        # ----- 基本烏龜命令 -----
        def do_forward(self, arg):
            '將烏龜向前移動指定的距離： FORWARD 10'
            forward(*parse(arg))
        def do_right(self, arg):
            '將烏龜右轉指定的角度： RIGHT 20'
            right(*parse(arg))
        def do_left(self, arg):
            '將烏龜左轉指定的角度： LEFT 90'
            left(*parse(arg))
        def do_goto(self, arg):
            '將烏龜移動到指定的絕對位置並改變方向。 GOTO 100 200'
            goto(*parse(arg))
        def do_home(self, arg):
            '將烏龜返回起始位置： HOME'
            home()
        def do_circle(self, arg):
            '畫出指定半徑、範圍和步數的圓： CIRCLE 50'
            circle(*parse(arg))
        def do_position(self, arg):
            '顯示當前烏龜位置： POSITION'
            print('當前位置是 %d %d\n' % position())
        def do_heading(self, arg):
            '顯示當前烏龜方向角度： HEADING'
            print('當前方向是 %d\n' % (heading(),))
        def do_color(self, arg):
            '設定顏色： COLOR BLUE'
            color(arg.lower())
        def do_undo(self, arg):
            '撤銷（重複）最後一次烏龜動作： UNDO'
        def do_reset(self, arg):
            '清除畫面並將烏龜返回到中心： RESET'
            reset()
        def do_bye(self, arg):
            '停止錄製、關閉烏龜視窗並退出： BYE'
            print('感謝使用 Turtle')
            self.close()
            bye()
            return True

        # ----- 錄製與回放 -----
        def do_record(self, arg):
            '將未來命令儲存至檔案： RECORD rose.cmd'
            self.file = open(arg, 'w')
        def do_playback(self, arg):
            '從檔案回放命令： PLAYBACK rose.cmd'
            self.close()
            with open(arg) as f:
                self.cmdqueue.extend(f.read().splitlines())
        def precmd(self, line):
            line = line.lower()
            if self.file and 'playback' not in line:
                print(line, file=self.file)
            return line
        def close(self):
            if self.file:
                self.file.close()
                self.file = None

    def parse(arg):
        '將一串零個或多個數字轉換為引數 tuple'
        return tuple(map(int, arg.split()))

    if __name__ == '__main__':
        TurtleShell().cmdloop()

以下是與 turtle shell 的一段範例互動，展示了說明功能、使用空白列重複執行命令，以及簡單的錄製與重播功能：

    歡迎來到 turtle shell。輸入 help 或 ? 來列出命令。

    (turtle) ?

    已記錄的命令（輸入 help <主題>）：
    ========================================
    bye     color    goto     home  playback  record  right
    circle  forward  heading  left  position  reset   undo

    (turtle) help forward
    將烏龜向前移動指定的距離： FORWARD 10
    (turtle) record spiral.cmd
    (turtle) position
    當前位置是 0 0

    (turtle) heading
    當前方向是 0

    (turtle) reset
    (turtle) circle 20
    (turtle) right 30
    (turtle) circle 40
    (turtle) right 30
    (turtle) circle 60
    (turtle) right 30
    (turtle) circle 80
    (turtle) right 30
    (turtle) circle 100
    (turtle) right 30
    (turtle) circle 120
    (turtle) right 30
    (turtle) circle 120
    (turtle) heading
    當前方向是 180

    (turtle) forward 100
    (turtle)
    (turtle) right 90
    (turtle) forward 100
    (turtle)
    (turtle) right 90
    (turtle) forward 400
    (turtle) right 90
    (turtle) forward 500
    (turtle) right 90
    (turtle) forward 400
    (turtle) right 90
    (turtle) forward 300
    (turtle) playback spiral.cmd
    當前位置是 0 0

    當前方向是 0

    當前方向是 180

    (turtle) bye
    感謝使用 Turtle