0%

【Awesome Go】Cobra

Cobra是一个为创建命令行程序提供了强大接口的Go Library,同时也是一个用来生成应用框架的应用程序。在Go的世界中,很多 知名项目 比如Docker、Kubernetes、Hugo等都使用了Cobra。本文将介绍Cobra包的基本使用,并介绍其实现原理,本文中使用的代码可以参考我的 github

Cobra包提供了以下特性:

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

Concepts

cobra 中有个重要的概念,分别是 commands、arguments 和 flags。其中 commands 代表行为,arguments 就是命令行参数(或者称为位置参数),flags 代表对行为的改变(也就是我们常说的命令行选项)。执行命令行程序时的一般格式为:

1
APPNAME COMMAND ARG --FLAG

比如下面的例子:

1
2
3
4
5
# server是 commands,port 是 flag
hugo server --port=1313

# clone 是 commands,URL 是 arguments,brae 是 flag
git clone URL --bare

如果是一个简单的程序(功能单一的程序),使用 commands 的方式可能会很啰嗦,但是像 git、docker 等应用,把这些本就很复杂的功能划分为子命令的形式,会方便使用(对程序的设计者来说又何尝不是如此)。

Cobra支持像 Go flag 一样的兼容POSIX的命令行参数,通过 pflag library 支持。

Getting Started

创建 cobra 应用

在创建 cobra 应用前需要先安装 cobra 包:

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

然后就可以使用 cobra 生成应用程序框架

1
2
3
4
╭─ ~/go/src/github.com/SimpCosm/cobrademo $
╰─ cobra init cobrademo --pkg-name github.com/SimpCosm/cobrademo
Your Cobra application is ready at
/Users/houmin/go/src/github.com/SimpCosm/cobrademo

生成目录如下:

1
2
3
4
5
6
7
8
9
╭─ ~/go/src/github.com/SimpCosm/cobrademo $
╰─ tree
.
├── LICENSE
├── cmd
│   └── root.go
└── main.go

1 directory, 3 files

查看生成代码:

main.go
1
2
3
4
5
6
7
package main

import "github.com/SimpCosm/cobrademo/cmd"

func main() {
cmd.Execute()
}

可以看到 main.go 非常简单,主要代码逻辑在 cmd/root.go

cmd/root.go
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
75
76
package cmd

import (
"fmt"
"github.com/spf13/cobra"
"os"

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

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "cobrademo",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func init() {
cobra.OnInitialize(initConfig)

// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobrademo.yaml)")

// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
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 ".cobrademo" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobrademo")
}

viper.AutomaticEnv() // read in environment variables that match

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}

编译程序并运行,可以看到:

1
2
3
4
5
6
7
$ ./main                                                                                
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

下面依次分析创建命令的每个部分。

创建 rootCmd

Cobra 不需要额外的创建特殊的构造函数,只需要简单的创建你自己的命令。

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 http://hugo.spf13.com`,
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 函数中定义命令行参数并处理配置文件:

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
package cmd

import (
"fmt"
"os"

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

var (
// Used for flags.
cfgFile string
userLicense string

rootCmd = &cobra.Command{
Use: "cobra",
Short: "A generator for Cobra based Applications",
Long: `Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
}
)

// Execute executes the root command.
func Execute() error {
return rootCmd.Execute()
}

func init() {
cobra.OnInitialize(initConfig)

rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution")
rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project")
rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
viper.SetDefault("license", "apache")

rootCmd.AddCommand(addCmd)
rootCmd.AddCommand(initCmd)
}

func er(msg interface{}) {
fmt.Println("Error:", msg)
os.Exit(1)
}

func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
er(err)
}

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

viper.AutomaticEnv()

if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}

自动生成子命令

除了生成应用程序框架,还可以通过 cobra add 命令生成子命令的代码文件,比如下面的命令会添加子命令 image 和 相关的代码文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
╭─ ~/go/src/github.com/SimpCosm/cobrademo $                                   
╰─ cobra add image
image created at /Users/houmin/go/src/github.com/SimpCosm/cobrademo

╭─ ~/go/src/github.com/SimpCosm/cobrademo $
╰─ tree
.
├── LICENSE
├── cmd
│   ├── image.go
│   └── root.go
└── main.go

1 directory, 4 files

创建自定义命令

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

1
2
3
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("cobra demo program created by houmin.")
},

编译之后并不带参数运行,这次就不再输出帮助信息了,而是执行了 rootCmd 的 Run 方法:

1
2
$ ./main                                  
cobra demo program created by houmin.

再创建一个 version Command 用来输出当前的软件版本。先在 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 crddemo",
Long: `All software has versions. This is crddemo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("cobrademo version is v0.1")
},
}

编译程序运行命令如下:

1
2
$ ./main version                                                                           
cobrademo version is v0.1

如果你想要获得执行命令返回的错误,可以使用 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)用来控制 Command 的具体行为。根据选项的作用范围,可以把选项分为两类:

  • persistent
  • local

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

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

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

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

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

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

创建命令行参数

首先我们来搞清楚命令行参数(arguments)与命令行选项的区别(flags/options)。以常见的 ls 命令来说,其命令行的格式为:

1
$ 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 之中,报错

比如要让 Command cmdTimes 至少有一个位置参数,可以这样初始化它:

1
2
3
4
5
6
7
var cmdTimes = &cobra.Command{
Use: …
Short: …
Long: …
Args: cobra.MinimumNArgs(1),
Run: …
}

我们在前面创建的代码的基础上,为 image 命令添加行为(打印信息到控制台),并为它添加一个子命令 cmdTimes,下面是更新后的 image.go 文件的内容:

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
package cmd

import (
"fmt"
"github.com/spf13/cobra"
"strings"
)

var echoTimes int

// imageCmd represents the image command
var imageCmd = &cobra.Command{
Use: "image [argument]",
Short: "Print images information",
Long: "Print all images information",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}

func init() {
rootCmd.AddCommand(imageCmd)

// Here you will define your flags and configuration settings.

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
imageCmd.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
}

执行命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
╭─ ~/go/src/github.com/SimpCosm/cobrademo $
╰─ ./main image
Error: requires at least 1 arg(s), only received 0
Usage:
cobrademo image [argument] [flags]

Flags:
-h, --help help for image
-t, --times int times to echo the input (default 1)

Global Flags:
--config string config file (default is $HOME/.cobrademo.yaml)

requires at least 1 arg(s), only received 0

╭─ ~/go/src/github.com/SimpCosm/cobrademo $
╰─ ./main image -t 3 houmin
Echo: houmin
Echo: houmin
Echo: houmin

帮助信息 help command

cobra 会自动添加 —help(-h)选项,所以我们可以不必添加该选项而直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
╭─ ~/go/src/github.com/SimpCosm/cobrademo $
╰─ ./main --help
long description shows how to use cobra package

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
cobrademo [flags]
cobrademo [command]

Available Commands:
help Help about any command
image Print images information
version Print the version number of crddemo

Flags:
--config string config file (default is $HOME/.cobrademo.yaml)
-h, --help help for cobrademo
-t, --toggle Help message for toggle

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

参考资料