项目地址:spf13/cobra: A Commander for modern Go CLI interactions (github.com)
文档地址:cobra/user_guide.md at master · spf13/cobra (github.com)
Overview
cobra是一个用于创建命令行工具的库(框架),可以创建出类似git或者go一样的工具,进行我们平时熟悉的git clone/pull、go get/install等操作。kubernetes工具中就使用了cobra。
上述这些命令行工具的--help/-h既好看又好理解,开始时我也会好奇这些工具是如何实现多级子命令支持的?cobra的目标即是提供快速构建出此类工具的能力(cobra是框架,具体的业务逻辑当然还是要自己写)。例如可以为我们app或系统工具的bin文件添加--config来启动服务、添加--version来查看版本号等等。相比起标准库里的flag包,cobra强大了很多。
官网列出了cobra的一大堆优点:
-
易用的subcommand模式(即嵌套命令或子命令)
-
强大的flags支持(参考标准库flagSet)
-
支持global、local、cascading级别的flag设置
-
错误时的智能提示
-
自动生成的、美观的help信息,并默认支持--help和-h打印help信息
-
提供命令自动补全功能(bash, zsh, fish, powershell环境)
-
提供命令man page自动生成功能
-
有兄弟项目viper可以快捷的实现配置文件解析和flag绑定(暂不建议直接使用,viper包装了一些配置文件解析的细节,应当在熟悉cobra之后再去单独了解)
本文的核心目的为:通过手写一个示例来迅速入门cobra。
先来简单看下cobra框架中主要概念:
kubectl get pod|service [podName|serviceName] -n <namespace>
以上述kubectl get为例,cobra将kubectl称作做rootcmd(即根命令),get称做rootcmd的subcmd,pod|service则是get的subcmd,podName、serviceName是pod/service的args,-n/--namespace称作flag。同时我们还观察到-n这个flag其实写在任意一个cmd之后都会正常生效,这说明这是一个global flag,global flag对于rootcmd及所有子命令生效。
此外,当为cmd指定了一个与subcmd同名的args时,subcmd优先生效。
一、使用cobra的建议的项目目录结构
cobra文档告诉我们,使用cobra时最好遵循如下的项目目录设置(即:把命令行定义相关的部分单独写在一个名为cmd的包内):
▾ appName/
▾ cmd/
add.go
your.go
commands.go
here.go
main.go
使用cobra的项目,其程序入口main.go一般极为简洁(因为相应的业务逻辑都在cmd里调用或实现):
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
二、写一个简单地mytool工具
本文示例不会去写一个常驻的后台服务,只写一个命令行工具mytool进行演示。
我们为mytool工具预设如下功能,朝着这个目标前进:
// 打印mytool的版本号,预设为1.0-beta
mytool version
// names目录中创建name同名文件,将desc内容写入文件中,成功后打印"${name} is added"
mytool add "Donald.Trump" --desc "He knows everything"
// names目录中删除name同名文件,成功后打印"${name} is deleted"
mytool del "Donald.Trump"
// names目录中读取name同名文件中的信息,如未找到对应name那么显示"${name} not found"
mytool get "Donald.Trump"
// names目录中读取所有文件并打印相关信息
mytool get all
1. 首先,创建如下的项目结构:

然后使用go mod初始化包管理文件go.mod:go mod init mytool
2. 编写cmd/root.go文件,正如其名这是所有命令行的root:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "mytool is a tool to record names",
Long: `The best tool to record names in the world!
Just try it!!!
Complete documentation is available at http://mytool.com`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Use mytool -h or --help for help.")
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
3. 然后创建main/main.go文件,并编译后试试效果:

可以看到基础的-h功能展示了出来。接下来我们为框架补充实体逻辑。
4. 先增加一个version功能试试
// cmd目录下新增version.go文件,内容如下:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "show version of mytool",
Long: `All tools have a version, this is mytool's"`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("mytool-1.0-beta")
},
}
func initVersion() {
rootCmd.AddCommand(versionCmd)
}
我们定义了一个全新的cobra.Command:versionCmd,我们希望他成为mytool的一个subcmd,只要使用rootCmd.AddCommand(versionCmd)就可以实现了。这个操作可以在cmd包中任意地方写,但统一起见我们在每个cmd文件中写一个init函数来执行对应cmd的add操作和未来可能存在的flag绑定等操作,然后在root.go中增加initAll()函数来调用组织子命令的init函数(需要注意的是init()已经被cobra占用,所以我们自己的init函数加上各种后缀即可)。
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
var rootCmd = &cobra.Command{
Use: "mytool",
Short: "mytool is a tool to record names",
Long: `The best tool to record names in the world!
Just try it!!!
Complete documentation is available at http://mytool.com`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Use mytool -h or --help for help.")
},
}
func initAll() {
initVersion()
}
func Execute() {
initAll()
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}


可以看到关于version的help已经呈现了出来,也能正常查看mytool的version了,但实测还没有子命令自动预测补全的功能,这个在下边尝试。
5. 增加add子命令
cmd/add.go,这个子命令使用指定的name args作为参数,使用--desc作为flag
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"io/ioutil"
)
var (
name string
nameDesc string
)
var addNameCmd = &cobra.Command{
Use: "add",
Short: "mytool name add operations",
Long: `mytool add: add name info in names dir`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Println(cmd.Help())
return
}
name = args[0]
err := ioutil.WriteFile(fmt.Sprintf("./names/%s",name), []byte(nameDesc), 0644)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%s is added\n", name)
},
}
func initAdd() {
addNameCmd.Flags().StringVarP(&nameDesc, "desc", "D", "", "description of this person")
rootCmd.AddCommand(addNameCmd)
}
然后在root.go initAll()中增加initAdd()的调用:
func initAll() {
initVersion()
initAdd()
}

可以看到add子命令已经出现在help中了,且多了一个completion子命令,尝试一下可以发现,这个子命令可以用于生成各个shell环境下的自动补全脚本,我这里就略过不做尝试了。
6. 接下来照猫画虎添加delete.go和get.go文件
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
var delNameCmd = &cobra.Command{
Use: "del",
Short: "mytool name del operations",
Long: `mytool del: del name info in names dir`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Println("Only one name can be deleted in one time!")
return
}
name = args[0]
err := os.Remove(fmt.Sprintf("./names/%s",name))
if err != nil {
fmt.Printf("%s not found\n", name)
return
}
fmt.Printf("%s is deleted\n", name)
},
}
func initDelete() {
rootCmd.AddCommand(delNameCmd)
}
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"io/ioutil"
)
var getNameCmd = &cobra.Command{
Use: "get",
Short: "mytool name get operations",
Long: `mytool get: get name info in names dir`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
fmt.Println("Only one name can be get in one time! Or use get all to get all names!")
return
}
name = args[0]
nameInfo,err := ioutil.ReadFile(fmt.Sprintf("./names/%s",name))
if err != nil {
fmt.Printf("%s not found\n", name)
return
}
fmt.Println(name,":",string(nameInfo))
},
}
var getAllNamesCmd = &cobra.Command{
Use: "all",
Short: "mytool name get all operations",
Long: `mytool get all: get all names info in names dir`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
fmt.Println(getNameCmd.Help())
return
}
files, err := ioutil.ReadDir("./names")
if err != nil {
fmt.Println("Dir names not found!")
return
}
for _, file := range files {
nameInfo,_ := ioutil.ReadFile(fmt.Sprintf("./names/%s", file.Name()))
fmt.Println(file.Name(),":",string(nameInfo))
}
},
}
func initGet() {
getNameCmd.AddCommand(getAllNamesCmd)
rootCmd.AddCommand(getNameCmd)
}
至此所有cmd添加完毕,我们只需要在root.go中集成相关init函数即可。
func initAll() {
initVersion()
initAdd()
initDelete()
initGet()
}
经测试各项功能正常。
三、思考
1. 我做了什么?
在本次笔记中,根据官方文档编写了一个mytool的指令,其实现了基本的subcommand添加、flag添加、args判断,基于此初步了解了cobra的命令添加体系和组织逻辑。
2. 有哪些地方需要注意的 以及 我还能做些什么?
实际项目中的情况会比示例复杂的多,但只要清楚其脉络就可以融会贯通。
- 如何设置无需指定参数的flag:通过指定包含默认值的bool类型的flag来实现无参flag设置,例如如何将本例中的get all改为无参flag get --all
- args的自动校验:本例通过简单的len(args)进行参数校验,cobra提供了便捷的Command内置字段Args来支持args校验,其值可以是cobra内置args校验函数,如NoArgs,MinimumNArgs(int),MaximumNArgs(int),ExactArgs(int)等等,也可以自定义匿名函数进行args的提前校验
- 如何强制指定flag:rootCmd.MarkFlagRequired(<flagName>)、rootCmd.MarkPersistentFlagRequired(<flagName>)
- cobra.Command的Run函数返回error:默认的Run函数不会返回error,所以实际上本例中的Execute()没有遇到异常panic时都会返回nil。如果想要捕捉各个cmd的运行时的业务error,可以使用RunE参数来代替Run参数,RunE支持error返回
- help和usage的自定义:一般不需要
- preRun和PostRun钩子(hooks):cobra提供了preRun和PostRun的hooks,参考webhooks理解即可,他们提供了在运行Run函数之前/之后执行其他更多操作的能力,这部分的使用很简单,只需要为Command对象的几个字段赋值相应的匿名函数即可。
-
自动补全和man page生成功能测试:rootcmd completion生成脚本还需实测,关于man page生成则可参考:cobra/README.md at master · spf13/cobra (github.com)
cobra作者spf13还有一个viper项目,此项目提供便捷的针对json,yaml,toml,ini,hcl等类型配置文件的解析,配合cobra使用更佳,当然单独其中一个使用也很好。