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 项目,例如 KubernetesHugoGitHub 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 有 SliceCountDuration,IP,IPMask,IPNet 之类的类型,Slice 类型可以多个传入,直接获取就是一个切片,例如 --master ip1 --master ip2
  • 类似 --force 这样的开关型选项,实际上用 Bool 类型即可,默认值设置为 false,单独给选项不带值就是 true,也可以手动传入 false 或者 true
  • MarkDeprecated 告诉用户放弃这个标注位,应该使用新标志位,MarkShorthandDeprecated 是只放弃短的,长标志位依然可用。MarkHidden 隐藏标志位
  • MarkFlagRequired("region") 表示 region 是必须的选项,不设置下选项都是可选的

读取配置文件

类似 kubectl~/.kube/configgcloud 这些 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() 的逻辑。


最后修改 September 9, 2024: perf (65cb84d7)