zhangguanzhang's Blog

golang cobra的一些笔记

字数统计: 4.1k阅读时长: 20 min
2019/06/02

这几天稍微写了下cobra,网上搜到的博客分为两类,一类是把官方的readme翻译一下完事的,一类是写的都太简单了没有实际常见场景的。这里不废话了,先用官方demo讲解下

Cobra介绍

是一个golang的库,其提供简单的接口来创建强大现代的CLI接口,类似于git或者go工具。同时,它也是一个应用,用来生成个人应用框架,从而开发以Cobra为基础的应用。热门的docker和k8s源码中都使用了Cobra
Cobra 结构由三部分组成:命令( Command )、标志( Flag )、参数( Args )。

1
2
3
4
5
6
7
type Command struct {
Use string // The one-line usage message.
Short string // The short description shown in the 'help' output.
Long string // The long message shown in the 'help<this-command>' output.
Run func(cmd *Command, args []string) // Run runs the command.
...
}

传统Linux和unix的话命令规范为情况为下面几种

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
# 单独命令,例如date
date

# 带选项的命令
ls -l

# 选项有值
last -n 3

# 短选项合起来写,注意合起来写的时候最后一个选项以外的选项都必须是无法带值的,例如last -n 3 -R只能合起来写成下面的
last -Rn 3

# 无值的长选项
rm --force

# 带值的长选项
last --num 3
last --num=3
find -type f

# 值能追加的命令
command --host ip1 --host ip2 #命令内部能完整读取所有host做处理

# 带args的命令
rm file1 file2
cat -n file1 file2

# 多级命令
ip addr show
ip addr delete xxx

# 所有情况的命令
cmd sub_cmd1 subcmd2 --host 10.0.0.2 -nL3 -d ':' --username=admin '^a' '^b'

而cobra是针对长短选项和多级命令都支持的库,单独或者混合都是支持的,不过大多数还是用来写多级命令的cli tool用的
命令的格式为下列

1
rootCommand subcommand1 subcommand2 -X value --XXXX value -Y a -Y b --ZZ c --ZZ d args1  args2

前三个是不同场景下的说明,最后一个是要执行的函数

安装与导入

如果拉取不下来用go module

1
2
export GO111MODULE=on
export GOPROXY=https://goproxy.cn

安装

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

cobra生成器

安装后会创建一个可执行文件cobra位于$GOPATH/bin目录中,自行配制好GOPATH
可以使用它来生成大体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@k8s-m1 guanzhang]# cd $GOPATH
[root@k8s-m1 go]# cd src
[root@k8s-m1 src]# ll
total 12
drwxr-xr-x 4 root root 4096 Jun 3 14:03 guanzhang
drwxr-xr-x 3 root root 4096 May 29 13:18 spyder
drwxr-xr-x 2 root root 4096 May 22 11:56 test
[root@k8s-m1 src]# cobra init test/cli
Your Cobra application is ready at
/root/go/src/test/cli

Give it a try by going there and running `go run main.go`.
Add commands to it by running `cobra add [cmdname]`.
[root@k8s-m1 src]# cd test/cli
[root@k8s-m1 cli]# ll
total 20
drwxr-xr-x 2 root root 4096 Jun 3 16:26 cmd
-rw-r--r-- 1 root root 11358 Jun 3 16:26 LICENSE
-rw-r--r-- 1 root root 674 Jun 3 16:26 main.go

默认情况下,Cobra将添加Apache许可证。如果您不想这样,可以将标志添加-l none到所有生成器命令。但是,它会在每个文件(// Copyright © 2018 NAME HERE )的顶部添加版权声明。如果您通过选项,-a YOUR NAME则索赔将包含您的姓名。这些标志是可选的。

进入目录并运行demo

1
2
3
4
5
6

[root@k8s-m1 cli]# go mod init
go: creating new go.mod: module test/cli
[root@k8s-m1 cli]# go mod why
# test/cli
test/cli

在Cobra应用程序中,通常main.go是暴露的文件。它有一个目的:初始化Cobra,它只是调用executecmd包的功能

1
2
3
4
5
6
7
8
9
10
[root@k8s-m1 app]# cat main.go 
// license 信息注释

package main

import "test/cli/cmd"

func main() {
cmd.Execute()
}

查看cmd/root.go发现命令的长短帮助文字,字面看的话说使用app运行,然后给app命令添加长短的帮助说明文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "cli",
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)
}
}

运行查看

1
2
3
4
5
6
7
[root@k8s-m1 app]# go run main.go
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.

当前命令为cli没有子命令,无法观察出它的强大

添加子命令达到想要的层级关系

1
2
cli
|----app

使用cobra生成器添加一个上面二级的app命令

1
2
3
4
5
6
[root@k8s-m1 cli]# cobra add app
app created at /root/go/src/test/cli/cmd/app.go
[root@k8s-m1 cli]# ll cmd/
total 8
-rw-r--r-- 1 root root 1611 Jun 3 16:29 app.go
-rw-r--r-- 1 root root 2776 Jun 3 16:26 root.go

再来run一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@k8s-m1 cli]# go run main.go 
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.

Usage:
cli [command]

Available Commands:
app A brief description of your command
help Help about any command

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

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

发现没有子命令的时候会打印可用的二级命令,里面有我们添加的app命令,来run下app命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@k8s-m1 cli]# go run main.go help app
A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.

Usage:
cli app [flags]

Flags:
-h, --help help for app

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

我们可以看到cmd/app.go里有这段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// appCmd represents the app command
var appCmd = &cobra.Command{
Use: "app",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("app called")
},
}

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

rootCmd为我们init的root.go定义的结构体,rootCmd.AddCommand(appCmd)这里字面意思可以得知command这个结构体生成对应的命令格式,可以用上一层次的命令方法AddCommand添加一个下一级别的命令
这里我们测试下如下结构

1
2
3
cli
|----app
|----remove

按照生成器生成的代码会是下面的结构,所以生成后我们需要修改remove.go里的代码

1
2
3
cli
|----app
|----remove
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
[root@k8s-m1 cli]# cobra add remove
remove created at /root/go/src/test/cli/cmd/remove.go
[root@k8s-m1 cli]# grep AddCommand cmd/remove.go
rootCmd.AddCommand(removeCmd)
[root@k8s-m1 cli]# sed -i '/rootCmd/s#rootCmd#appCmd#' cmd/remove.go
[root@k8s-m1 cli]# grep AddCommand cmd/remove.go
appCmd.AddCommand(removeCmd)
[root@k8s-m1 cli]# go run main.go app
app called
[root@k8s-m1 cli]# go run main.go app remove
remove called
[root@k8s-m1 cli]# go run main.go app help
app called
[root@k8s-m1 cli]# go run main.go app --help
A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.

Usage:
cli app [flags]
cli app [command]

Available Commands:
remove A brief description of your command

Flags:
-h, --help help for app

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

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

上面并没有达到我们预期的输出,我们期望是go run main.go app的时候输出最后的--help这样的命令帮助提醒用户。这样有两种实现方法,一种是把var appCmd = &cobra.Command的时候Run删掉,或者像下面改成RunE:

1
2
3
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("Provide item to the app command")
},

也可以改成

1
2
3
4
5
6
7
Run: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
cmd.Help()
return
}
your_need_to_run_func() //这里一般是分包写,另一个包专门接收参数去处理,cmd包专注命令和选项
},

然后再运行看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@k8s-m1 cli]# go run main.go app  
Error: Provide item to the app command
Usage:
cli app [flags]
cli app [command]

Available Commands:
remove A brief description of your command

Flags:
-h, --help help for app

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

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

Provide item to the app command

选项(Flag)

添加选项及其相关

实际命令都有选项,分为持久和本地,持久例如kubectl-n可以用在很多二级命令下,本地命令选项则不会被继承到子命令。我们给remove添加一个移除指定名字的选项,修改cmd/remove.go的init函数:

1
2
3
4
func init() {
appCmd.AddCommand(removeCmd)
removeCmd.Flags().StringP("name", "n", "", "The application to be executed")
}

为了表示出来,我们得在removeCmd的Run里写逻辑获取选项的参数

1
2
3
4
5
6
7
Run: func(cmd *cobra.Command, args []string) {
name, _:= cmd.Flags().GetString("name")
if name == "" {
name = "default"
}
fmt.Println("remove the "+name)
},

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@k8s-m1 cli]# go run main.go app remove -n test
remove the test
[root@k8s-m1 cli]# go run main.go app remove
remove the default
[root@k8s-m1 cli]# go run main.go app remove --help
A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.

Usage:
cli app remove [flags]

Flags:
-h, --help help for remove
-n, --name string The application to be executed

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

添加选项参数都是在init函数里使用cmd.Flags()或者cmd.PersistentFlags()的方法,具体有以下使用规律

  • type typeP typeVar typeVarP

带P的相对没带P的多了个短选项,没带P的选项只能用--long-iotion这样。单独的短选项官方提了issue确定了官方从不打算单独的短选项。获取选项的值用cmd.Flags().GetString("name")
不带P下纯type例如.String("name", "","The application to be executed")就是单独的长选项,最后俩参数是默认值和打印输出帮助时候后面的描述字符。
不带Var的获取值使用GetType("optionName"),这样似乎非常麻烦,实际中都是用后面俩种Var直接传入地址自动注入的,例如

1
2
var dates int32
cmd.Flags().Int32VarP(&dates,"date", "d", 1234, "this is var test")
  • type也有SliceCountDuration,IP,IPMask,IPNet之类的类型,Slice类型可以多个传入,直接获取就是一个切片,例如–master ip1 –master ip2
  • 类似--force这样的开关型选项,实际上用Bool类型即可,默认值设置为false,单独给选项不带值就是true,也可以手动传入false或者true
  • MarkDeprecated告诉用户放弃这个标注位,应该使用新标志位,MarkShorthandDeprecated是只放弃短的,长标志位依然可用。MarkHidden隐藏标志位
  • MarkFlagRequired(“region”)表示region是必须的选项,不设置下选项都是可选的

自定义类型的 Set

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 main

import (
"fmt"
"strconv"
"strings"

flag "github.com/spf13/pflag"
)

func main() {

foo := NodeResourceUtilizationThresholds{
Thresholds: make(ResourceThresholds),
}

flag.Var(&foo.Thresholds, "test", "")
flag.Parse()
fmt.Printf("%v\n", foo)
}



type ResourceName string

const (
ResourceCPU ResourceName = "cpu"
ResourceMemory ResourceName = "memory"
)

type Percentage float64
type ResourceThresholds map[ResourceName]Percentage
func (a *ResourceThresholds) Set(value string) error {
var mapStore map[ResourceName]Percentage
if *a == nil {
mapStore = make(map[ResourceName]Percentage)
} else {
mapStore = *a
}

kvArrs := strings.Split(value, ",")
for _, kvStr := range kvArrs {
kvArr := strings.Split(kvStr, "=")
if len(kvArr) != 2 {
return fmt.Errorf("invalid flag value")
}
percent, err := strconv.ParseFloat(kvArr[1], 64)
if err != nil {
return err
}
v := Percentage(percent)
switch kvArr[0] {
case "cpu":
mapStore[ResourceCPU] = v
case "memory":
mapStore[ResourceMemory] = v
default:
return fmt.Errorf("only cpu, memory, or pods thresholds can be specified")
}
}
*a = mapStore

return nil
}
func (a *ResourceThresholds) String() string {
return ""
}
func (a *ResourceThresholds) Type() string {
return ""
}


type NodeResourceUtilizationThresholds struct {
Thresholds ResourceThresholds
TargetThresholds ResourceThresholds
}

运行:

1
2
3
$ ./main.exe --test cpu=1 --test memory=0.2
{map[cpu:1 memory:0.2] map[]}

读取配置文件

类似kubectl~/.kube/configgcloud这些 cli 都会读取一些配置信息,也可以从命令行指定信息。细心观察的话可以看到这个是一直存在在命令帮助上的

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

cmd/root.go里看到viper包的几个方法就是干这个的,viper是cobra集成的配置文件读取的库
可以通过环境变量读取

1
removeCmd.Flags().StringP("name", "n", viper.GetString("ENVNAME"), "The application to be executed")

默认可以在 cmd/root.go 文件里看到默认配置文件是家目录下的.应用名,这里我是$HOME/.cli.yaml,创建并添加下面内容

1
2
name: "Billy"
greeting: "Howdy"

Command的Run里提取字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Run: func(cmd *cobra.Command, args []string) {
greeting := "Hello"
name, _ := cmd.Flags().GetString("name")
if name == "" {
name = "World"
}
if viper.GetString("name")!=""{
name = viper.GetString("name")
}
if viper.GetString("greeting")!=""{
greeting = viper.GetString("greeting")
}
fmt.Println(greeting + " " + name)
},

也可以bind到变量里

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"))
}
不想使用的话相关可以注释掉viper相关的,编译出来的程序能小几M

Command的常见字段

别名(Aliases)

现在我们想添加一个别名

1
2
3
cli
|----app
|----remove|rm

我们修改下初始化值即可

1
2
3
var removeCmd = &cobra.Command{
Use: "remove",
Aliases: []string{"rm"},

命令帮助添加示例(Example)

我们修改下remove的Run为下面

1
2
3
4
5
6
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
cmd.Help()
return
}
},

运行输出里example是空的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@k8s-m1 cli]# go run main.go app remove 
A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.

Usage:
cli app remove [flags]

Aliases:
remove, rm

Flags:
-h, --help help for remove
-n, --name string The application to be executed

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

添加example

1
2
3
4
5
6
7
var removeCmd = &cobra.Command{
Use: "remove",
Aliases: []string{"rm"},
Example: `
cli remove -n test
cli remove --name test
`,
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
go run main.go app remove 
A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.

Usage:
cli app remove [flags]

Aliases:
remove, rm

Examples:

cli remove -n test
cli remove --name test


Flags:
-h, --help help for remove
-n, --name string The application to be executed

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

参数验证器(Args)

该字段接收类型为type PositionalArgs func(cmd *Command, args []string) error
内置的为下面几个:

  • NoArgs: 如果存在任何位置参数,该命令将报告错误。
  • ArbitraryArgs: 该命令将接受任何args。
  • OnlyValidArgs: 如果存在任何不在ValidArgs字段中的位置参数,该命令将报告错误Command。
  • MinimumNArgs(int): 如果没有至少N个位置参数,该命令将报告错误。
  • MaximumNArgs(int): 如果有多于N个位置参数,该命令将报告错误。
  • ExactArgs(int): 如果没有确切的N位置参数,该命令将报告错误。
  • RangeArgs(min, max): 如果args的数量不在预期args的最小和最大数量之间,则该命令将报告错误。
  • 自己写的话传入符合类型定义的函数即可
    1
    2
    3
    4
    5
    6
    7
    8
    9
      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])
    },
    前面说的没传递选项和任何值希望打印命令帮助也可以用MinimumNArgs(1)来触发

    Run的hook

    Run功能的执行先后顺序如下:
  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun
    接收func(cmd *Command, args []string)类型的函数,Persistent的能被下面的子命令继承
    RunE功能的执行先后顺序如下:
  • PersistentPreRunE
  • PreRunE
  • RunE
  • PostRunE
  • PersistentPostRunE

接收func(cmd *Command, args []string) error的函数

自定义help,usage输出

CATALOG
  1. 1. Cobra介绍
    1. 1.1. 安装与导入
    2. 1.2. cobra生成器
    3. 1.3. 进入目录并运行demo
    4. 1.4. 添加子命令达到想要的层级关系
  2. 2. 选项(Flag)
    1. 2.1. 添加选项及其相关
    2. 2.2. 自定义类型的 Set
    3. 2.3. 读取配置文件
  3. 3. Command的常见字段
    1. 3.1. 别名(Aliases)
    2. 3.2. 命令帮助添加示例(Example)
    3. 3.3. 参数验证器(Args)
    4. 3.4. Run的hook
    5. 3.5. 自定义help,usage输出