Golang : cobra 包解析


筆者在《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的使用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM