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 代表对行为的改变(也就是我们常说的命令行选项)。执行命令行程序时的一般格式为:

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

查看生成代码:

1
2
3
4
5
6
7
package main

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

func main() {
	cmd.Execute()
}

可以看到 main.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.

参考资料