0%

go 库学习之 cobra

很多 Go 的开源软件都使用 Cobra 库来构建自己的命令行(CLI)应用程序,这些开源软件包括 KubernetesHugo 等等。这篇文章会对 Cobra 库的使用做一个基本介绍。

Cobra 简介

Cobra 库提供了简单的接口来帮助 Gopher 创建强大的、现代化的命令行应用程序(类似于 git 命令)。Cobra 提供了以下特性:

  • 简易的基于子命令的 CLI:例如 app serverapp fetech
  • 兼容 POSIX 的 flags(短、长版本)
  • 嵌套的子命令
  • 全局、局部、级联 flags
  • 智能建议
  • 自动生成帮助
  • 子命令帮助分组
  • 自动识别 -h--help 帮助标志
  • 自动生成 shell 补全
  • 自动生成 manpages
  • 命令别名
  • 自定义帮助、usage 等等
  • viper 无缝集成

Cobra 建立在 commandsargumentsflags 这三个概念之上:

  • commands 代表动作,应用程序支持的每一种交互应该包含在一个 Command 中。Command 可以包含子命令
  • arguments 代表事物
  • flags 代表对这些动作的装饰,用来修改 command 的行为。Cobra 支持 POSIX 兼容的 flags,也支持 Go 标准 flag 库。Cobra 的 flags 功能是通过 pflag library 来实现的。pflag 库完全兼容 flag 标准库的接口,同时又增加了对 POSIX 的兼容

Cobra 认为使用命令行程序时应该像阅读句子一样自然,应该遵循 APPNAME VERB NOUN --ADJECTIVEAPPNAME COMMAND ARG --FLAG 这样的模式。

使用方法

当使用 Cobra 库来构建命令行应用程序时,同时按照如下方式组织相关代码:

1
2
3
4
5
6
7
▾ appName/
▾ cmd/
add.go
your.go
commands.go
here.go
main.go

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 库的命令行程序,无需自己为每个命令编写对应的代码:

例如想创建如下的命令行接口:

1
2
3
app serve
app config
app config create

直接按照如下方式即可生成:

1
2
3
cobra-cli add serve
cobra-cli add config
cobra-cli add create -p 'configCmd'

Cobra-CLI 只是帮助我们快速创建出 CLI 程序的框架,我们也可以手动完成这一过程。所以接下来将完整地介绍 Cobra 库的使用。

创建 rootCmd

Cobra 不需要任何特殊的构造器,而是直接创建命令即可。首先需要创建 rootCmd,这个通常定义在 app/cmd/root.go 文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 https://gohugo.io/documentation/`,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}

func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

同时我们可以在 init() 函数定义额外的 flags 以及处理配置等。

创建 main.go

为了让 main 函数执行该 root 命令,通常会按照方式调用 cmd 包中的 Execute()。如上所示, Execute() 则通常就是简单地调用 rootCmd.Execute()

1
2
3
4
5
6
7
8
9
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
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")
},
}

组织子命令

要在某个命令中增加子命令,可以通过 AddCommand() 来实现。对于一些大型应用程序而言,每个子命令可能定义在它自己的 go 包中。推荐方法是父命令使用 AddCommand() 添加其最直接的子命令。例如,考虑以下目录结构:

1
2
3
4
5
6
7
8
9
├── cmd
│ ├── root.go
│ └── sub1
│ ├── sub1.go
│ └── sub2
│ ├── leafA.go
│ ├── leafB.go
│ └── sub2.go
└── main.go

那么:

  • root.goinit 函数将 sub1.go 中定义的命令添加到 root 命令中
  • sub1.goinit 函数将 sub2.go 中定义的命令添加到 sub1 命令中
  • sub2.go 中的 init 函数将 leafA.goleafB.go 中的命令添加到 sub2 命令中

返回和处理错误

如果想给命令的调用方返回错误,可以使用 RunE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

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

var tryCmd = &cobra.Command{
Use: "try",
Short: "Try and possibly fail at something",
RunE: func(cmd *cobra.Command, args []string) error {
if err := someFunc(); err != nil {
return err
}
return nil
},
}

这样函数执行时返回的错误可以被捕获。

使用 flags

flags 可以控制命令的行为,接下来介绍如何使用 flags。

首先需要给命令增加 flags,由于 flags 的定义和使用通常在不同的位置,所以会在合适的作用域内定义相应的变量,来记录 flag 的设置:

1
2
var Verbose bool
var Source string

有两种方式来添加 flag:

  • Persistent Flags:意味这该 flag 对该命令及其所有子命令都有效
1
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
  • Local Flags:只对指定的命令生效
1
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

默认,Cobra 只会解析目标命令上的 local flags,而其 parent 命令上的 local flags 都会被忽略。通过开启 Command.TraverseChildren,Cobra 在执行目标命令前会解析每一个命令上的 local flags。

在 viper 中也可以使用 flags,详细参考 viper 文档

flag 默认都是可选的,如果想把某个 flag 设置为必须得,可以通过如下方式实现:

1
2
rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkPersistentFlagRequired("region")

Flag 分组

如果有不同的 flag,而有些 flag 必须一起提供,可以这样实现:

1
2
3
rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)")
rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)")
rootCmd.MarkFlagsRequiredTogether("username", "password")

如果某些 flag 是互斥的,可以这样设置:

1
2
3
rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")

如果一组 flag 必须使用其中的某一个,可以使用 MarkFlagsOneRequired。如果结合 MarkFlagsMutuallyExclusive 使用则表示必须只能使用某一个 flag:

1
2
3
4
rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
rootCmd.MarkFlagsOneRequired("json", "yaml")
rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")

位置和自定义参数

可以通过 CommandArgs 字段来对位置参数进行校验,Cobra 内建了以下校验器:

  • 参数个数校验:NoArgs、ArbitraryArgs、MinimumNArgs(int)、MaximumNArgs(int)、ExactArgs(int)、RangeArgs(min, max)
  • 参数内容校验:OnlyValidArgs(如果某个位置参数不在 CommandValidArgs 中,则报告错误)

MatchAll(pargs ...PositionalArgs) 可以将多个检查进行 操作。

可以自定义参数校验器,其函数原型需要满足:func(cmd *cobra.Command, args []string) error。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var cmd = &cobra.Command{
Short: "hello",
Args: func(cmd *cobra.Command, args []string) error {
// Optionally run one of the validators provided by cobra
if err := cobra.MinimumNArgs(1)(cmd, args); err != nil {
return err
}
// Run the custom validation logic
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!")
},
}

帮助命令

Cobra 会自动为应用程序(只要有子命令)添加 help 子命令,可以通过 app help 的方式运行 help 子命令。而且 help 命令也支持将其他子命令作为参数输入,以获取某个子命令的帮助信息。另外,每个命令自动都添加了 -h flag,用于获取某个的帮助信息。

1
2
3
# ./app help
# ./app help echo
# ./app echo times -h

Cobra 支持在帮助输出里对命令进行分组,通过在父命令中使用 AddGroup 可以显式定义每个分组,之后子命令可以通过 GroupID 来加入某个分组。如果是使用生成的 helpcompletion 命令,可以在 root 命令中使用 SetHelpCommandGroupIdSetCompletionCommandGroupId 来设置它们的 group id。

也可以自己提供 Help 命令,或者使用默认的 Help 命令,但是提供新的帮助信息模版:

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

Usage 信息

当用户提供了无效的 flag 或命令时,Cobra 会给用户展示 usage 消息。同样可以提供自己的 usage 函数或者模版:

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

Version 标志

如果 root 命令下有 Version 字段,那么 cobra 会自动添加一个顶级的 --version flag。当使用 --version 运行程序时,cobra 会在标准输出中输出版本信息。可以通过 cmd.SetVersionTemplate(s string) 设置自定义的版本信息模版。

错误消息前缀

当收到错误消息时,Cobra 会打印错误消息。默认的错误消息时 Error: <error contents>。可以通过 cmd.SetErrPrefix(s string) 替换默认的前缀 Error:

PreRun and PostRun Hooks

可以在运行命令的 Run 函数之前或之后运行指定的函数。PersistentPreRunPreRunRun 函数之前运行;PersistentPostRunPostRunRun 函数之后运行。Persistent*Run 函数会被子命令继承(如果它们没有定义自己的 Persistent*Run 函数)。所以函数运行的顺序如下:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

未知命令时的行为

unknown command 错误发生时,Cobra 会打印自动地建议消息:

1
2
3
4
5
6
7
# ./app ec
Error: unknown command "ec" for "app"

Did you mean this?
echo

Run 'app --help' for usage.

建议信息是基于当前已经存在的子命令以及 Levenshtein distance 自动生成的。可以通过 command.DisableSuggestions = true 关闭自动建议,或者 command.SuggestionsMinimumDistance = 1 调整生成的帮助信息。也可以通过 SuggestFor 属性来为指定的字符串设置推荐的命令。

生成帮助文档、 shell 补全、Active Help

Cobra 也支持根据子命令、flag 等信息生成文档,可以参考 docs generation documentation[https://github.com/spf13/cobra/blob/main/site/content/docgen/_index.md].

Cobra 也支持为各种 shell 生成 shell 补全文件,可以参考 Shell Completions[https://github.com/spf13/cobra/blob/main/site/content/completions/_index.md]。

Cobra 也使用 shell 的补全系统来提供 Active Help,具体参考 Active Help[https://github.com/spf13/cobra/blob/main/site/content/active_help.md]。

简单示例

最后是一个简单的实例:

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
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 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("Echo: " + strings.Join(args, " "))
},
}

var cmdTimes = &cobra.Command{
Use: "times [string to echo]",
Short: "Echo anything to the screen more times",
Long: `echo things multiple times back to the user by 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", Version: "1.0"}
rootCmd.AddCommand(cmdPrint, cmdEcho)
cmdEcho.AddCommand(cmdTimes)
rootCmd.Execute()
}