CMDR-05: Tricks / Walks / Hooks

hedzr發表於2019-06-16

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

所有這個系列包括:

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

Tricks

~~debug

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

--tree

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

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

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

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

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

一個樣例如下圖:

CMDR-05: Tricks / Walks / Hooks

這是我在開發階段執行 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 則允許你自行提供你的實現。

小結

暫時結束。繼續改進。