GoLang Cobra 命令行解析组件

Cobra 命令行

特性

  • 简单的基于子命令的 CLIs:app serverapp fetch 等;
  • 完全兼容 POSIX(可移植操作系统接口) 的标志(包括短版和长版)
  • 嵌套子命令
  • 全局、局部和级联的标志
  • 使用 cobra init appnamecobra add cmdname 轻松生成应用程序和命令
  • 智能提示(app srver …did you mean app server
  • 自动生成命令和标志的帮助
  • 自动识别 -h--help 等帮助标识
  • 自动为你的应用程序生成的 bash 自动完成
  • 自动为你的应用程序生成 man 手册
  • 命令别名,以便你可以更改内容而不会破坏它们
  • 定义自己的帮助,用法等的灵活性。
  • 可选与 viper 紧密集成,可用于 12factor 应用程序

安装

Cobra 非常易用,首先使用 go get 命令安装最新版本。此命令将安装 cobra generator 的可执行文件及其依赖项:

1
$ go get -u github.com/spf13/cobra/cobra

概念

Cobra 构建在命令(commands)、参数(arguments)和 标志(flags)上。

Commands 代表动作,Args 是事物,Flags 是这些动作的修饰符。

最好的应用程序在使用时会像句子一样读起来。用户将知道如何使用该应用程序,因为他们将自然地了解如何使用它。

遵循的模式是 APPNAME VERB NOUN --ADJECTIVE。 或 APPNAME COMMAND ARG --FLAG

一些真实的例子可以更好地说明这一点。

在以下示例中,server 是命令,port 是标志:

1
hugo server --port=1313

在此命令中,我们告诉 Git 克隆 url 的内容:

1
git clone URL --bare

命令(Command)

命令是应用程序的核心。应用程序提供的每一个交互都包含在 Command 中。一个命令可以有子命令和可选的运行一个动作。

在上面的示例中,server 是命令。

cobra.Command API

标志(Flags)

一个标志是一种修饰命令行为的方式。Cobra 支持完全符合 POSIX(可移植操作系统接口) 的标志和 Go flag 包。

Cobra 命令可以定义一直保留到子命令的标志和仅可用于该命令的标志。

在上面的例子中,port 是标志。

标志的功能是 pflag 库提供的,该库是一个标准库的 fork,在维护相同接口的基础上兼容了 POSIX(可移植操作系统接口)

入门

欢迎大家提供自己的项目组织结构,但是通常基于 Cobra 的应用程序将遵循以下组织结构

  • ​ cmd

    放置命令的文件夹

    • add.go
    • your.go
    • commands.go
    • here.go
  • ​ main.go 应用程序入口

在 Cobra 应用程序中,通常 main.go 文件非常。它有一个目的:初始化 Cobra。

1
2
3
4
5
6
7
8
9
package main

import (
"{pathToYourApp}/cmd"
)

func main() {
cmd.Execute()
}

使用 Cobra 生成器

Cobra 提供了 CLI 来创建您的应用程序和添加任意你想要的命令。这是将 Cobra 集成到您的应用程序中的最简单方法。

这里 你可以查看更多关于生成器的资料。

使用 Cobra 库

要手动接入 Cobra,您需要创建一个 main.go 文件和 rootCmd 文件。您可以选择提供合适的其他命令。

创建 rootCmd

Cobra 不需要任何特殊的构造函数。只需创建您的命令。

理想情况下,将其放置在 /cmd/root.go 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// rootCmd 代表没有调用子命令时的基础命令
var rootCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com`,
// 如果有相关的 action 要执行,请取消下面这行代码的注释
// Run: func(cmd *cobra.Command, args []string) { },
}

// Execute 将所有子命令添加到root命令并适当设置标志。
// 这由 main.main() 调用。它只需要对 rootCmd 调用一次。
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

您还将在 init() 函数中定义标志并处理配置。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// cmd/root.go
package cmd

import (
"fmt"
"os"

"github.com/spf13/cobra"

homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)

var cfgFile string
var projectBase string
var userLicense string

// rootCmd 代表没有调用子命令时的基础命令
var rootCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com`,
// 如果有相关的 action 要执行,请取消下面这行代码的注释
// Run: func(cmd *cobra.Command, args []string) { },
}

// Execute 将所有子命令添加到root命令并适当设置标志。会被 main.main() 调用一次。
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
rootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
viper.BindPFlag("projectbase", rootCmd.PersistentFlags().Lookup("projectbase"))
viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
viper.SetDefault("license", "apache")
}

func initConfig() {
// Don't forget to read config either from cfgFile or from home directory!
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}

// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}

if err := viper.ReadInConfig(); err != nil {
fmt.Println("Can't read config:", err)
os.Exit(1)
}
}

创建 main.go

有了根命令,你需要一个 main 函数去执行它。为了清晰起见,Execute 应该在根目录上运行,尽管可以在任何命令上调用它。

在 Cobra 应用中,main.go 是非常简单的。它只有一个作用——初始化 Cobra。

1
2
3
4
5
6
7
8
9
10
11
// main.go
package main

import (
"{pathToYourApp}/cmd"
)

func main() {
cmd.Execute()
}

创建额外的命令

可以定义其他命令,并且通常在 cmd/ 目录中为每个命令提供自己的文件。

如果要创建 version 命令,则可以创建 cmd/version.go 并使用以下命令进行填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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 Hugo",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
},
}

使用标志

标志提供修饰符以控制命令的操作方式。

由于标志是在不同位置定义和使用的,我们需要在外部定义一个具有正确作用域的变量,以分配要使用的标志。

1
2
3
var verbose bool
var source string

这里有两种不同分配标志的方法。

持久标志

标志可以是 “persistent” 的,这意味着该标志将可用于分配给它的命令以及该命令下的每个命令。对于全局标志,将标志分配为根上的持久标志。

1
2
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")

本地标志

也可以在本地分配一个标志,该标志仅适用于该特定命令。

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

父命令上的本地标志

默认情况下,Cobra 仅解析目标命令上的本地标志,而忽略父命令上的任何本地标志。通过启用 Command.TraverseChildren,Cobra 将在执行目标命令之前解析每个命令上的本地标志

1
2
3
4
5
command := cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
TraverseChildren: true,
}

用配置绑定标志

您还可以将标志与 viper 绑定:

1
2
3
4
5
6
7
var author string

func init() {
rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}

在此示例中,持久标记 author 与 viper 绑定。请注意,当用户未提供 --author 标志时,变量 author 不会设置为 config 中的值。

更多信息请查看 viper

必需标志

标志默认是可选的。如果你想在缺少标志时命令报错,请设置该标志为必需:

1
2
3
4
5
var region string

rootCmd.Flags().StringVarP(&region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")

位置和自定义参数

可以使用 Command 的 Args 字段指定位置参数的验证。

下面的验证符是内置的:

  • NoArgs - 如果有任何位置参数,该命令将报告错误。
  • ArbitraryArgs - 命令将接受任意参数
  • OnlyValidArgs - 如果 Command 的 ValidArgs 字段中不存在该位置参数,则该命令将报告错误。
  • MinimumNArgs(int) - 如果不存在至少 N 个位置参数,则该命令将报告错误。
  • MaximumNArgs(int) - 如果存在超过 N 个位置参数,则该命令将报告错误。
  • ExactArgs(int) - 如果不存在 N 个位置参数,则该命令将报告错误。
  • ExactValidArgs(int) - 如果没有确切的 N 个位置参数,或者如果 Command 的 ValidArgs 字段中不存在该位置参数,则该命令将报告并出错。
  • RangeArgs(min, max) - 如果 args 的数目不在期望的 args 的最小和最大数目之间,则该命令将报告并出错。

内置验证符使用实例:

1
2
3
4
5
6
7
8
9
var cmd = &cobra.Command{
Use: "hello",
Short: "hello",
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, World!")
},
}

如果只传递一个位置参数会报 Error: requires at least 2 arg(s), only received 1 的警告。

设置自定义验证器的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var cmd = &cobra.Command{
Short: "hello",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires at least one arg")
}
if myapp.IsValidColor(args[0]) {
return nil
}
return fmt.Errorf("invalid color specified: %s", args[0])
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, World!")
},
}

示例

在下面的例子中,我们定义了三个命令。两个在顶层,一个(cmdTimes)是子命令。在这种情况下,根目录不可执行,这意味着需要一个子命令。通过不为 rootCmd 提供 Run 来实现。

我们只为一个命令定义了一个标志。

关于标志的文档在 [pflag]github.com/spf13/pflag…%E3%80%82)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import (
"fmt"
"strings"

"github.com/spf13/cobra"
)

func main() {
var echoTimes int

var cmdPrint = &cobra.Command{
Use: "Print [string to print]",
Short: "Print anything to the screen",
Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Print: " + strings.Join(args, " "))
},
}

var cmdEcho = &cobra.Command{
Use: "echo [string to echo]",
Short: "Echo anything to the screen",
Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Print: " + strings.Join(args, " "))
},
}

var cmdTimes = &cobra.Command{
Use: "times [# times] [string to echo]",
Short: "Echo anyting to the screen more times",
Long: `echo things multiple times back to the user y providing
a count and a string.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}

cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")

// 设置根命令
var rootCmd = &cobra.Command{Use: "app"}
rootCmd.AddCommand(cmdPrint, cmdEcho)
cmdEcho.AddCommand(cmdTimes)

// 初始化应用
rootCmd.Execute()
}

更复杂的应用,请参考 Hugo 或者 GitHub CLI

帮助命令

当你添加了子命令,Cobra 会自动添加一些帮助命令。当你执行 app help 命令时会显示帮助信息。另外,help 还支持其他命令作为输入参数。举例来说,你有一个没有额外配置的 create 命令,app help create 是有效的。每一个命令还会自动获取一个 --help 标志。

示例

以下输出由 Cobra 自动生成。 除了命令和标志定义外,什么都不需要。

help 就像其他命令一样。并没有特殊的逻辑或行为。实际上,你可以根据需要提供自己的服务。

定义你自己的 help

你可以使用下面的方法提供你自己的 Help 命令或模板。

1
2
3
4
cmd.SetHelpCommand(cmd *Command)
cmd.setHelpCommand(f func(*Command, []string))
cmd.setHelpTemplate(s string)

后两者也适用于所有子命令。

使用信息

当用户提供无效的标志或无效的命令时,Cobra 会通过向用户显示 usage 进行响应。

定义你自己的使用信息

你可以提供你自己的 usage 函数或模板。像 help 一样,函数和模板可通过公共方法重写:

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

可以参考 GitHub CLI 的写法。

版本标志

如果给根命令设置了 Version 字段,Cobra 会添加一个顶级的 --version 标志。运行带有 –version 标志的应用程序,将使用版本模板将版本打印到 stdout。模板可以使用 cmd.SetVersionTemplate(s string) 函数自定义。

SetVersionTemplate 的使用可以参考 GitHub CLI

PreRun 和 PostRun Hooks

可以在执行命令之前和之后运行一个函数。PersistentPreRunPreRun 函数将在 Run 之前执行。PersistentPostRunPostRun 会在 Run 之后运行。如果子级未声明自己的 Persistent * Run 函数,则子级将继承父级的。这些函数的执行顺续如下:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

下面这个包含了两个命令的例子使用了这些特性。当子命令执行时,它会运行根命令的 PersistentPreRun,但是不会运行根命令的 PersistentPostRun

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (
"fmt"

"github.com/spf13/cobra"
)

func main() {
var rootCmd = &cobra.Command{
Use: "root [sub]",
Short: "My root command",
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("Inside rootCmd Run 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)
},
}

subCmd := &cobra.Command{
Use: "sub [no options!]",
Short: "My subcommand",
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside subCmd PreRun with args: %v\n", args)
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside subCmd Run with args: %v\n", args)
},
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside subCmd PostRun with args: %v\n", args)
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Printf("Inside subCmd PersistentPostRun with args: %v\n", args)
},
}

rootCmd.AddCommand(subCmd)

rootCmd.SetArgs([]string{""})
rootCmd.Execute()
fmt.Println()
rootCmd.SetArgs([]string{"sub", "arg1", "arg2"})
rootCmd.Execute()
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
Inside rootCmd PersistentPreRun with args: []
Inside rootCmd PreRun with args: []
Inside rootCmd Run with args: []
Inside rootCmd PostRun with args: []
Inside rootCmd PersistentPostRun with args: []

Inside rootCmd PersistentPreRun with args: [arg1 arg2]
Inside subCmd PreRun with args: [arg1 arg2]
Inside subCmd Run with args: [arg1 arg2]
Inside subCmd PostRun with args: [arg1 arg2]
Inside subCmd PersistentPostRun with args: [arg1 arg2]

“unknown command” 时的提示

"unknown command" 错误发生时,Cobra 会自动打印提示。这和 git 命令的行为一致。比如

1
2
3
4
5
6
7
8
$ hugo srever
Error: unknown command "srever" for "hugo"

Did you mean this?
server

Run 'hugo --help' for usage.

系统会根据注册的每个子命令自动生成建议,并使用萊文斯坦距離的实现。每个匹配最小距离 2(忽略大小写)的注册命令都将显示为建议。

如果需要禁用建议或在命令中调整字符串距离,请使用:

1
2
cmd.DisableSuggestions = true

1
2
cmd.SuggestionsMinimumDistance = 1

您还可以使用 SuggestFor 属性显式为给定命令设置建议的名称。这样就可以针对不是距离很近的字符串提出建议,但是对于您的命令集和不希望使用别名的命令来说,它们都是有意义的。比如:

1
2
3
4
5
6
7
$ kubectl remove
Error: unknown command "remove" for "kubectl"

Did you mean this?
delete

Run 'kubectl help' for usage.