CMDR-05: Tricks / Walks / Hooks

暫時來講,這是最後一篇關於 cmdr 的系列介紹文章了。

所有這個系列包括:

這一次的內容算是雜燴亂燉。

Tricks

~~debug

已經在前文講述過了。這裏不再湊字數了。

--tree

cmdr 提供了一個內置的選項:--tree

雖然這是一個選項,但它和 --version 一樣是有着命令一樣的效果:如果 cmdr 在命令行參數中檢測到了 --tree,那麼它會忽略已經處理的和將要處理的子命令、選項,直接執行 --treeAction

要想達到類似的效果並不困難:

定義一個選項,重載其 Action 字段到一個響應函數,並且在該響應函數的結尾返回 cmdr.ErrShouldBeStopException,這樣就會在該選項被識別時並執行Action後直接退出應用程序了。

--tree 的功能是打印出全部命令和子命令,以樹結構方式呈現出來。

一個樣例如下圖:

clipboard.png

這是我在開發階段執行 examples/demo 小程序所得到的結果。

Walk for all commands

--tree 實際上是利用了 cmdr 內建的 WalkAllCommands() 所提供的遍歷方式。

對所有命令及其選項進行遍歷,實際上有兩種方式:一是利用 Painter 以及相應的內部機制,二是通過 WalkAllCommands 明確地遍歷。

Painter

Painter 是一個接口。它被用在輸出幫助屏這個方面。儘管輸出幫助屏只是一個小小的功能,但你還是可以自定義它的行爲。你可以自行實現 Painter 接口並通過 SetCurrentHelpPainter(painter) 來更改幫助屏的顯示內容。

如果你真的想這麼做,可以查閱 Painter 的定義,也可以 issue 到我,或許說不定我能夠有所建議。

Walker

WalAllCommands(cmd, index, walker) 是一個更爲強大的遍歷器,實際上 manpage,markdown 的輸出就是通過這個機制來實現的。利用這個遍歷器,你可以便利整個命令集的樹狀結構。一般來說,你應該給它傳遞 cmd=nil, index=0 的參數值來開始你的遍歷,這表示將會從頂級命令開始遍歷,而且將其視作第 0 層。index 這個參數將會在遍歷器遞歸時自動修正到符合層級計數,然後會被傳遞給 walker。我只是懶得將它改成 level 名字了,它就是那個用途。

例如 --tree 的實現源代碼如下:

func dumpTreeForAllCommands(cmd *Command, args []string) (err error) {
    command := &rootCommand.Command
    _ = walkFromCommand(command, 0, func(cmd *Command, index int) (e error) {
        if cmd.Hidden {
            return
        }

        deep := findDepth(cmd) - 1
        if deep == 0 {
            fmt.Println("ROOT")
        } else {
            sp := strings.Repeat("  ", deep)
            // fmt.Printf("%s%v - \x1b[%dm\x1b[%dm%s\x1b[0m\n",
            //     sp, cmd.GetTitleNames(),
            //     BgNormal, CurrentDescColor, cmd.Description)

            if len(cmd.Deprecated) > 0 {
                fmt.Printf("%s\x1b[%dm\x1b[%dm%s - %s\x1b[0m [deprecated since %v]\n",
                    sp, BgNormal, CurrentDescColor, cmd.GetTitleNames(), cmd.Description,
                    cmd.Deprecated)
            } else {
                fmt.Printf("%s%s - \x1b[%dm\x1b[%dm%s\x1b[0m\n",
                    sp, cmd.GetTitleNames(), BgNormal, CurrentDescColor, cmd.Description)
            }
        }
        return
    })
    return ErrShouldBeStopException
}

比較

可以想象到你能夠藉助這個遍歷器實現某些更強大的特性,在具備遍歷能力的基礎上,我們其實可以設計更強大的命令行界面結構,而不必擔心過分複雜帶來的負面效果。

關於如何設計命令行界面的體系結構,保持其清晰性,這個不是我們再這個系列文章中要討論的話題。

至於 Painter 和 Walker,其區別也很明顯。Painter 是被限定在幫助屏構造層面的,且不會遞歸下去,除非你想自行實現。Walker 是全局層面的遞歸遍歷器,面向的是所有的命令。

Actions

Action for Command

CommandAction 字段可以定義你的命令的業務實現邏輯。

func MsTagsList(cmd *cmdr.Command, args []string) (err error) {
    return
}

一般來說,你在 impl package 中定義業務實現邏輯的入口,如同上面的代碼示例,並在某個 Command 的數據定義中引用它。

msTagsListCommand = &cmdr.Command {
    BaseOpt: cmdr.BaseOpt {
        Short: "ls",
        Full: "list",
        Description: "list all tags of a micro-service",
        Action: impl.MsTagsList,
    }
}

所以,對於 Command 來說,Action 可能是最重要的 Hook。

PreAction & PostAction for Command

CommandAction 被執行之前,其 PreAction 會被首先調用,你可以定義自己的邏輯,例如檢查特定條件是否滿足,如果不滿足則返回一個error以通知cmdr錯誤性結束。如果你認爲並沒有錯誤發生,但仍應該結束處理,你可以返回一個 cmdr.ErrShouldBeStopException,這樣的話 cmdr 也會結束處理,但整個 program 會被正常終止:反應到 Shell 層面上時,此時程序是無錯的,Shell返回值爲0。

CommandAction 被執行之後,無論處理結果如何,PostAction 將會被調用。它可以用來進行某些退出時邏輯處理。

PreAction & PostAction for RootCommand

在被命中的命令的 PreActionPostAction 被執行之際,RootCommandPreActionPostAction 也會分別被執行。這是一個關鍵性的特性。它使得你可以妥善地實現你的全局預處理和後處理邏輯,例如註冊微服務到註冊中心以及撤銷註冊,連接到數據庫和關閉數據庫連接,等等。

Action for Flag

對於 Flag 來說,沒有 Pre/Post 機制。

Flag 具有 Action 的 Hook,所以你可以在每個 Flag 被命中時做點什麼事。例如,反轉或復位 Owner 的所有其他 bool flags,或者爲應用程序的其他配置設定一整組預設方案,等等。

值得一提的是,在 v0.2.15 之後,我們已經實現了 ToggleGroup,因此對於想要建立RadioButtonGroup 效果的場景來說,你倒是不必再手寫邏輯了。

Listeners

AddOnAfterXrefBuilt(cb HookXrefFunc), AddOnBeforeXrefBuilding(cb HookXrefFunc)

Xref 術語是一個特定節點的表述。

當 cmdr.Exec(rootCmd) 進入狀態時,它依次做這些事:

  1. 初始化相關內務
  2. 調用所有 beforeXrefBuilding Hooks
  3. buildXref
  4. 調用所有 afterXrefBuilt Hooks
  5. 開始處理命令行的所有參數
  6. ...

在 buildXref 階段,cmdr 實際上建立了 rootCmd 及其所有子命令和選項集合 的內部map、索引等等交叉引用。這個過程中,cmdr 處理 rootCmd,也查找配置文件以及子目錄 conf.d,也對環境變量進行搜尋。

當 buildXref 完成之後,cmdr.GetXXX() 就是完全可用的狀態了。

如此,beforeXrefBuilding 和 afterXrefBuilt 的 hooks 實際上是你想要進行定製化操作的關鍵節點。

歡迎使用。

AddOnConfigLoadedListener,RemoveOnConfigLoadedListener,SetOnConfigLoadedListener

應該不必解釋太多。

當外部文件被修改時,cmdr自動載入變化,併合併到已有的選項集合中,然後發出回調通知觀察者應該做點什麼了。

你可以在任何地方任意地發出觀察約定。整體上說這是極爲輕量級的。這麼做的原因是不同業務模塊有必要自行管理自己的相關選項集而不是在全局的某一個集中點處理全部模塊的全部選項集合,那太耦合了。

需要特別指出的是,配置文件總是一個主文件,加上可選多個子配置文件(都被放在配置文件所在目錄的 conf.d 子目錄之下)。而我們內部的文件系統 Watcher 只會監聽 conf.d 之中的文件變化,而忽略主文件的變化。

所以你不應該在主配置文件中放置太多具體選項。好的實踐是將一切配置都切分到 conf.d 之中。

SetPredefinedLocations(locations string) 允許你指定配置文件的搜索路徑。

SetOnConfigLoadedListener 的用途是臨時禁用和啓用一個觀察者。

SetCustomShowBuildInfo(fn func())SetCustomShowVersion(fn func())

說它們是 Hook 也不算錯。

cmdr 自動提供 ShowBuildInfo 和 ShowVersion 實現,用於打印 編譯信息屏 和 版本信息屏。

SetCustonShowBuildInfo 和 SetCustomShowVersion 則允許你自行提供你的實現。

小結

暫時結束。繼續改進。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章