项目地址: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文件,并编译后试试效果:
![](https://img2020.cnblogs.com/blog/1075888/202201/1075888-20220104163610859-730530077.png)
可以看到基础的-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使用更佳,当然单独其中一个使用也很好。