文章首发于个人公众号:阿拉平平
最近折腾了下命令行库 Cobra,和大家分享下。本文演示环境为 CentOS 7.5,Golang 1.11。
文章目录:
Cobra 介绍
1.1 概念
1.2 安装
1.3 初始化
1.4 代码分析Cobra 实践
2.1 子命令
2.2 子命令嵌套
2.3 参数
2.4 标志
2.5 读取配置
2.6 编译运行
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/cobra
但是由于网络原因,有些包会下载失败,提示 i/o timeout
:
package golang.org/x/sys/unix: unrecognized import path "golang.org/x/sys/unix" (https fetch: Get https://golang.org/x/sys/unix?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
package golang.org/x/text/transform: unrecognized import path "golang.org/x/text/transform" (https fetch: Get https://golang.org/x/text/transform?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
package golang.org/x/text/unicode/norm: unrecognized import path "golang.org/x/text/unicode/norm" (https fetch: Get https://golang.org/x/text/unicode/norm?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
网上解决方法很多,这里我推荐使用 gopm 来下载:
# 下载 gopm,之后会在 $GOPATH/bin 目录下生成 gopm
go get -u github.com/gpmgo/gopm
# 使用 gopm 来下载 cobra
gopm get -u -g github.com/spf13/cobra/cobra
下载完成后安装 cobra 工具,在 $GOPATH/bin
会生成可执行文件:
go install github.com/spf13/cobra/cobra
将生成的 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 项目:
[root@localhost ~]# cd $GOPATH/src
[root@localhost src]# cobra init demo --pkg-name=demo
Your Cobra applicaton is ready at
/root/go/src/demo
当前项目结构为:
demo
├── cmd
│ └── root.go
├── LICENSE
└── main.go
可以看到初始化后的项目非常简单,主要是 main.go
和 root.go
文件。在编写代码之前,我们先分析下目前代码的逻辑。
1.4 代码分析
先查看下入口文件 main.go
。代码逻辑很简单,就是调用 cmd 包里 Execute()
函数:
package main
import "demo/cmd"
func main() {
cmd.Execute()
}
再看下 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) { },
}
// Execute 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 Execute() {
if err := rootCmd.Execute(); 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
如果运行的结果和我的一致,那我们就可以进入到实践环节了。
Cobra 实践
铺垫了这么久,终于可以开始实践了。实践环节中,我会 提一些需求,然后我们一起实现一个简单的命令行工具。
子命令
之前运行会提示 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?
create
子命令嵌套
对于功能相对复杂的 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 called
参数
先说说参数。现在有个需求:给 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,这里就不多做赘述了。
标志
再说说标志。现在要求 CLI 不接受参数,而是通过标志 --name
对 rule
进行描述。这个又该如何实现?
demo create rule --name foo
Cobra 中有两种标志:持久标志 ( 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.
读取配置
最后说说配置。需求:要求 --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=false
编译运行
编译生成命令行工具:
[root@localhost demo]# go build -o demo
运行测试:
[root@localhost demo]# ./demo create rule
Using config file: /root/.demo.yaml
Create rule foo success.
参考文档:
- Github - https://github.com/spf13/cobra
- Cobra 的一些笔记 - https://zhangguanzhang.github.io/2019/06/02/cobra/
- Golang之使用Cobra - https://o-my-chenjian.com/2017/09/20/Using-Cobra-With-Golang/
- golang命令行库Cobra的使用 - https://www.jianshu.com/p/7abe7cff5384
有疑问加站长微信联系(非本文作者)