cobra也许是go语言现有最好的命令行框架了,在各大项目中皆有使用,比如最出名的kubernetes, 所以要写一个稍微复杂的命令行工具,使用cobra还是不错的,cobra内置了非常多有用的功能,包括但不限于,自动生成帮助文档, 生成命令行代码的脚手架工具, 智能提示等等。
命令行相关知识
在学习cobra之前我们应该先了解一下命令行下的参数类型,cobra将命令行参数分为了两类,一类叫做args(位置参数), 一类叫做flags(可选参数)。
以下面的命令为例
ls -l -a -h --color=auto /tmp /var/log
上面的命令可以分为三个部分, ls, -l -a -h, /tmp /var/log, 第一部分是可执行文件名, 第二部分是可选参数, 第三部分是位置参数。
如果你使用linux命令足够多,你会注意到flags(可选参数)不都是上述说明的那样,比如下面的命令
ls -l -a -h --color=auto /tmp /var/log
可以看到上面的命令多了--color=auto这样的flags。
总的来说在这两类参数中,flags(可选参数)总是以一个或多个-(横线)开头, 而命令行参数一般不以-(横线)开头。
但,事情总有意外, 比如linux的很多命令接受参数-(横线), 表示读取标准输出, 这个参数说实话,还真不好分类,个人倾向于分为位置参数。
如果你熟悉python的typer或者看过我写的typer快速入门教程的话,你会发现两者对于参数的定义是大同小异的, 虽然两者使用的术语不同。
快速入门
首先从一个超级简单的例子来了解一下cobra的使用。
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var age int
var rootCmd = &cobra.Command{
Use: "cabra1 [Name]",
Short: "cabra1 demo command",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("hello %s, your age is %d?", args[0], age)
},
}
func init() {
rootCmd.Flags().IntVarP(&age, "age", "a", 18, "how old are you?")
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func main() {
rootCmd.Execute()
}
像所有命令行框架一样,它会自动生成帮助文档,命令如下:
go run main.go --help
# 下面为帮助文档
cabra1 demo command
Usage:
cabra1 [Name] [flags]
Flags:
-a, --age int how old are you? (default 18)
-h, --help help for cabra1
如果直接运行会是以下结果。
go run main.go
# 输出如下
Error: requires at least 1 arg(s), only received 0
...省略帮助文档...
你会发现它会提示你至少需要一个args(位置参数), 而这个设置由Args: cobra.MinimumNArgs(1)这部分代码设置。
所以我们至少需要一个参数,比如。
go run main.go zhangsan
# 输出如下
hello zhangsan, your age is 18?
当然了,我们也可以设置flags(可选参数), 比如
go run main.go zhangsan -a 188
# 输出如下
hello zhangsan, your age is 188?
注意: 这个简单的例子并不是cobra的最佳实践!!!
位置参数
cobra对于位置参数的支持其实并不多,提供的校验方法基本都是检查参数的个数而不是参数类型。
默认情况下,有以下默认方法
NoArgs- 如果出现任何位置参数就报错ArbitraryArgs- 接受任意数量的位置参数OnlyValidArgs- 如果位置参数不在ValidArgs的参数列表中就报错MinimumNArgs(int)- 如果小于指定参数个数就报错MaximumNArgs(int)- 如果大于指定参数个数就报错ExactArgs(int)- 不完全等于指定参数个数就拨错ExactValidArgs(int)- 需要配合ValidArgs的参数列表使用, 在ValidArgs列表中且参数个数等于指定参数个数RangeArgs(min, max)- 位置参数的数量范围
就是简单的翻译了一下官方文档...
唯一需要注意的就是ValidArgs的配合使用, 比如。
var rootCmd = &cobra.Command{
Use: "cabra1 [Name]",
Short: "cabra1 demo command",
Args: cobra.OnlyValidArgs,
// validArgs的配置
ValidArgs: []string{"zhangsan", "lisi"},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("hello %s, your age is %d?", args[0], age)
},
}
如果位置参数不在ValidArgs列表内就会报错, 如下:
go run main.go wangwu
# 报错如下
Error: invalid argument "wangwu" for "cabra1"
除此之外我们还可以自定义位置参数的验证方法,代码如下:
var rootCmd = &cobra.Command{
Use: "cabra1 [Name]",
Short: "cabra1 demo command",
Args: func(cmd *cobra.Command, args []string) error {
for _, arg := range args {
_, err := strconv.Atoi(arg)
if err == nil {
return fmt.Errorf("仅支持字符串, 不支持使用数字[%s]作为参数", arg)
}
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("hello %s, your age is %d?", args[0], age)
},
}
使用以下命令会报错
go run main.go 123123
Error: 仅支持字符串, 不支持使用数字[123123]作为参数
可选参数
可选参数大致可以分为两个部分,非持久化可选参数, 持久化可选参数(PersistentFlags), , 这个持久化的意思是说,该参数会传递到子命令, 有点继承的意思。
非持久化参数
因为Golang是静态语言所以,每种内置类型都有一个对应的方法,比如int对应Int(), string对应String()
简单列举一下常用的类型。
var age int
var s *string
var i *int
var f *float64
var sa *[]string
func init() {
rootCmd.Flags().IntVarP(&age, "age", "a", 18, "how old are you?")
s = rootCmd.Flags().String("string", "", "specify string")
i = rootCmd.Flags().Int("int", 0, "specify int")
f = rootCmd.Flags().Float64("float64", 0.0, "specify float64")
sa = rootCmd.Flags().StringArray("stringarray", []string{}, "specify string array")
}
func main() {
rootCmd.Execute()
fmt.Println("s:", *s, "i:", *i, "f:", *f, "sa:", *sa)
}
查看帮助文档如下:
cabra1 demo command
Usage:
cabra1 [Name] [flags]
Flags:
-a, --age int how old are you? (default 18)
--float64 float specify float64
-h, --help help for cabra1
--int int specify int
--string string specify string
--stringarray stringArray specify string array
注意: --stringarray的多参数需要, --stringarray param1 --stringarray param2这样指定!!!
可选参数用多种使用形式, 以Int为例有三种额外的形式,如IntP, IntVar,IntVarP
比如:
var intp *int
var intVar int
var intVarP int
intp = rootCmd.Flags().IntP("intp", "i", 1, "intp set")
rootCmd.Flags().IntVar(&intVar, "intvar", 2, "intvar set")
rootCmd.Flags().IntVarP(&intVarP, "intvarp", "p", 3, "intvarp set")
各种形式的意义如下:
-
<Type>返回可选参数对应的类型指针, 不能设置参数缩写形式 -
<Type>P类似于前者, 不过可以设置参数的缩写形式 -
<Type>Var可传入指定类型的参数地址, 不能设置参数缩写形式 -
<Type>VarP类似于前者,不过可以设置参数的缩写形式
具体使用那种形式,根据自己的需求设置。
持久化参数
跟非持久化参数的差别主要是多了个Persistent, 比如下面的示例
var pi *int
pi = rootCmd.PersistentFlags().Int("pi", 18, "persisten int")
如果要体现持久化参数的价值,需要设置一个子命令。比如下面的例子。
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var pi *int
var childCmd = &cobra.Command{
Use: "child [Name]",
Short: "child command",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("child command called")
},
}
var rootCmd = &cobra.Command{
Use: "cabra1 [Name]",
Short: "cabra1 demo command",
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
fmt.Printf("hello %s", args[0])
} else {
fmt.Println("hello world")
}
},
}
func init() {
rootCmd.AddCommand(childCmd)
pi = rootCmd.PersistentFlags().Int("pi", 18, "persisten int")
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func main() {
rootCmd.Execute()
}
上面的例子是在rootCmd上面加了一个持久化参数,并且增加了一个子命令childCmd。
然后查看child子命令的帮助文档,会发现可以设置持久化参数--pi
$ go run demo2/main.go child --help
child command
Usage:
cabra1 child [Name] [flags]
Flags:
-h, --help help for child
Global Flags:
--pi int persisten int (default 18)
子命令
子命令在前面已经简单的提到了,总的来说子命令和一般的命令没有什么明显的区别,各命令之间的层级关系是通过cobra.Command的AddCommand方法来构造的,比如前面的rootCmd.AddCommand(childCmd)就是将childCmd变成rootCmd的子命令, 反过来也可以,不过要注意的是,需要将根节点放在执行入口。
分组
子命令中还有一个有用的功能点,即分组,如果你使用过kubectl应该不默认。
这里再介绍一下如何分组,示例代码如下
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var groups = []*cobra.Group{
{ID: "1", Title: "Basic Commands (Beginner):"},
{ID: "2", Title: "Troubleshooting and Debugging Commands:"},
}
var child1Cmd = &cobra.Command{
Use: "child1 [Name]",
Short: "child1 command",
GroupID: "2",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("child command called")
},
}
var child2Cmd = &cobra.Command{
Use: "child2 [Name]",
Short: "child2 command",
GroupID: "1",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("child command called")
},
}
var rootCmd = &cobra.Command{
Use: "cabra1 [Name]",
Short: "cabra1 demo command",
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
fmt.Printf("hello %s", args[0])
} else {
fmt.Println("hello world")
}
},
}
func init() {
rootCmd.AddGroup(groups...)
rootCmd.AddCommand(child1Cmd)
rootCmd.AddCommand(child2Cmd)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func main() {
rootCmd.Execute()
}
然后查看帮助文档
$ go run demo3/main.go --help
cabra1 demo command
Usage:
cabra1 [Name] [flags]
cabra1 [command]
Basic Commands (Beginner):
child2 child2 command
Troubleshooting and Debugging Commands:
child1 child1 command
Additional Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
Flags:
-h, --help help for cabra1
Use "cabra1 [command] --help" for more information about a command.
可以看到child1, child2两个子命令分别有了不同的分组
钩子函数
Cobra提供了许多钩子函数,比如PreRun在主命令之前执行, PostRun在主命令执行后执行
这里之所以用主命令而不是Run方法, 是因为, 还有一个RunE
下面是各个钩子函数的说明
PersistentPreRun持久化的PreRun, 即从父命令继承过来的PreRunPreRun主命令之前前Run主命令PostRun主命令执行后PersistentPostRun持久化的PostRun, 即从父命令继承过来的PostRun
脚手架工具
前面无论是单个命令还是多个命令都是在同一个文件中定义,这样自然是没问题的,但并不是Cobra推荐的最佳实践,为了让使用者可以更加方便的使用,Cobra提供了脚手架工具.
安装
可以通过下面命令安装
go install github.com/spf13/cobra-cli@latest
创建命令行
在自己的go项目中执行
如果还没有创建, 记得 go mod init <你的模块名>
cobra init
然后可以得到下面的目录接口
.
├── cmd
│ └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go
LICENSE是可以选的, 默认是空, 可选择的LICENSE列表有: GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD.
初始化完成之后就可以直接执行了
go run main.go help
# 默认只有长长的描述
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.
添加子命令
添加子命令也是比较简单的
# 添加子命令child1, child2
# cobra-cli add child1
child1 created at /root/youerning.top/go-play/cobra
# cobra-cli add child2
child2 created at /root/youerning.top/go-play/cobra
# 查看帮助文档
# go run main.go help
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:
cobratest [command]
Available Commands:
child1 A brief description of your command
child2 A brief description of your command
completion Generate the autocompletion script for the specified shell
help Help about any command
Flags:
-h, --help help for cobratest
-t, --toggle Help message for toggle
Use "cobratest [command] --help" for more information about a command.
添加子命令之后的目录结构如下:
.
├── cmd
│ ├── child1.go
│ ├── child2.go
│ └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go
总结
Cobra的设计的还是很棒的,提供了脚手架工具,提供了比较完备的命令行框架, 前者可以让使用者遵循Cobra的最佳实践,后者可以让开发更有效率并且专注于业务(好的代码框架总是这样), 其实命令行工具还有一部分是必不可少的,那就是配置管理(Cobra的最佳拍档viper,它们是同一个作者),但是这篇文章碍于篇幅就不介绍。
viper的快速入门: https://youerning.top/post/viper-tutorial
最后贴一下官方文档链接:
cobra: https://cobra.dev/
cobra-cli: https://github.com/spf13/cobra-cli/blob/main/README.md










