Cobra

Introduction

Command Line Interfaces (CLI) applications are software programs that run on terminals or command prompts. The user interacts with the software by specifying commands and receiving text as feedback.

CLIs are old but very popular for their versatility, portability, and speed. CLIs are popularly used for various tasks ranging from text processing to complex systems administration tasks to save time and effort.

Package cobra is a commander providing a simple interface to create powerful modern CLI interfaces. In addition to providing an interface, Cobra simultaneously provides a controller to organize your application code.

Concept

Cobra 建立在命令、参数和标志这三个结构之上。要使用 Cobra 编写一个命令行程序,需要明确这三个概念。

  • 命令(COMMAND):命令表示要执行的操作。

  • 参数(ARG):是命令的参数,一般用来表示操作的对象。

  • 标志(FLAG):是命令的修饰,可以调整操作的行为。

一个好的命令行程序在使用时读起来像句子,用户会自然的理解并知道如何使用该程序。

要编写一个好的命令行程序,需要遵循的模式是 APPNAME VERB NOUN --ADJECTIVEAPPNAME COMMAND ARG --FLAG

在这里 VERB 代表动词,NOUN 代表名词,ADJECTIVE 代表形容词。

现实世界中好的命令行程序的例子:

1
hugo server --port=1313

这个例子中,server 是一个命令(子命令),port 是一个标志(1313 是标志的参数,但不是命令的参数 ARG)。

一个 git 命令的例子:

1
$ git clone URL --bare

这个例子中,clone 是一个命令(子命令),URL 是命令的参数,bare 是标志。

Quick start

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

import "github.com/spf13/cobra"

Add cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var rootCmd = &cobra.Command{
Use: "bobo",
Short: "Bobo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run bobo...")
},
}

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

Run 属性是一个函数,当执行命令时会调用此函数。

rootCmd.Execute() 是命令的执行入口,其内部会解析 os.Args[1:] 参数列表(默认情况下是这样,也可以通过 Command.SetArgs 方法设置参数),然后遍历命令树,为命令找到合适的匹配项和对应的标志。

1
2
3
4
5
6
7
package main

import "github.com/chinabobo/learning-cobra/cmd"

func main() {
cmd.Execute()
}

Run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ go build -o bobo

$ ./bobo
run bobo...

$ ./bobo --help
A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.

Usage:
bobo [flags]

Flags:
-h, --help help for bobo

tree

1
2
3
4
5
6
7
8
$ tree bobo
.
bobo
├── cmd
│ └── root.go
├── go.mod
├── go.sum
└── main.go

Add subcommand

与定义 rootCmd 一样,我们可以使用 cobra.Command 定义其他命令,并通过 rootCmd.AddCommand() 方法将其添加为 rootCmd 的一个子命令。

1
2
3
4
5
6
7
8
9
10
11
12
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Bobo",
Long: `All software has versions. This is Bobo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Bobo Static Site Generator v1.0 -- HEAD")
},
}

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

重新编译后

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
$ ./bobo version
Bobo Static Site Generator v1.0 -- HEAD

$ ./bobo help
A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.

Usage:
bobo [flags]
bobo [command]

Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
version Print the version number of Bobo

Flags:
-h, --help help for bobo

Use "bobo [command] --help" for more information about a command.

$ ./bobo help version
All software has versions. This is Bobo's

Usage:
bobo version [flags]

Flags:
-h, --help help for version

Flag

PersistentFlags

如果一个标志是持久的,则意味着该标志将可用于它所分配的命令以及该命令下的所有子命令。

对于全局标志,可以定义在根命令 rootCmd 上。

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

Flags

标志也可以是本地的,只适用于该指定命令。

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

TraverseChildren

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

1
2
3
4
var rootCmd = &cobra.Command{
Use: "hugo",
TraverseChildren: true,
}

MarkFlagRequired

1
2
3
var Region string
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")

定义好以上几个标志后,为了展示效果,我们对 rootCmd.Run 方法做些修改,分别打印 VerboseSourceRegion 几个变量。

1
2
3
4
5
6
7
8
9
var rootCmd = &cobra.Command{
Use: "hugo",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run hugo...")
fmt.Printf("Verbose: %v\n", Verbose)
fmt.Printf("Source: %v\n", Source)
fmt.Printf("Region: %v\n", Region)
},
}

另外,为了测试启用 Command.TraverseChildren 的效果,添加了一个 print 子命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bobo/cmd/print.go
var printCmd = &cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run print...")
fmt.Printf("printFlag: %v\n", printFlag)
fmt.Printf("Source: %v\n", Source)
},
}

func init() {
rootCmd.AddCommand(printCmd)

// 本地标志
printCmd.Flags().StringVarP(&printFlag, "flag", "f", "", "print flag for local")
}

Hooks

在执行 Run 函数前后,我么可以执行一些钩子函数,其作用和执行顺序如下:

  1. PersistentPreRun:在 PreRun 函数执行之前执行,对此命令的子命令同样生效。

  2. PreRun:在 Run 函数执行之前执行。

  3. Run:执行命令时调用的函数,用来编写命令的业务逻辑。

  4. PostRun:在 Run 函数执行之后执行。

  5. PersistentPostRun:在 PostRun 函数执行之后执行,对此命令的子命令同样生效。

修改 rootCmd 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var rootCmd = &cobra.Command{
Use: "bobo",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("bobo PersistentPreRun")
},
PreRun: func(cmd *cobra.Command, args []string) {
fmt.Println("bobo PreRun")
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run bobo...")
},
PostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("bobo PostRun")
},
PersistentPostRun: func(cmd *cobra.Command, args []string) {
fmt.Println("bobo PersistentPostRun")
},
}

重新编译并运行 bobo 命令:

1
2
3
4
5
6
7
8
9
# 编译
$ go build -o bobo
# 执行
$ ./bobo
bobo PersistentPreRun
bobo PreRun
run hugo...
bobo PostRun
bobo PersistentPostRun

Define Help cmd

如果你对 Cobra 自动生成的帮助命令不满意,我们可以自定义帮助命令或模板。

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

Define Usage Message

当用户提供无效标志或无效命令时,Cobra 通过向用户显示 Usage 来提示用户如何正确的使用命令。

例如,当用户输入无效的标志 --demo 时,将得到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ ./bobo --demo
Error: unknown flag: --demo
Usage:
bobo [flags]
bobo [command]

Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
print
version Print the version number of Hugo

Flags:
--author string Author name for copyright attribution (default "YOUR NAME")
-c, --config string config file
-h, --help help for hugo
-s, --source string Source directory to read from
-v, --verbose verbose output

Use "bobo [command] --help" for more information about a command.

unknown flag: --demo

首先程序会报错 Error: unknown flag: --demo,报错后会显示 Usage 信息。

这个输出格式默认与 help 信息一样,也可以进行自定义。Cobra 提供了如下两个方法,来控制输出。

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

Reference

https://cobra.dev/

https://github.com/spf13/cobra

https://pkg.go.dev/github.com/spf13/cobra

https://github.com/spf13/cobra-cli/blob/main/README.md

https://xie.infoq.cn/article/915006cf3760c99ad0028d895