Cobra
概述
参考:
Cobra 是一个 Go 语言的库,其提供简单的接口来创建强大现代的 CLI 接口,类似于 git 或者 go 工具。同时,它也是一个应用,用来生成个人应用框架,从而开发以 Cobra 为基础的应用。热门的 docker 和 k8s 源码中都使用了 Cobra
Cobra 结构由三部分组成:
- Command(命令) #
- Args(参数) #
- Flag(标志) #
type Command struct {
Use string // The one-line usage message.
Short string // The short description shown in the 'help' output.
Long string // The long message shown in the 'help<this-command>' output.
Run func(cmd *Command, args []string) // Run runs the command.
...
}
传统 Linux 和 unix 的话命令规范为情况为下面几种
# 单独命令,例如date
date
# 带选项的命令
ls -l
# 选项有值
last -n 3
# 短选项合起来写,注意合起来写的时候最后一个选项以外的选项都必须是无法带值的,例如last -n 3 -R只能合起来写成下面的
last -Rn 3
# 无值的长选项
rm --force
# 带值的长选项
last --num 3
last --num=3
find -type f
# 值能追加的命令
command --host ip1 --host ip2 #命令内部能完整读取所有host做处理
# 带args的命令
rm file1 file2
cat -n file1 file2
# 多级命令
ip addr show
ip addr delete xxx
# 所有情况的命令
cmd sub_cmd1 subcmd2 --host 10.0.0.2 -nL3 -d ':' --username=admin '^a' '^b'
而 cobra 是针对长短选项和多级命令都支持的库,单独或者混合都是支持的,不过大多数还是用来写多级命令的 cli tool 用的。命令的格式为下列
rootCommand subcommand1 subcommand2 -X value --XXXX value -Y a -Y b --ZZ c --ZZ d args1 args2
前三个是不同场景下的说明,最后一个是要执行的函数
使用 Cobra 编写的典型项目
Cobra 用于许多 Go 项目,例如 Kubernetes、 Hugo 和 GitHub CLI 等等。此列表包含更广泛的使用 Cobra 的项目列表。
https://github.com/gohugoio/hugo
https://github.com/containerd/nerdctl
安装与导入
安装
go get -u github.com/spf13/cobra@latest
导入
import "github.com/spf13/cobra"
Cobra 命令行工具
cobra-cli 是一个命令行程序,用于生成 Cobra 应用程序和命令文件。它将引导您的应用程序脚手架以快速开发基于 Cobra 的应用程序。这是将 Cobra 合并到您的应用程序中的最简单方法。
go install github.com/spf13/cobra-cli@latest
安装后会创建一个可执行文件 cobra-cli 位于 ${GOPATH}/bin 目录中
$ go env | grep GOPATH
GOPATH="/home/desistdaydream/go"
$ which cobra-cli
/home/desistdaydream/go/bin/cobra-cli
Cobra 的基本使用
我们使用如下命令初始化一个项目
go mod init github.com/DesistDaydream/go-cobra
cobra-cli init
Cobra 的应用程序目录结构通常如下:
$ tree
.
├── LICENSE
├── cmd
│ └── root.go
├── go.mod
├── go.sum
└── main.go
Notes: cobra-cli 默认情况下,Cobra 将添加 Apache 许可证。如果不想这样,可以将标志添加
-l none到所有生成器命令。但是,它会在每个文件顶部添加Copyright © 2022 NAME HERE <EMAIL ADDRESS>这样的添加版权声明。如果通过选项-a YOUR NAME则将包含姓名。
main.go 文件非常简单,只有一个目的,初始化 Cobra
package main
import "github.com/DesistDaydream/go-cobra/cmd"
func main() {
cmd.Execute()
}
注意
这部分无法理解的话,可以先了解 Cobra 的基本机制,再写点项目、看点项目,可有有更深入的体会
使用 cobra-cli 生成的目录结构在真正使用时并不灵活,我们通常会将 XXXCmd 变量封装到函数数,以便可以对变量进行更多的处理。灵活性更大。下面的使用示例并不是生产推荐的结构和用法。
比较不错的 目录结构 与 添加子命令的抽象语法 参考早期的 nerdctl 项目,不使用 init() 函数进行初始化,而是将 *cobra.Command 的实例作为某个函数的返回值,由上级命令的函数批量管理子命令,效果如下
func Execute() {
app := newApp()
err := app.Execute()
if err != nil {
os.Exit(1)
}
}
func newApp() *cobra.Command {
var rootCmd = &cobra.Command{
......
}
rootCmd.AddCommand(
createOneCommand(),
createTwoCommand(),
)
return rootCmd
}
func createOneCommand() *cobra.Command {
oneCommand := &cobra.Command{
......
}
return oneCommand
}
func createTwokCommand() *cobra.Command {
twoCommand := &cobra.Command{
......
}
return twoCommand
}
创建命令
Command{} 是 Cobra 命令的核心结构体,只有有了这个结构体,才能围绕命令执行方法、设置命令行标志等。
创建根命令(rootCmd)
根命令通常放在 cmd/root.go 文件中
// rootCmd 表示在没有任何子命令调用的情况时的基本命令
var rootCmd = &cobra.Command{
Use: "go-cobra",
Short: "这个应用简要的描述",
Long: `横跨多行的较长描述,可能包含示例和使用应用程序的用法。 例如:
当我运行程序时,会显示该描述内容
如果使用缩进,这行在界面展示时有缩进。`,
Run: func(cmd *cobra.Command, args []string) {
// 如果这个应用没有任何子命令,直接使用 go-cobra 执行的话,将会执行这里面的代码
},
}
创建子命令
使用 Command.AddCommand() 方法将一个或多个命令添加到父命令中,下面的示例可以为根命令添加一个 version 子命令。
var versionCmd := &cobra.Command{
Use: "version",
Short: "这个命令的简要描述",
Long: `横跨多行的较长描述,可能包含示例和使用命令的用法。`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("version called")
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}
Flag(命令行标志)
标志可以是“持久的”,这意味着该标志将可用于分配给它的命令以及该命令下的每个命令。对于全局标志,将标志分配为根上的持久标志。
rootCmd.PersistentFlags().StringVarP(&rootFlags.CfgFile, "config", "c", "", "指定配置文件")
也可以在本地分配一个标志,它只适用于该特定命令
rootCmd.Flags().BoolP("toggle", "t", false, "关于toggle标志的帮助信息")
实际命令都有选项,分为持久和本地,持久例如kubectl的-n可以用在很多二级命令下,本地命令选项则不会被继承到子命令。我们给 remove 添加一个移除指定名字的选项,修改cmd/remove.go的 init 函数:
添加 Flags 使用 Command.Flags() 或 cmd.PersistentFlags() 方法,具体有以下使用规律
- <type>
- <type>P
- <type>Var
- <type>VarP
带 P 的相对没带 P 的多了个短选项,没带 P 的选项只能用--long-iotion这样,而不能使用 -l 这种。
- 获取选项的值用
cmd.Flags().GetString("name")
不带 Var 的获取值使用Get<type>("FlagName"),这样似乎非常麻烦,实际中都是用后面俩种 Var 直接传入地址自动注入的,例如
var dates int32
cmd.Flags().Int32VarP(&dates,"date", "d", 1234, "this is var test")
- type 有
Slice,Count,Duration,IP,IPMask,IPNet之类的类型,Slice 类型可以多个传入,直接获取就是一个切片,例如--master ip1 --master ip2 - 类似
--force这样的开关型选项,实际上用 Bool 类型即可,默认值设置为 false,单独给选项不带值就是 true,也可以手动传入 false 或者 true - MarkDeprecated 告诉用户放弃这个标注位,应该使用新标志位,MarkShorthandDeprecated 是只放弃短的,长标志位依然可用。MarkHidden 隐藏标志位
MarkFlagRequired("region")表示 region 是必须的选项,不设置下选项都是可选的
读取配置文件
类似 kubectl 的 ~/.kube/config 和 gcloud 这些 cli 都会读取一些配置信息,也可以从命令行指定信息。细心观察的话可以看到这个是一直存在在命令帮助上的
Global Flags:
--config string config file (default is $HOME/.cli.yaml)
spf13 里的 viper 包的几个方法就是干这个的,viper 是 cobra 集成的配置文件读取的库 可以通过环境变量读取~~
removeCmd.Flags().StringP("name", "n", viper.GetString("ENVNAME"), "The application to be executed")
默认可以在 cmd/root.go 文件里看到默认配置文件是家目录下的.应用名,这里我是 $HOME/.cli.yaml,创建并添加下面内容
name: "Billy"
greeting: "Howdy"
Command 的 Run 里提取字段
Run: func(cmd *cobra.Command, args []string) {
greeting := "Hello"
name, _ := cmd.Flags().GetString("name")
if name == "" {
name = "World"
}
if viper.GetString("name")!=""{
name = viper.GetString("name")
}
if viper.GetString("greeting")!=""{
greeting = viper.GetString("greeting")
}
fmt.Println(greeting + " " + name)
},
也可以将配置文件中的值绑定到命令行 Flag 里。在下面的示例中,通过 viper 包获取到的 author 的值将会绑定到命令行 Flag 的 author 中:
var author string
func init() {
rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}
// 不想使用的话相关可以注释掉 viper 相关的,编译出来的程序能小几M
root.go 文件简单示例
rootCmd 的声明通常会被封装在一个函数中,这个封装函数会被 Execute() 执行。
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"os"
vipercmd "github.com/DesistDaydream/go-cobra/cmd/viper"
"github.com/DesistDaydream/go-cobra/config"
"github.com/spf13/cobra"
)
type RootFlags struct {
// 这里定义的变量,可以在下面的 init 函数中,通过 rootCmd.PersistentFlags().StringVar(&CfgFile, "config", "", "指定配置文件(默认在$HOME/.cobracli.yaml)") 进行绑定
// 也可以通过 viper 进行绑定
CfgFile string
}
var rootFlags RootFlags
// Execute 将所有子命令添加到根命令并设置 Flags。这由 main.main() 调用。它只需要对 rootCmd 发生一次。
func Execute() {
app := newApp()
err := app.Execute()
if err != nil {
os.Exit(1)
}
}
func newApp() *cobra.Command {
// rootCmd 表示在没有任何子命令调用的情况时的基本命令。
var rootCmd = &cobra.Command{
Use: "go-cobra",
Short: "这个应用简要的描述",
Long: `横跨多行的较长描述,可能包含示例和使用应用程序的用法。 例如:
当我运行程序时,会显示该描述内容
如果使用缩进,这行在界面展示时有缩进。`,
// 如果这个应用没有任何子命令,直接使用 go-cobra 执行的话,将会执行下面 Run 字段指定的函数
Run: rootRun,
}
// 我们可以在这里定义命令行 Flags 和 配置设置。
// 这里可以做一些初始化的工作,比如初始化数据库连接、初始化日志、读取配置文件等等
// ######## 添加 命令行Flags ########
// Cobra 支持 持久性flags (i.e. Global Flags),如果在这个位置定义,则这些 flags 对应用程序来说是全局的。
// 第一个参数是变量,用于存储该flag的值;第二个参数为该flag的名字;第三个参数为该flag的默认值,无默认值可以为空;第四个参数是该flag的描述信息
// 比如我现在使用如下命令: go-cobra --config abc 。那么 cfgFile 的值为abc。
rootCmd.PersistentFlags().StringVarP(&rootFlags.CfgFile, "config", "c", "", "指定配置文件")
// Cobra 还支持本地 flags ,仅在直接调用此命令时才有意义。
rootCmd.Flags().BoolP("toggle", "t", false, "关于toggle标志的帮助信息")
// ######## 添加 配置 ########
// !!!注意!!!:Cobra 只有在上面的 Run 字段定义的函数运行之前才会解析手动指定的命令行 Flags,否则只能获取到代码中设置的 Flags 默认值。
// 比如运行 go run main.go --config="abc.yaml" 时,rootFlags.CfgFile 并不会被赋值为 abc.yaml,而是默认值。
// 此时有两种方式解决这个问题:
// 1. 使用 Prase() 函数,提前解析 Flags:
// rootCmd.PersistentFlags().Parse(os.Args)
// 2. 使用 OnInitialize() 函数,该函数会在 Command.Run 字段指定的函数执行前,先执行 initConfig 函数。
// 查看 Cobra 源码,OnInitialize() 中的 initializers 变量会在 preRun() 函数中被执行。
cobra.OnInitialize(initConfig)
// 假如我现在在这里执加了一行 config.NewConfig(rootFlags.CfgFile),那么这个函数其实是会在 OnInitialize 函数执行之前执行的。
// config.NewConfig(rootFlags.CfgFile)
// ######## 添加 子命令 ########
// 为了更好的管理子命令,我们通常会将子命令放在不同的文件中,然后在这里进行注册
rootCmd.AddCommand(
NewVersionCmd(),
vipercmd.NewViperCmd(),
)
return rootCmd
}
func initConfig() {
// 使用 Viper 简化处理配置文件的过程。Viper 可以从 JSON、TOML、YAML、HCL、环境变量和命令行参数等等地方中读取配置。
config.NewConfig(rootFlags.CfgFile)
}
func rootRun(cmd *cobra.Command, args []string) {
fmt.Println("主程序运行后执行的代码块。如果注销 Run,则运行主程序会显示上面Long上的信息")
fmt.Println("在 Run 字段指定的函数中,我们可以获取到 Flags 的值:", rootFlags.CfgFile)
}
cobra.Command 结构体解析(待整理)
别名(Aliases)
现在我们想添加一个别名
cli
|----app
|----remove|rm
我们修改下初始化值即可
var removeCmd = &cobra.Command{
Use: "remove",
Aliases: []string{"rm"},
命令帮助添加示例(Example)
我们修改下 remove 的 Run 为下面
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
cmd.Help()
return
}
},
运行输出里 example 是空的
[root@k8s-m1 cli]# go run main.go app remove
A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.
Usage:
cli app remove [flags]
Aliases:
remove, rm
Flags:
-h, --help help for remove
-n, --name string The application to be executed
Global Flags:
--config string config file (default is $HOME/.cli.yaml)
添加 example
var removeCmd = &cobra.Command{
Use: "remove",
Aliases: []string{"rm"},
Example: `
cli remove -n test
cli remove --name test
`,
go run main.go app remove
A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.
Usage:
cli app remove [flags]
Aliases:
remove, rm
Examples:
cli remove -n test
cli remove --name test
Flags:
-h, --help help for remove
-n, --name string The application to be executed
Global Flags:
--config string config file (default is $HOME/.cli.yaml)
参数验证器(Args)
该字段接收类型为type PositionalArgs func(cmd *Command, args []string) error
内置的为下面几个:
NoArgs: 如果存在任何位置参数,该命令将报告错误。ArbitraryArgs: 该命令将接受任何 args。OnlyValidArgs: 如果存在任何不在 ValidArgs 字段中的位置参数,该命令将报告错误 Command。MinimumNArgs(int): 如果没有至少 N 个位置参数,该命令将报告错误。MaximumNArgs(int): 如果有多于 N 个位置参数,该命令将报告错误。ExactArgs(int): 如果没有确切的 N 位置参数,该命令将报告错误。RangeArgs(min, max):如果 args 的数量不在预期 args 的最小和最大数量之间,则该命令将报告错误。- 自己写的话传入符合类型定义的函数即可
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires at least one arg")
}
if myapp.IsValidColor(args[0]) {
return nil
}
return fmt.Errorf("invalid color specified: %s", args[0])
},
前面说的没传递选项和任何值希望打印命令帮助也可以用MinimumNArgs(1)来触发
自定义 help,usage 输出
help
command.SetHelpCommand(cmd *Command)
command.SetHelpFunc(f func(*Command, []string))
command.SetHelpTemplate(s string)
usage
command.SetUsageFunc(f func(*Command) error)
command.SetUsageTemplate(s string)
Run 的 hook
Run 功能的执行先后顺序如下:
- PersistentPreRun
- PreRun
- Run
- PostRun
- PersistentPostRun
接收 func(cmd *Command, args []string) 类型的函数,Persistent 的能被下面的子命令继承
RunE 功能的执行先后顺序如下:
- PersistentPreRunE
- PreRunE
- RunE
- PostRunE
- PersistentPostRunE
接收 func(cmd *Command, args []string) error 的函数
预处理相关函数说明
当具有多级子命令时,PersistentXXX() 相关函数只会执行一次
比如现在创建了一个 cobra 命令,具有如下几个子命令:
- add
- command
- args
- del
如果在 cobra 和 add 中都使用了 PersistentPreRun() 函数的话,只会有一个执行,并且是子命令的方法优先,参考 Issue:
可以在最底层的子命令中,通过如下方式执行父命令的 PersistenXXX() 函数
func CreateCommand() *cobra.Command {
subCmd := &cobra.Command{
PersistentPreRun: subPersistentPreRun,
}
subCmd.AddCommand(
CreateSubSubCommand(),
)
return subCmd
}
func subPersistentPreRun(cmd *cobra.Command, args []string) {
// 执行父命令的预运行逻辑
parent := cmd.Parent()
if parent.PersistentPreRun != nil {
parent.PersistentPreRun(parent, args)
}
// 本子命令的预运行逻辑
}
OnInitialize与OnFinalize函数
除了 PersistentXXX() 这种函数以外,我们还可以使用 OnInitialize() 函数来执行预运行的逻辑,该函数可以避免在每一个子命令的 PersistentPreRun() 中重复调用 parent.PersistentPreRun。
OnInitialize()函数会在调用每个命令的Execute()方法时运行。OnFinalize()函数则会在完成每个命令的Execute()方法时运行。
OnInitialize() 会将其参数赋值给 initializers 变量(这个变量的类型是一个函数),该变量会在 Command.preRun() 函数中被执行
Notes: 这个 Command.preRun() 与 Command.PreRun() 不同。前者是 Command 结构体的方法,后者是 Command 的一个属性。
在 execute() 中,preRun 在 PreRun 之前执行。
但是,在任何子命令中使用了 cobra.OnInitialize(),那么执行其他子命令时,同样会执行 cobra.OnInitialize() 的逻辑。
反馈
此页是否对你有帮助?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.