golang-cobra 使用教程

Install

go install github.com/spf13/cobra-cli@latest

配合 viper 的简单示例

cd 到项目目录

执行 go mod init <projectName>

执行 cobra-cli init --viper 初始化项目,并引入 viper

这时项目结构如下

1
2
3
4
5
6
7
.
├── cmd
│   └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go

编辑 main.go

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

import (
"errors"
"log"
"<projectName>/cmd" // <projectName> 替换成项目名

"github.com/spf13/viper"
)

func main() {
viper.SetConfigName("server") // 配置文件名称,不要写扩展名,不然找不到文件。看样子又是个bug v1.11.0
viper.SetConfigType("yaml") // 配置文件类型(扩展名)。必须写,不然找不到文件
viper.AddConfigPath(".") // 放置配置文件的目录,可以添加多个
err := viper.ReadInConfig()
if err != nil {
if !errors.As(err, new(viper.ConfigFileNotFoundError)) { // 配置文件没找到时使用默认配置或命令行参数
log.Fatal(err)
}
}
cmd.Execute()
}

执行 cobra-cli add genConf,添加一个 genConf 命令。用来生成配置文件。这会在 cmd 下创建一个 genConf.go 文件

编辑 cmd/genConf.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
package cmd

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

const ConfigFileName = "client.yaml" // 配置文件名,我没找到从 viper 获取配置文件名的方法

var forcedFileOverwrite = false // 用来存放变量值

var genConfCmd = &cobra.Command{
Use: "genConf",
Short: "Create example configuration yaml", // --help 输出的帮助
Run: func(cmd *cobra.Command, args []string) { // 这里是命令的处理逻辑
if forcedFileOverwrite {
viper.WriteConfigAs(ConfigFileName) // 写到指定位置,创建文件,覆盖文件
} else {
viper.SafeWriteConfigAs(ConfigFileName) // 写到指定位置,创建文件,不会覆盖文件
}
},
}

func init() {
rootCmd.AddCommand(genConfCmd)
genConfCmd.Flags().BoolVarP(&forcedFileOverwrite, "forced", "f", false, "forced file overwrite") // 添加 -f 参数
}

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

import (
"os"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var rootCmd = &cobra.Command{
Use: "client <task>",
Short: "this is a task client, select your task type and run.",
Run: func(cmd *cobra.Command, args []string) {
println(viper.GetString("server.host"), viper.GetString("server.port"), viper.GetInt("concurrentNum"))
},
}

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

func init() {
// StringP 会返回参数值,配合 viper 时要用 viper 处理后的参数。这里不接收。如果不用 viper 可以使用 StringVarP。这个方法接受一个指针并赋值。
rootCmd.Flags().StringP("host", "H", "127.0.0.1", "connecting to <host>") // 参数名 "host",缩写名 "-H"。默认值
viper.SetDefault("server.host", "127.0.0.1") // 设置配置文件的默认值,写配置文件会用到
viper.BindPFlag("server.host", rootCmd.Flags().Lookup("host")) // 关联配置文件某个参数到参数

rootCmd.Flags().StringP("port", "p", "8000", "server port to listen commit to")
viper.SetDefault("server.port", "8000")
viper.BindPFlag("server.port", rootCmd.Flags().Lookup("port"))

rootCmd.Flags().IntP("concurrentNum", "c", 1, "concurrent number")
viper.SetDefault("concurrentNum", 1)
viper.BindPFlag("concurrentNum", rootCmd.Flags().Lookup("concurrentNum"))
}

完成

执行 go run main.go -h 查看帮助

执行 go run main.go genConf 生成配置文件

cobra-cli

cd 到项目目录

执行 cobra-cli init --viper 初始化项目并引入viper。注意cobra-cli会覆盖 main.go 文件,并创建 cmd 目录 和 cmd/root.go。初始结构如下:

1
2
3
4
5
6
7
.
├── cmd
│   └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go

cobra-cli add <arg> 增加一个命令,这会在cmd 目录下创建一个对应的 .go 文件,并生成默认定义。

cobra-cli add <arg> -p <parentArg>Cmd 增加一个命令,并指派到一个父命令下。cobra-cli 会给每个参数加上 Cmd 作为变量名结尾,这里指定父命令的时候也要。

cobra.Command

初始定义

Cobra 会创建类似如下的基础文件。添加新的参数也可以手动添加文件。接下来讲如何给cobra.Command添加更多设定。

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)
}
}

使用

cobra 有两个维度:命令(commands)和参数(flags)。增加命令用 cobra-cli add或者手动添加cobra.Command。下文中称为<localCmd>。增加参数编辑命令对应的文件,使用rootCmd.Flags() rootCmd.PersistentFlags() 添加。参数会出现在命令的 --help 里。下文中称为<flagName>

获取参数值

cobra.Command 对象有两种获取参数的方法

  • Flags().<type>() 返回一个对应<type> 的指针,e.g. rootCmd.Flags().Bool("forced", false, "forced file overwrite")。这种方法会再内部声明对应类型的数据结构,返回它的指针。
  • Flags().<type>Var() 接收一个参数的指针并赋值,e.g. rootCmd.Flags().BoolVar(&forcedFileOverwrite, "forced", false, "forced file overwrite")

两种方法后加PFlags().<type>VarP()) 会增加端名参数,e.g. rootCmd.Flags().BoolVarP(&forcedFileOverwrite, "forced", "f", false, "forced file overwrite") 这会增加 -f 参数

添加参数

参数分了两种:持久性(Persistent Flags)和本地(Local Flags)。持久性是在所有命令都可用的;本地参数只在所定义的命令下可用。

持久性参数

rootCmdinit 函数中调用 rootCmd.PersistentFlags() 的子方法添加,参数统一为:要赋值的参数地址,全名,短名(<shortFlags>),默认值,备注(p *string, name string, shorthand string, value string, usage string)

1
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

本地参数

在关联的命令文件中的 init 函数中调用 <localCmd>.Flags() 的子方法添加,参数统一为:要赋值的参数地址,全名,短名(-shorthand ),默认值,备注(p *string, name string, shorthand string, value string, usage string)

1
serverCmd.Flags().StringVarP(&Bind, "bind", "b", "0.0.0.0", "bind port")

位置和变长参数

设置 CommandArgs 处理指定位置参数的验证。位置参数是为预定义的命令- 开头外的所有参数。

Args 定义:func(cmd *cobra.Command, args []string) error cmd 为定义所在的Command 对象。

1
2
3
4
5
6
7
8
9
var cmd = &cobra.Command{
Short: "hello",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires a color argument")
}
return nil
},
}

cobra 有一些预定义的验证器

  • NoArgs 有任何未定义的参数就报错
  • ArbitraryArgs 接受所有位置参数。-开头的不在flag此列
  • OnlyValidArgs 如果参数不在ValidArgs中会报错。ValidArgs 是所有shell 传进来的non-flag参数??这个看样子没实现(v1.4),我在源码里没找到任何赋值(要不要这么不靠谱)
  • MinimumNArgs(int) 最少位置参数数量
  • MaximumNArgs(int) 最大位置参数数量
  • ExactArgs(int) 确切数量
  • ExactValidArgs(int)确切数量排除ValidArgs
  • RangeArgs(min, max) 数量范围
  • MatchAll(pargs ...PositionalArgs) 结合多个验证器

自定义 help

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

定义 usage

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

定义 Version

如果 rootCmd 定义了 --version,可以用cmd.SetVersionTemplate(s string)设置版本信息。

但是没有用,请参考hugo添加 version 命令,在RunE 中处理和输出版本信息

运行时钩子

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

错误参数建议

参数是用 Levenshtein distance 实现的,默认值为2

禁用建议:

1
2
3
command.DisableSuggestions = true
or
command.SuggestionsMinimumDistance = 1

不过这个东西在我这里根本不能用

配合 viper

在 init 中调用 viper.BindPFlag("<configFlag>", serverCmd.Flags().Lookup("<flagName>"))

serverCmd.Flags().Lookup 如果参数中有<flagName>,则返回对应的Flag 数据结构,否则返回nil。viper.BindPFlag 会覆盖配置中的<configFlag>。注意这里的<flagName>rootCmd.PersistentFlags()<localCmd>.Flags()name,第二个参数 (注意大小写,参数名不对的话没有Warning的)

推荐的配合方式:编辑对应的 cmd 文件,在init()中添加

1
2
3
rootCmd.Flags().StringVarP(&bind, "host", "h", "127.0.0.1", "connecting to <host>")
viper.SetDefault("host", "127.0.0.1") // 这里是增加默认配置,WriteConfig 之类的方法会用到
viper.BindPFlag("host", rootCmd.Flags().Lookup("host")) // 关联对应的参数值

参数相关选项

让父命令的参数在子命令中可用

默认情况下参数只在本地命令可用(不存在的参数会报错),想要父命令的参数在子命令中可用,实例化cobra.Command时添加TraverseChildren: true,

1
2
3
4
command := cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
TraverseChildren: true,
}

必要参数

对于 持久性(Persistent Flags)参数:

<localCmd>.MarkPersistentFlagRequired(<flagName>)

对于 本地(Local Flags)参数:

<localCmd>.MarkFlagRequired(<flagName>)

这样会报错 Error: required flag(s) "<flagName>" not set

注意参数名不对的话不会抛出warning

参数组

这两个参数将在 v1.5 提供https://github.com/spf13/cobra/issues/1671 (本文时版本号:v1.4.0)

如果有两个参数必须提供,比如 指定--username 时必须同时指定--possword

rootCmd.MarkFlagsRequiredTogether("username", "password")

或者两个互斥的参数,比如--jsonor--yaml

rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")

这个方法可以处理 持久性和本地参数,参数可以是任意多个。同一个参数可以出现在不同组。

注意 参数组只在参数被定义的命令中生效

错误处理

如果想返回错误给调用者,可以用RunE

1
2
3
4
5
6
RunE: func(cmd *cobra.Command, args []string) error {
if err := someFunc(); err != nil {
return err
}
return nil
},

err 非空时,会输出err的String(),并输出help

本作品采用 知识共享许可协议 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。