zoukankan      html  css  js  c++  java
  • 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
    ", 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
    ", 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的使用

  • 相关阅读:
    合并2个dll成一个,好处你懂的
    来吧,给你的Winform列表控件画个妆
    DataGridView 的cell赋值没有线程间访问的限制吗?
    设计模式之单例模式
    一个铜钱的故事(转)
    博客美化小结
    FTP操作类(支持异步)
    多线程学习之AsyncOperation实现线程间交互
    消息传递选择:返回值 or 抛出异常
    IIS8.5关于“ 配置错误 不能在此路径中使用此配置节”的解决办法
  • 原文地址:https://www.cnblogs.com/sparkdev/p/10868873.html
Copyright © 2011-2022 走看看