项目地址: [spf13/pflag at v1.0.5 (github.com)](https://github.com/spf13/pflag/tree/v1.0.5)
Cobra 是用 go 语言编写的一款命令行工具,也是 go 的常用库之一,它提供了一个简单的接口来创建类似于git和go工具的强大现代CLI接口。
项目的作者 Steve Francia 是前 go 团队成员,其用 go 创作了很多热门的作品如 hugo、vim、viper再到我今天要介绍的cobra。作者的地址放在下面:spf13 (Steve Francia) (github.com)
关于 go 的描述在项目主页里有这样一句话——pflag 是Go的 flag 包的替代物,实现了POSIX/ gnu风格的——flag。pflag在与Go语言相同风格的BSD许可下可用,可以在license文件中找到。
这可以从一个小demo中看出区别:
flag包
packagemain
import (
"flag"
"fmt"
)
funcmain() {
flag.Parse()
args :=flag.Args()
iflen(args) <=0 {
fmt.Println("Usage: admin-cli [command]")
return
}
switchargs[0] {
case"help":
// ...
case"export":
//...
iflen(args) ==3 { // 导出到文件
// todo
} elseiflen(args) ==2 { // 导出...
// todo
}
default:
//...
}
}
packagemain
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
// rootCmd represents the base command when called without any subcommands
varrootCmd=&cobra.Command{
Use: "api",
Short: "A brief description of your application",
Long: `A longer description `,
}
// 命令一
varmockMsgCmd=&cobra.Command{
Use: "mockMsg",
Short: "批量发送测试文本消息",
Long: ``,
Run: func(cmd*cobra.Command, args []string) {
fmt.Println("mockMsg called")
},
}
// 命令二
varexportCmd=&cobra.Command{
Use: "export",
Short: "导出数据",
Long: ``,
Run: func(cmd*cobra.Command, args []string) {
fmt.Println("export called")
},
}
funcExecute() {
err :=rootCmd.Execute()
iferr!=nil {
os.Exit(1)
}
}
funcinit() {
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.AddCommand(mockMsgCmd)
rootCmd.AddCommand(exportCmd)
exportCmd.Flags().StringP("out", "k", "./backup", "导出路径")
}
funcmain() {
Execute()
}
我们可以明显得发现,使用了 github.com/spf13/cobra包后,我我们再也不用处理各种参数的组合了,只需要编写自己的业务逻辑代码即可。
Cobra是建立在一个由 commands, arguments & flags 组成的结构上。
Commands 代表 action(行为,具体操作的动作),Args 是事物,Flags是这些方法的修饰语。
最好的应用程序在使用时就像句子一样,因此,用户凭直觉就知道如何与它们互动。
要遵循的模式是 APPNAME VERB NOUN --ADJECTIVE 或者 APPNAME COMMAND ARG --FLAG。
几个好的实际的例子可以更好地说明这一点。
在下面的例子中,'server'是一个 command ,'port'是一个 flag 。
hugo server --port=1313
在这个命令中,我们告诉 Git 克隆直链 url。
git clone URL --bare
command(命令)是应用程序的中心点。应用程序支持的每一个交互都将包含在一个命令中。一个命令可以有子命令,也可以选择运行一个 action 。
在上面的例子中,"server "是一个命令。
flag 是一种修改命令行为的方法。Cobra 支持完全符合 POSIX 标准的 flags ,以及 Go 的 flag package 。一个 Cobra 命令可以定义持续到子命令的 flags ,以及只对该命令有效的 flags 。如下:
./learncobra father --help
./learncobra father son --help
在上面的例子中,'help'是 flag 。(需要注意的是,两个'help'分别用于输出 father 命令和 son 命令的说明文档),当然严格意义上来讲 father 命令本身也是一个子命令,其位于 root 命令下。具体的命令父子关系问题稍后再讨论。
flag 功能是由 pflag 库提供的,它是 pflag library 的分叉,保持了相同的接口,同时增加了 POSIX 兼容性。
表示数值
使用 Cobra 很容易。首先,使用 go get 来安装最新版本的库。
go get -u github.com/spf13/cobra@latest
注:由于库迭代速度比较快,可能会发生库转移,命令改变等等,具体安装命令建议到项目地址处查看
接下来,在你的应用程序中包含 Cobra:
import "github.com/spf13/cobra"
cobra-cli是一个命令行程序,用于生成 cobra 应用程序和命令文件。它将引导你的应用程序脚手架,快速开发基于 Cobra 的应用程序。它是将 Cobra 纳入你的应用程序的最简单方法。
它可以通过运行以下指令来安装:
go install github.com/spf13/cobra-cli@latest
Cobra 提供了自己的程序,它将创建您的应用程序并添加您想要的任何命令。这是将 Cobra 纳入你的应用程序的最简单方法。
用命令 go install github.com/spf13/cobra-cli@latest 来安装 cobra 生成器。Go 会自动将其安装在你的 $GOPATH/bin 目录中,该目录应该在你的 $PATH 中。
一旦安装完毕,你的 cobra-cli 命令便可以使用。在命令行中输入 cobra-cli 来确认。
目前Cobra生成器只支持两种操作。
cobra-cli init [app] 命令将为您创建初始的应用程序代码。这是一个非常强大的应用程序,它将为您的程序填充正确的结构,使您可以立即享受到 Cobra 的所有好处。它还可以将你指定的许可证应用于你的应用程序。
随着 Go 模块的引入,Cobra 生成器已被简化以利用模块的优势。Cobra 生成器在 Go 模块中工作。
如果你已经创建了一个模块,跳过这一步。
如果你想初始化一个新的 Go 模块。
创建一个新的目录
cd进入该目录
运行 go mod init <MODNAME>。
例如
cd $HOME/code
mkdir myapp
cd myapp
go mod init github.com/spf13/myapp
在 Go 模块中运行 cobra-cli init 。这将创建一个新的裸机项目供你编辑。
你应该能够立即运行你的新应用程序。用 go run main.go 试试。
你要打开并编辑'cmd/root.go'并提供你自己的描述和逻辑。
例如
cd $HOME/code/myapp
cobra-cli init
go run main.go
cobra-cli init
也可以从一个子目录中运行,这可以根据 cobra 生成器本身的组织结构看出。如果你想把你的应用程序代码和库的代码分开,这很有用。
你可以用 --author flag 向它提供作者名字。例如,cobra-cli init --author "Steve Francia spf@spf13.com"
你可以用 --license 来提供使用许可,例如:cobra-cli init --license apache。
使用 --viper 标志来自动设置 viper
Viper 是 Cobra 的一个伙伴,旨在提供对环境变量和配置文件的简单处理,并将它们与应用程序的标志无缝连接。
一旦cobra应用程序被初始化,你就可以继续使用Cobra生成器来为你的应用程序添加额外的命令。做到这一点的命令是 cobra-cli add 。
比方说,你创建了一个应用程序,你想为它添加以下命令。
app serve
app config
app config create
在你的项目目录中(你的 main.go 文件所在的位置),你将运行以下命令。
cobra-cli add serve
cobra-cli add config
cobra-cli add create -p 'configCmd'
cobra-cli add 支持所有与 cobra-cli init 一样的可选 flags(如上所述)。
你会注意到,最后这条命令有一个 -p 标志。这是用来给新添加的命令分配一个父命令的。在这个例子中,我们想把 "create" 命令分配给 "config" 命令。如果没有指定,所有的命令都有一个默认的父命令,即 rootCmd 。
默认情况下,cobra-cli 会将 Cmd 附加到所提供的名称上,并使用这个名称作为内部变量名称。当指定父级时,请确保与代码中使用的变量名称相匹配。
注意:命令名称使用 camelCase(不是 snake_case/kebab-case)。否则,你会遇到错误。例如,cobra-cli add add-user 是不正确的,但 cobra-cli add addUser 是有效的。
一旦你运行了这三个命令,你就会有一个类似以下的应用程序结构。
▾ app/
▾ cmd/
config.go
create.go
serve.go
root.go
main.go
在这一点上,你可以运行 go run main.go ,它将运行你的应用程序。go run main.go serve、go run main.go config、go run main.go config create 以及 go run main.go help serve 等都可以工作。当然,如果你用的是 go build 命令来运行程序,那么 ./moduleName serve、./moduleName create 以及 ./moduleName serve 等都可以工作( moduleName 是当前文件的模块名)。
现在你已经有了一个基于Cobra的基本应用程序,并开始运行。下一步是在cmd中编辑这些文件,为你的应用程序进行定制。
关于使用Cobra库的完整细节,请阅读The Cobra User Guide.。
尽情享受吧!
如果你提供一个简单的配置文件,Cobra 生成器将更容易使用,这将帮助你消除在标志中反复提供的一堆信息。
一个例子~/.cobra.yaml文件。
author: Steve Francia <spf@spf13.com>
license: MIT
useViper: true
你也可以使用内置的许可证。例如,GPLv2、GPLv3、LGPL、AGPL、MIT、2-Clause BSD 或 3-Clause BSD。
你可以通过将 license 设置为 none 来指定没有 license,或者你可以指定一个自定义 license 。
author: Steve Francia <spf@spf13.com>
year: 2020
license:
header: This file is part of CLI application foo.
text: |
{{ .copyright }}
This is my license. There are many like it, but this one is mine.
My license is my best friend. It is my life. I must master it as I must
master my life.
在上面的自定义许可证配置中,许可证文本中的 copyright 行是由 author 和 year 属性生成的。LICENSE 文件的内容是:
Copyright © 2020 Steve Francia <spf@spf13.com>
This is my license. There are many like it, but this one is mine.
My license is my best friend. It is my life. I must master it as I must
master my life.
header 属性被用作许可证头文件。没有进行插值。这是 go 文件头的例子。
/*
Copyright © 2020 Steve Francia <spf@spf13.com>
This file is part of CLI application foo.
*/
在安装了 cobra-cli 生成器后,指向 init 初始化创建项目
cobra-cli init
将在当前文件夹自动生成以下的目录结构
├── go.mod
├── go.sum
├── LICENSE
├── main.go
│
└───cmd
└── root.go
main.go:
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package main
import "test/cmd"
func main() {
cmd.Execute()
}
root.go(有删减):
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "test",
Short: "A brief description of your application",
Long: `A longer description `,
//Run: func(cmd *cobra.Command, args []string) {
// fmt.Println("api called")
//},
}
// 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() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// 全局flag
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.api.yaml)")
// local flag
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
如果此刻运行程序,不指定参数,会默执行 rootCmd ,打印使用说明
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.
可见,初始的,Cobra Application 的 RootCmd 不具备任何逻辑功能,仅仅是输出一些描述信息。
RootCmd 初始仅有一个 --help flag,下面让我们来新增一个命令试试,这本身也是命令行程序的魅力,即可以通过各种不同的参数执行不同的动作。
语法:
cobra-cli add [command]
实例
cobra-cli add mock-msg
mockMsg created at E:\backend\Go_backend\test
可以看到指令里我们希望创建的 command 名为 mock-msg ,但实际创建的 command 名为 mockMsg(驼峰命名法),这点在上文介绍向项目中添加命令时提到过。
此时,在cmd下会多一个文件(mockMsg.go),内容如下:(有删改)
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var mockMsgCmd = &cobra.Command{
Use: "mockMsg",
Short: "A brief description of your command",
Long: `mock msg command`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("mockMsg called")
},
}
func init() {
rootCmd.AddCommand(mockMsgCmd)
}
再次编译程序,并执行 rootCmd
go build
./test
会发现多了一个命令,且多了 Flags 提示
...
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
mockMsg A brief description of your command
Flags:
-h, --help help for test
-t, --toggle Help message for toggle
...
执行 mockMsg 命令:
./test mockMsg
mockMsg called
可以发现程序输出了 mockMsg called ,这意味着它正常运行了,此时就可以在生成的 mockMsg.go: Run() 函数中,放你自己的业务代码了。
上面新增了一个命令 mockMsg ,通过 ./test -h 打印了命令的 help 内容,但是 Use 里面指定的内容打印到哪里去了呢?
这个时候,需要针对 Command 再指定 help,此时就能打印这个命令的具体用法了。(./test -h打印的是 rootCmd 的 help 内容,./test mockMsg -h 打印的是 mockMsg 内的 help 内容,两个 command 是父子关系)
让我们在 mockMsg.go: init() 中添加以下代码
func init() {
mockmsgCmd.Flags().Int32P("goroutine", "g", 1, "并发routine数量")
mockmsgCmd.Flags().Int32P("packet", "p", 20, "每个routine一秒写入mq的数量")
rootCmd.AddCommand(mockmsgCmd)
}
注:经试验,rootCmd.AddCommand(mockmsgCmd)和前两句代码的顺序可互换,不影响程序的运行和实现。
在 &cobra.Command 中进行修改
// mockMsgCmd represents the mockMsg command
var mockMsgCmd = &cobra.Command{
Use: "mockMsg",
Short: "Mass produce mq messages",
// Long: `A longer description `,
Run: func(cmd *cobra.Command, args []string) {
// GetXXX() 内要写全名名
g, _ := cmd.Flags().GetInt32("goroutine")
p, _ := cmd.Flags().GetInt32("packet")
fmt.Println("mockmsg called,flags:g=", g, ",p=", p, ",args:", args)
},
}
再次编译程序,并执行 mockMsg
go build
./test mockMsg -h
可以得到
Mass produce mq messages
Usage:
test mockMsg [flags]
Flags:
-g, --goroutine int32 并发routine数量 (default 1)
-h, --help help for mockMsg
-p, --packet int32 每个routine一秒写入mq的数量 (default 20)
其中-g和-p是新增的2个flag,后面紧跟着的是它的接收值类型以及用途
执行 mockMsg 命令
./test mockMsg -p 322 -g 5 args1 args2
mockmsg called,flags:g= 5 ,p= 322 ,args: [args1 args2]
发现回显正常,值被接受收了,并打印了 args 构成的数组。
cobra 里实现父子命令非常简单,且有两种方式,下面逐一介绍。
自动创建子命令是基于 Cobra 生成器的,通过简单的 cobra-cli add father,就能创建一个命令,该命令一经创建就被默认设为 rootCmd 的子命令,具体的代码可以在 father.go: init() 方法中看到,一般如下:
func init() {
rootCmd.AddCommand(fatherCmd)
}
该句代码是系统默认生成的,让我们通过一个 demo 来更加升入了解父子命令
让我们添加一对父子命令,并输出目录结构
cobra-cli add father
cobra-cli add son
tree /f
可以看到我们的目录结构如下
├── go.mod
├── go.sum
├── LICENSE
├── main.go
├── test.exe
│
└───cmd
├── father.go
├── mockMsg.go
├── root.go
└─── son.go
让我们将 father.go 修改为如下:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var fatherCmd = &cobra.Command{
Use: "father",
Short: "I'm son's father",
Long: `This is a parent-child command relationship demo.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Print("father start\n\n")
// 此处左侧的变量名必须和 shorthand 或者 name 保持一致
// GetXXX() 内要写全名
source, _ := cmd.Flags().GetInt32("source")
fmt.Print("father called,flags:S=", source, ",args:", args, "\n\n")
fmt.Println("father end")
},
}
func init() {
// 需要注意的是此处的 shorthand 只能为单个英文字符(包含大小写)
// func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string
fatherCmd.Flags().Int32P("source", "S", 8, "Int32 directory to read from")
rootCmd.AddCommand(fatherCmd)
}
试着编译运行程序,并使用 father 指令
go build
./test father -S 9 arg1
返回结果:
father start
father called,flags:S=9,args:[arg1]
son start
再让我们修改 son.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var sonCmd = &cobra.Command{
Use: "son",
Short: "I'm father's son",
Long: `This is a parent-child command relationship demo.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Print("son start\n\n")
s, _ := cmd.Flags().GetInt32("source2")
t, _ := cmd.Flags().GetString("string2")
fmt.Print("son called,flags:s=", s, ",t=", t, ",args:", args, "\n\n")
fmt.Println("son end")
},
}
func init() {
// 此处对三条语句的顺序并没有先后要求,都可以正常执行
fatherCmd.AddCommand(sonCmd)
sonCmd.Flags().Int32P("source2", "s", 2, "Int32 directory to read from")
sonCmd.Flags().StringP("string2", "t", "", "String directory to read from")
}
试着编译运行程序,并使用 son 指令
go build
./test father son -s 9 -t hahaha arg1 arg2
返回结果:
son start
son called,flags:s=9,t=hahaha,args:[arg1 arg2]
son end
有关 Int32P、StringP、GetInt32、GetString 等方法的描述详情,请到 github.com/spf13/pflag 包查看。
这就是一个简单的自动创建子命令的案例。
存在自动创建子命令,当然也可以手动创建子命令,相信通过前面的种种学习,我们对一个 command 的语法格式应该已经很熟悉了。
让我们再对 father.go 稍加修改,在其内手动添加一个子命令 cmdTimes 用于根据 times(次数)打印输入的单词。
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
var fatherCmd = &cobra.Command{
Use: "father",
Short: "I'm son's father",
Long: `This is a parent-child command relationship demo.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Print("father start\n\n")
// 此处左侧的变量名必须和 shorthand 或者 name 保持一致
// GetXXX() 内要写全名
source, _ := cmd.Flags().GetInt32("source")
fmt.Print("father called,flags:S=", source, ",args:", args, "\n\n")
fmt.Println("son end")
},
}
var echoTimes int
var cmdTimes = &cobra.Command{
Use: "times [string to echo]",
Short: "Echo anything to the screen more times",
Long: `echo things multiple times back to the user by providing a count and a string.`,
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}
func init() {
// 需要注意的是此处的 shorthand 只能为单个英文字符(包含大小写)
// func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string
fatherCmd.Flags().Int32P("source", "S", 8, "Int32 directory to read from")
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
// 给 father 子命令添加一个 cmdTimes 子命令
fatherCmd.AddCommand(cmdTimes)
rootCmd.AddCommand(fatherCmd)
}
试着编译运行程序,并使用 son 指令
go build
./test father times -t 10 args1 args2
返回结果:
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
这样我们就成功手动创建了一个子命令。
NoArgs //如果存在任何位置参数,该命令将报错
ArbitraryArgs //该命令会接受任何位置参数
OnlyValidArgs //如果有任何位置参数不在命令的 ValidArgs 字段中,该命令将报错
MinimumNArgs(int) //至少要有 N 个位置参数,否则报错
MaximumNArgs(int) //如果位置参数超过 N 个将报错
ExactArgs(int) //必须有 N个位置参数,否则报错
ExactValidArgs(int) //必须有 N 个位置参数,且都在命令的 ValidArgs 字段中,否则报错
RangeArgs(min, max) //如果位置参数的个数不在区间 min 和 max 之中,报错
我们可以通过一个简单的案例来了解
如果我们通过以下指令来运行 father 命令下的 times 子命令
./test father times -t 10
会得到结果:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
但我们知道这不是我们想要的,因为我们没有传递任何 arg 的值,我们希望在这种情况下,程序能给予我们提示信息。
现在将 father.go 的内容稍作修改,也就是在 cmdTimes 内添加如下验证
Args: cobra.MinimumNArgs(1)
现在我们的 father.go 变成了如下的样子:
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
// fatherCmd represents the father command
var fatherCmd = &cobra.Command{
Use: "father",
Short: "I'm son's father",
Long: `This is a parent-child command relationship demo.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Print("father start\n\n")
// 此处左侧的变量名必须和 shorthand 或者 name 保持一致
// GetXXX() 内要写全名
source, _ := cmd.Flags().GetInt32("source")
fmt.Print("father called,flags:S=", source, ",args:", args, "\n\n")
fmt.Println("son start")
},
}
var echoTimes int
var cmdTimes = &cobra.Command{
Use: "times [string to echo]",
Short: "Echo anything to the screen more times",
Long: `echo things multiple times back to the user by providing a count and a string.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}
func init() {
// 需要注意的是此处的 shorthand 只能为单个英文字符(包含大小写)
// func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string
fatherCmd.Flags().Int32P("source", "S", 8, "Int32 directory to read from")
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
// 给 father 子命令添加一个 cmdTimes 子命令
fatherCmd.AddCommand(cmdTimes)
rootCmd.AddCommand(fatherCmd)
}
让我们再次试着编译运行程序,并使用 times 命令
go build
./test father times -t 10
返回结果:
Error: requires at least 1 arg(s), only received 0
Usage:
test father times [string to echo] [flags]
Flags:
-h, --help help for times
-t, --times int times to echo the input (default 1)
可以看到,当我们在为传递任何 arg 信息时,程序会报错并给予我们提示,即我们至少需要传递一个 arg 让其接收(因为我们对 args 参数做的验证是至少传递一个参数————Args: cobra.MinimumNArgs(1),若要求为 2,则至少传递 2 个)
而当我们试着传递一个参数的时候
./test father times -t 10 arg1
会返回结果:
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
可以发现命令 times 正常执行了。
有关 cobra 的介绍就到这里了,希望能对你带来帮助!
参考文献:
go Cobra命令行工具入门 Go和分布式IM的博客-CSDN博客 go cobra
Go 语言编程 — Cobra 指令行工具 范桂飓的博客-CSDN博客 cobra go语言
Golang学习(二十七)强大的命令行工具cobra 默子昂的博客-CSDN博客 cobra golang
golang常用库之-命令行工具 Cobra(眼镜蛇) 西京刀客的博客-CSDN博客 spf13/cobra