Go命令行库cobra

Go命令行库cobra

Cobra 是一个 Golang 包,它提供了简单的接口来创建命令行程序。同时,Cobra 也是一个应用程序,用来生成应用框架,从而开发以 Cobra 为基础的应用。

主要功能

  • 简易的子命令行模式,如 app server, app fetch 等等
  • 完全兼容 posix 命令行模式
  • 嵌套子命令 subcommand
  • 支持全局,局部,串联 flags
  • 使用 cobra 很容易的生成应用程序和命令,使用 cobra create appname 和 cobra add cmdname
  • 如果命令输入错误,将提供智能建议,如 app srver,将提示 srver 没有,是不是 app server
  • 自动生成 commands 和 flags 的帮助信息
  • 自动生成详细的 help 信息,如 app help
  • 自动识别帮助 flag -h,–help
  • 自动生成应用程序在 bash 下命令自动完成功能
  • 自动生成应用程序的 man 手册
  • 命令行别名
  • 自定义 help 和 usage 信息
  • 可选的与 viper apps 的紧密集成

cobra 中的主要概念

  • commands 行为
  • args 命令行参数(或称为位置参数)
  • flags 对行为的改变(即命令行选项)

执行命令行程序时的一般格式为: APPNAME COMMAND ARG --FLAG

创建 cobra 应用

获取最新版本

$ go get -u github.com/spf13/cobra@latest

安装 cobra-cli

$ go install github.com/spf13/cobra-cli@latest

创建

$ cd /pathto/mysrc
$ go mod init
$ cobra-cli init

执行后,该目录下生成的结构如下:

▾ demo
    ▾ cmd/
        root.go
    main.go

使用 cobra 程序生成命令代码

除了生成应用程序框架,还可以生成子命令的代码文件。添加自命令 mysub1

$ cd /pathto/mysrc
$ cobra-cli add mysub1

为命令添加具体的功能

打开文件 cmd/root.go ,找到变量 rootCmd 的初始化过程并为之设置 Run 方法:

Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("cobra demo program")
},

创建一个 version Command 用来输出当前的软件版本。先在 cmd 目录下添加 version.go 文件,编辑文件的内容如下:

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

func init() {
    rootCmd.AddCommand(versionCmd)
}

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Print the version number of cobrademo",
    Long:  `All software has versions. This is cobrademo's`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("cobrademo version is v1.0")
    },
}

为 Command 添加选项(flags)

选项(flags)用来控制 Command 的具体行为。根据选项的作用范围,可以把选项分为两类:

  • persistent
  • local

对于 persistent 类型的选项,既可以设置给该 Command,又可以设置给该 Command 的子 Command。对于一些全局性的选项,比较适合设置为 persistent 类型,比如控制输出的 verbose 选项:

var Verbose bool
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")

local 类型的选项只能设置给指定的 Command,比如下面定义的 source 选项:

var Source string
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

该选项不能指定给 rootCmd 之外的其它 Command。 默认情况下的选项都是可选的,但一些用例要求用户必须设置某些选项,这种情况 cobra 也是支持的,通过 Command 的 MarkFlagRequired 方法标记该选项即可:

var Name string
rootCmd.Flags().StringVarP(&Name, "name", "n", "", "user name (required)")
rootCmd.MarkFlagRequired("name")

命令行参数(arguments)

命令行参数(arguments)与命令行选项的区别(flags/options)。以常见的 ls 命令来说,其命令行的格式为: ls [OPTION]... [FILE]… 其中的 OPTION 对应本文中介绍的 flags,以 --- 开头;而 FILE 则被称为参数(arguments)或位置参数。一般的规则是参数在所有选项的后面,上面的 … 表示可以指定多个选项和多个参数。

cobra 默认提供了一些验证方法:

  • NoArgs - 如果存在任何位置参数,该命令将报错
  • ArbitraryArgs - 该命令会接受任何位置参数
  • OnlyValidArgs - 如果有任何位置参数不在命令的 ValidArgs 字段中,该命令将报错
  • MinimumNArgs(int) - 至少要有 N 个位置参数,否则报错
  • MaximumNArgs(int) - 如果位置参数超过 N 个将报错
  • ExactArgs(int) - 必须有 N 个位置参数,否则报错
  • ExactValidArgs(int) 必须有 N 个位置参数,且都在命令的 ValidArgs 字段中,否则报错
  • RangeArgs(min, max) - 如果位置参数的个数不在区间 min 和 max 之中,报错

帮助信息(help command)

cobra 会自动添加 --help(-h) 选项,同时还自动添加了 help 子命,默认效果和使用 –help 选项相同。如果为 help 命令传递其它命令作为参数,则会显示对应命令的帮助信息。也可以自定义 help 的处理

cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)

提示信息(usage message)

提示信息和帮助信息很相似,只不过它是在你输入了非法的参数、选项或命令时才出现的。也可以自定义提示信息:

cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)

在 Commnad 执行前后执行额外的操作

Command 执行的操作是通过 Command.Run 方法实现的,为了支持我们在 Run 方法执行的前后执行一些其它的操作,Command 还提供了额外的几个方法,它们的执行顺序如下:

  1. PersistentPreRun
  2. PreRun
  3. Run
  4. PostRun
  5. PersistentPostRun
var rootCmd = &cobra.Command{
    Use:   "cobrademo",
    Short: "sparkdev's cobra demo",
    Long: "the demo show how to use cobra package",
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args)
    },
    PreRun: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Inside rootCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("cobra demo program, with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Inside rootCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args)
    },
}

代码解析

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() 方法中,在执行 execute() 方法前,需要先通过 Find() 方法解析命令行上的子命令:

cmd, flags, err = c.Find(args)

比如我们执行下面的命令:

$ ./myApp mycmd1

解析出的 cmd 就是 imamycmd1ge 子命令,接下来就是执行 mycmd1 子命令的执行逻辑。

Find() 方法的逻辑如下:

$ ./myApp help mycmd1

这里的 myApp 对应代码中的 rootCmd,Find() 方法中定义了一个名称为 innerfind 的函数,innerfind 从参数中解析出下一个名称,这里是 help,然后从 rootCmd 开始查找解析出的名称 help 是不是当前命令的子命令,如果 help 是 rootCmd 的子命令,继续查找。接下来查找名称 mycmd1,发现 mycmd1 不是 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()

示例:

通过 cobra 实现了一个命令行程序 myApp,它有一个子命令 image,image 也有一个子命令 times。执行下面的命令:

$ ./myApp help mycmd1

在 help 命令的 Run 方法中,c 为 help 命令, args 为 mycmd1。结果就是通过 help 查看 mycmd1 命令的帮助文档。如果 mycmd1 后面还有其他的子命令,比如:

$ ./myApp help mycmd1 mysub1

则 c.Root().Find(args) 逻辑会找出子命令 mysub1(此时 args 为 mycmd1 mysub1),最终由 help 查看 mysub1 命令的帮助文档。 注意: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 就可以直接使用

输出 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}}`

参考资料:

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计