golang的Cobra包解析
1. Cobra 介绍
Cobra 是一个用来创建命令行的 golang 库,同时也是一个用于生成应用和命令行文件的程序。
1.1 概念
Cobra 结构由三部分组成:命令 (commands)、参数 (arguments)、标志 (flags)。基本模型如下:
APPNAME VERB NOUN –ADJECTIVE 或者 APPNAME COMMAND ARG –FLAG
如果不是太理解的话,没关系,我们先看个例子:
hugo server --port=1313 hugo:根命令 server:子命令 --port:标志
再看个带有参数的例子:
git clone URL --bare git:根命令 clone:子命令 URL:参数,即 clone 作用的对象 --bare:标志
总结下:
commands 代表行为,是应用的中心点 arguments 代表行为作用的对象 flags 是行为的修饰符
相信看了例子后,应该有个直观的认识了。接下来我们安装 Cobra。
1.2 安装
安装很简单:
go get -u github.com/spf13/cobra@latest
下载完成后安装 cobra 工具,在 $GOPATH/bin 会生成可执行文件:
go install github.com/spf13/cobra-cli@latest
将生成的 cobra 工具放到 $PATH 目录下,可以看到:
[root@localhost ~]# cp -a $GOPATH/bin/cobra /usr/local/bin [root@localhost ~]# cobra 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: cobra [command] Available Commands: add Add a command to a Cobra Application help Help about any command init Initialize a Cobra Application Flags: -a, --author string author name for copyright attribution (default "YOUR NAME") --config string config file (default is $HOME/.cobra.yaml) -h, --help help for cobra -l, --license string name of license for the project --viper use Viper for configuration (default true) Use "cobra [command] --help" for more information about a command.
接下来我们初始化一个项目。
1.3 初始化
通过 cobra init 初始化 demo 项目:
cobra init
当前项目结构为:demo ├── cmd │ └── root.go ├── LICENSE └── main.go可以看到初始化后的项目非常简单,主要是 main.go 和 root.go 文件。在编写代码之前,我们先分析下目前代码的逻辑。
1.4 代码分析
先查看下入口文件 main.go。代码逻辑很简单,就是调用 cmd 包里 Ex ecute()函数:package main import "demo/cmd" func main() { cmd.Ex ecute() }再看下 root.go 中 rootCmd 的字段:
... var rootCmd = &cobra.Command{ Use: "demo", 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) { }, } // Ex ecute 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 Ex ecute() { if err := rootCmd.Ex ecute(); err != nil { fmt.Println(err) os.Exit(1) } } ...简单说明下:
Use:命令名 Short & Long:帮助信息的文字内容 Run:运行命令的逻辑 Command 结构体中的字段当然远不止这些,受限于篇幅,这里无法全部介绍。有兴趣的童鞋可以查阅下官方文档。运行测试:
[root@localhost demo]# 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. subcommand is required exit status 1如果运行的结果和我的一致,那我们就可以进入到实践环节了。
2. Cobra 实践
铺垫了这么久,终于可以开始实践了。实践环节中,我会 提一些需求,然后我们一起实现一个简单的命令行工具。2.1 子命令
之前运行会提示 subcommand is required,是因为根命令无法直接运行。那我们就添加个子命令试试。通过 cobra add 添加子命令 create:
[root@localhost demo]# cobra add create create created at /root/go/src/demo当前项目结构为:
demo ├── cmd │ ├── create.go │ └── root.go ├── LICENSE └── main.go查看下 create.go,init() 说明了命令的层级关系:
... func init() { rootCmd.AddCommand(createCmd) }运行测试:
# 输入正确 [root@localhost demo]# go run main.go create create called # 未知命令 [root@localhost demo]# go run main.go crea Error: unknown command "crea" for "demo" Did you mean this? create Run 'demo --help' for usage. unknown command "crea" for "demo" Did you mean this? create2.2 子命令嵌套
对于功能相对复杂的 CLI,通常会通过多级子命令,即:子命令嵌套的方式进行描述,那么该如何实现呢?demo create rule首先添加子命令 rule :
[root@localhost demo]# cobra add rule rule created at /root/go/src/demo当前目录结构如下:
demo ├── cmd │ ├── create.go │ ├── root.go │ └── rule.go ├── LICENSE └── main.go目前create 和 rule 是同级的,所以需要修改 rule.go 的 init() 来改变子命令间的层级关系:
... func init() { // 修改子命令的层级关系 //rootCmd.AddCommand(ruleCmd) createCmd.AddCommand(ruleCmd) }虽然调整了命令的层级关系,但是目前运行 demo create 会打印 create called,我希望运行时可以打印帮助提示。所以我们继续完善下代码,修改 create.go:
... var createCmd = &cobra.Command{ Use: "create", Short: "create", Long: "Create Command.", Run: func(cmd *cobra.Command, args []string) { // 如果 create 命令后没有参数,则提示帮助信息 if len(args) == 0 { cmd.Help() return } }, } ...运行测试:
直接运行 create,打印帮助提示:
[root@localhost demo]# go run main.go create Create Command. Usage: demo create [flags] demo create [command] Available Commands: rule A brief description of your command Flags: -h, --help help for create Global Flags: --config string config file (default is $HOME/.demo.yaml) Use "demo create [command] --help" for more information about a command.运行 create rule,输出 rule called:
[root@localhost demo]# go run main.go create rule rule called2.3 参数
先说说参数。现在有个需求:给 CLI 加个位置参数,要求参数有且仅有一个。这个需求我们要如何实现呢?demo create rule foo实现前先说下,Command 结构体中有个 Args 的字段,接受类型为 type PositionalArgs func(cmd *Command, args []string) error
内置的验证方法如下:
NoArgs:如果有任何参数,命令行将会报错 ArbitraryArgs: 命令行将会接收任何参数 OnlyValidArgs: 如果有如何参数不属于 Command 的 ValidArgs 字段,命令行将会报错 MinimumNArgs(int): 如果参数个数少于 N 个,命令行将会报错 MaximumNArgs(int): 如果参数个数多于 N 个,命令行将会报错 ExactArgs(int): 如果参数个数不等于 N 个,命令行将会报错 RangeArgs(min, max): 如果参数个数不在 min 和 max 之间, 命令行将会报错由于需求里要求参数有且仅有一个,想想应该用哪个内置验证方法呢?相信你已经找到了 ExactArgs(int)。
改写下 rule.go:
... var ruleCmd = &cobra.Command{ Use: "rule", Short: "rule", Long: "Rule Command.", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { fmt.Printf("Create rule %s success.\n", args[0]) }, } ...运行测试:
不输入参数:
[root@localhost demo]# go run main.go create rule Error: accepts 1 arg(s), received 0输入 1 个参数:
[root@localhost demo]# go run main.go create rule foo Create rule foo success.输入 2 个参数:
[root@localhost demo]# go run main.go create rule Error: accepts 1 arg(s), received 2从测试的情况看,运行的结果符合我们的预期。如果需要对参数进行复杂的验证,还可以自定义 Args,这里就不多做赘述了。
2.4 标志
再说说标志。现在要求 CLI 不接受参数,而是通过标志 --name 对 rule 进行描述。这个又该如何实现?demo create rule --name fooCobra 中有两种标志:持久标志 ( Persistent Flags ) 和 本地标志 ( Local Flags ) 。
持久标志:指所有的 commands 都可以使用该标志。比如:--verbose ,--namespace
本地标志:指特定的 commands 才可以使用该标志。这个标志的作用是修饰和描述 rule的名字,所以选用本地标志。修改 rule.go:
package cmd import ( "fmt" "github.com/spf13/cobra" ) // 添加变量 name var name string var ruleCmd = &cobra.Command{ Use: "rule", Short: "rule", Long: "Rule Command.", Run: func(cmd *cobra.Command, args []string) { // 如果没有输入 name if len(name) == 0 { cmd.Help() return } fmt.Printf("Create rule %s success.\n", name) }, } func init() { createCmd.AddCommand(ruleCmd) // 添加本地标志 ruleCmd.Flags().StringVarP(&name, "name", "n", "", "rule name") }说明:StringVarP 用来接收类型为字符串变量的标志。相较StringVar, StringVarP 支持标志短写。以我们的 CLI 为例:在指定标志时可以用 --name,也可以使用短写 -n。
运行测试:
# 这几种写法都可以执行 [root@localhost demo]# go run main.go create rule -n foo Create rule foo success. [root@localhost demo]# go run main.go create rule --name foo Create rule foo success. [root@localhost demo]# go run main.go create -n foo rule Create rule foo success.2.5 读取配置
最后说说配置。需求:要求 --name 标志存在默认值,且该值是可配置的。如果只需要标志提供默认值,我们只需要修改 StringVarP 的 value 参数就可以实现。但是这个需求关键在于标志是可配置的,所以需要借助配置文件。
很多情况下,CLI 是需要读取配置信息的,比如 kubectl 的~/.kube/config。在帮助提示里可以看到默认的配置文件为 $HOME/.demo.yaml:
Global Flags: --config string config file (default is $HOME/.demo.yaml)配置库我们可以使用 Viper。Viper 是 Cobra 集成的配置文件读取库,支持 YAML,JSON, TOML, HCL 等格式的配置。
添加配置文件 $HOME/.demo.yaml,增加 name 字段:
[root@localhost ~]# vim $HOME/.demo.yaml name: foo修改 rule.go:
package cmd import ( "fmt" // 导入 viper 包 "github.com/spf13/viper" "github.com/spf13/cobra" ) var name string var ruleCmd = &cobra.Command{ Use: "rule", Short: "rule", Long: "Rule Command.", Run: func(cmd *cobra.Command, args []string) { // 不输入 --name 从配置文件中读取 name if len(name) == 0 { name = viper.GetString("name") // 配置文件中未读取到 name,打印帮助提示 if len(name) == 0 { cmd.Help() return } } fmt.Printf("Create rule %s success.\n", name) }, } func init() { createCmd.AddCommand(ruleCmd) ruleCmd.Flags().StringVarP(&name, "name", "n", "", "rule name") }运行测试:
[root@localhost demo]# go run main.go create rule Using config file: /root/.demo.yaml Create rule foo success.如果 CLI 没有用到配置文件,可以在初始化项目的时候关闭 Viper 的选项以减少编译后文件的体积,如下:
cobra init demo --pkg-name=demo --viper=false2.6 编译运行
编译生成命令行工具:[root@localhost demo]# go build -o demo运行测试:
[root@localhost demo]# ./demo create rule Using config file: /root/.demo.yaml Create rule foo success.