筆者在《Golang : cobra 包簡介》一文中簡要的介紹了 cobra 包及其基本的用法,本文我們從程式碼的角度來了解下 cobra 的核心邏輯。
Command 結構體
Command 結構體是 cobra 抽象出來的核心概念,它的例項表示一個命令或者是一個命令的子命令。下面的程式碼僅展示 Command 結構體中一些比較重要的欄位:
type Command struct { // 使用者通過指定 Run 函式來完成命令 // PreRun 和 PostRun 則允許使用者在 Run 執行的前後時機執行自定義程式碼 PersistentPreRun func(cmd *Command, args []string) PreRun func(cmd *Command, args []string) Run func(cmd *Command, args []string) PostRun func(cmd *Command, args []string) PersistentPostRun func(cmd *Command, args []string) // commands 欄位包含了該命令的所有子命令 commands []*Command // parent 欄位記錄了該命令的父命令 parent *Command // 該命令的 help 子命令 helpCommand *Command ... }
執行命令的邏輯
cobra 包啟動程式執行的程式碼一般為:
cmd.Execute()
Execute() 函式會呼叫我們定義的 rootCmd(Command 的一個例項)的 Execute() 方法。
在 Command 的 Execute() 方法中又呼叫了 Command 的 ExecuteC() 方法,我們可以通過下面的呼叫堆疊看到執行命令邏輯的呼叫過程:
cmd.Execute() -> // main.go rootCmd.Execute() -> // root.go c.ExecuteC() -> // command.go cmd.execute(flags) -> // command.go c.Run() // command.go
c.Run() 方法即使用者為命令(Command) 設定的執行邏輯。
總是執行根命令的 ExecuteC() 方法
為了確保命令列上的子命令、位置引數和 Flags 能夠被準確的解析,cobra 總是執行根命令的 ExecuteC() 方法,其實現為在 ExecuteC() 方法中找到根命令,然後執行根命令的 ExecuteC() 方法,其邏輯如下:
// ExecuteC executes the command. func (c *Command) ExecuteC() (cmd *Command, err error) { // Regardless of what command execute is called on, run on Root only if c.HasParent() { return c.Root().ExecuteC() } ... }
解析命令列子命令
ExecuteC() 方法中,在執行 execute() 方法前,需要先通過 Find() 方法解析命令列上的子命令:
cmd, flags, err = c.Find(args)
比如我們執行下面的命令:
$ ./myApp image
解析出的 cmd 就是 image 子命令,接下來就是執行 image 子命令的執行邏輯。
Find() 方法的邏輯如下:
$ ./myApp help image
這裡的 myApp 在程式碼中就是 rootCmd,Find() 方法中定義了一個名稱為 innerfind 的函式,innerfind 從引數中解析出下一個名稱,這裡是 help,然後從 rootCmd 開始查詢解析出的名稱 help 是不是當前命令的子命令,如果 help 是 rootCmd 的子命令,繼續查詢。接下來查詢名稱 image,發現 image 不是 help 的子命令,innerfind 函式就返回 help 命令。execute() 方法中就執行這個找到的 help 子命令。
為根命令新增 help 子命令
在執行 ExecuteC() 方法時,cobra 會為根命令新增一個 help 子命令,這個子命令主要用來提供子命令的幫助資訊。因為任何一個程式都需要提供輸出幫助資訊的方式,所以 cobra 就為它實現了一套預設的邏輯。help 子命令是通過 InitDefaultHelpCmd() 方法新增的,其實現程式碼如下:
// InitDefaultHelpCmd adds default help command to c. // It is called automatically by executing the c or by calling help and usage. // If c already has help command or c has no subcommands, it will do nothing. func (c *Command) InitDefaultHelpCmd() { if !c.HasSubCommands() { return } if c.helpCommand == nil { c.helpCommand = &Command{ Use: "help [command]", Short: "Help about any command", Long: `Help provides help for any command in the application. Simply type ` + c.Name() + ` help [path to command] for full details.`, Run: func(c *Command, args []string) { cmd, _, e := c.Root().Find(args) if cmd == nil || e != nil { c.Printf("Unknown help topic %#q\n", args) c.Root().Usage() } else { cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown cmd.Help() } }, } } c.RemoveCommand(c.helpCommand) c.AddCommand(c.helpCommand) }
沒有找到使用者指定的子命令
如果沒有找到使用者指定的子命令,就輸出錯誤資訊,並呼叫根命令的 Usage() 方法:
c.Printf("Unknown help topic %#q\n", args) c.Root().Usage()
cobra 預設提供的 usage 模板如下:
`Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} Aliases: {{.NameAndAliases}}{{end}}{{if .HasExample}} Examples: {{.Example}}{{end}}{{if .HasAvailableSubCommands}} Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} Flags: {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} Global Flags: {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} `
找到了使用者指定的子命令
如果找到使用者指定的子命令,就為子命令新增預設的 help flag,並執行其 Help() 方法:
cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown cmd.Help()
為了解釋 help 子命令的執行邏輯,我們舉個例子。比如我們通過 cobra 實現了一個命令列程式 myApp,它有一個子命令 image,image 也有一個子命令 times。執行下面的命令:
$ ./myApp help image
在 help 命令的 Run 方法中,c 為 help 命令, args 為 image。結果就是通過 help 檢視 image 命令的幫助文件。如果 image 後面還有其他的子命令,比如:
$ ./myApp help image times
則 c.Root().Find(args) 邏輯會找出子命令 times(此時 args 為 image times),最終由 help 檢視 times 命令的幫助文件。
注意:help 資訊中包含 usage 資訊。
為命令新增 help flag
除了在 InitDefaultHelpCmd() 方法中會呼叫 InitDefaultHelpFlag() 方法,在 execute() 方法中執行命令邏輯前也會呼叫 InitDefaultHelpFlag() 方法為命令新增預設的 help flag,
c.InitDefaultHelpFlag()
下面是 InitDefaultHelpFlag() 方法的實現:
// InitDefaultHelpFlag adds default help flag to c. // It is called automatically by executing the c or by calling help and usage. // If c already has help flag, it will do nothing. func (c *Command) InitDefaultHelpFlag() { c.mergePersistentFlags() if c.Flags().Lookup("help") == nil { usage := "help for " if c.Name() == "" { usage += "this command" } else { usage += c.Name() } c.Flags().BoolP("help", "h", false, usage) } }
這讓我們不必為命令新增 help flag 就可以直接使用!至於 falg 的解析,則是通過 pflag 包實現的,不瞭解 pflag 包的朋友可以參考《Golang : pflag 包簡介》。
輸出 help 資訊
不管是 help 命令還是 help falg,最後都是通過 HelpFunc() 方法來獲得輸出 help 資訊的邏輯:
// HelpFunc returns either the function set by SetHelpFunc for this command // or a parent, or it returns a function with default help behavior. func (c *Command) HelpFunc() func(*Command, []string) { if c.helpFunc != nil { return c.helpFunc } if c.HasParent() { return c.Parent().HelpFunc() } return func(c *Command, a []string) { c.mergePersistentFlags() err := tmpl(c.OutOrStdout(), c.HelpTemplate(), c) if err != nil { c.Println(err) } } }
如果我們沒有指定自定義的邏輯,就找父命令的,再沒有就用 cobra 的預設邏輯。cobra 預設設定的幫助模板如下(包含 usage):
`{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} {{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
總結
本文簡要介紹了 cobra 包的主要邏輯,雖然忽略了眾多的實現細節,但梳理出了程式執行的主要過程,並對 help 子命令的實現以及 help flag 的實現進行了介紹。希望對大家瞭解和使用 cobra 包有所幫助。
參考:
spf13/cobra
Golang之使用Cobra
MAKE YOUR OWN CLI WITH GOLANG AND COBRA
Cobra簡介
golang命令列庫cobra的使用