初识 Cobra 需要了解的基础知识

260 阅读6分钟

Cobra:命令行框架

Cobra 就像 gotools 和 git,在命令行输入命令即可完成任务,如:

  • git clone URL --bare
  • go mod tidy

Cobra 能让我们轻松的实现命令行。

背景介绍

项目地址:github.com/spf13/cobra

Kubernetes, Hugo, and GitHub CLI 这样的项目,都使用了 Cobra

了解 Cobra 命令结构

Cobra 建立在 commands、arguments 和 flags 结构之上

  • commands 代表命令
  • arguments 代表非选项参数(必需参数)
  • flags 代表选项参数(也叫标志),标志是通过 --xxx 或者 -x 来使用的

在用户视角,命令就像下面这样:

 # 'server' is a command, and 'port' is a flag
 hugo server --port=1313
 # 类似 git
  # 'URL' is an argument
 git clone URL --bare

命令的组成部分

在开发者视角,一个命令如下所示:

 var greetCmd = &cobra.Command{
     Use:   "greet [name]",
     Short: "Greet a person by name",
     Long:  `This command greets a person by the provided name. 
     For example, if you run 'myapp greet John', it will print 'Hello, John!'.`,
     Run: func(cmd *cobra.Command, args []string) {
         name := args[0]
         fmt.Printf("Hello, %s!\n", name)
     },
 }

1、Use:命令的使用格式

可以包含命令名、参数占位符和标志等信息,主要用于向用户展示命令的基本调用方式。

 // greet 是命令名,[name] 是一个参数占位符
 var greetCmd = &cobra.Command{
     Use:   "greet [name]",
     // 其他字段...
 }

2、short & long:描述

描述命令的主要功能,short 简短描述,long 详细描述

3、run:命令处理函数

当用户在命令行中调用该命令时,这个函数会被执行。

PreRun and PostRun Hooks

同时,run 函数支持前置与后置处理的钩子函数

 var rootCmd = &cobra.Command{
     Use:   "myapp",
     Short: "A simple application with hooks",
     // 根命令的前置钩子,仅对根命令生效
     PreRun: func(cmd *cobra.Command, args []string) {
         fmt.Println("PreRun of root command is executing...")
     },
     // 根命令的持久化前置钩子,对根命令及其所有子命令生效
     PersistentPreRun: func(cmd *cobra.Command, args []string) {
         fmt.Println("PersistentPreRun of root command is executing...")
     },
     // 根命令的执行逻辑
     Run: func(cmd *cobra.Command, args []string) {
         fmt.Println("Root command is running...")
     },
     // 根命令的后置钩子,仅对根命令生效
     PostRun: func(cmd *cobra.Command, args []string) {
         fmt.Println("PostRun of root command is executing...")
     },
     // 根命令的持久化后置钩子,对根命令及其所有子命令生效
     PersistentPostRun: func(cmd *cobra.Command, args []string) {
         fmt.Println("PersistentPostRun of root command is executing...")
     },
 }

这些函数的运行顺序如下:

image-20250220222112570

注意事项

  • PersistentPreRun 在根命令及其所有子命令执行之前都会被调用
  • PreRun 只会在该命令运行时调用,子命令是不会调用的

4、Args:参数验证

cmd 中还有个 Args 部分,用于参数验证,避免传递了非选项参数

内置验证函数

Cobra内置了一些验证函数:

  • NoArgs:如果存在任何非选项参数,该命令将报错。
  • ArbitraryArgs:该命令将接受任何非选项参数。
  • OnlyValidArgs:如果有任何非选项参数不在Command的ValidArgs字段中,该命令将报错。
  • MinimumNArgs(int):如果没有至少N个非选项参数,该命令将报错。
  • MaximumNArgs(int):如果有多于N个非选项参数,该命令将报错。
  • ExactArgs(int):如果非选项参数个数不为N,该命令将报错。
  • ExactValidArgs(int):如果非选项参数的个数不为N,或者非选项参数不在Command的ValidArgs字段中,该命令将报错。
  • RangeArgs(min, max):如果非选项参数的个数不在min和max之间,该命令将报错。

使用预定义验证函数,示例如下:

 var cmd = &cobra.Command{
   Short: "hello",
   Args: cobra.MinimumNArgs(1), // 使用内置的验证函数
   Run: func(cmd *cobra.Command, args []string) {
     fmt.Println("Hello, World!")
   },
 }
自定义验证函数
 var cmd = &cobra.Command{
   Short: "hello",
   // Args: cobra.MinimumNArgs(10), // 使用内置的验证函数
   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])
   },
   Run: func(cmd *cobra.Command, args []string) {
     fmt.Println("Hello, World!")
   },
 }

5、TraverseChildren

默认情况下,Cobra只解析目标命令上的本地标志,父命令上的任何本地标志都会被忽略。通过启用 TraverseChildren,在执行目标命令之前,Cobra 将解析每个命令上的本地标志。

 command := cobra.Command{
   Use: "print [OPTIONS] [COMMANDS]",
   TraverseChildren: true,
 }

创建命令

Cobra提供了两种方式来创建命令:Cobra命令和Cobra库。Cobra命令可以生成一个Cobra命令模板,而命令模板也是通过引用Cobra库来构建命令的。

方法一:Cobra 库

如果要用Cobra库编码实现一个应用程序,需要首先创建一个空的main.go文件和一个rootCmd文件,之后可以根据需要添加其他命令。具体步骤如下:

1、创建 rootCmd:根命令

通常情况下,我们会将rootCmd放在文件cmd/root.go 或者 cmd/command.go中。

 // rootCmd represents the base command when called without any subcommands
 // 1. Hugo
 var rootCmd = &cobra.Command{
   Use:   "hugo",
   Short: "Hugo is a very fast static site generator",
   Long: `A Fast and Flexible Static Site Generator built with
                 love by spf13 and friends in Go.
                 Complete documentation is available at http://hugo.spf13.com`,
   Run: func(cmd *cobra.Command, args []string) {
     // Do Stuff Here
   },
 }
 ​
 // 2. go-answer
     rootCmd = &cobra.Command{
         Use:   "answer",
         Short: "Answer is a minimalist open source Q&A community.",
         Long: `Answer is a minimalist open source Q&A community.
 To run answer, use:
     - 'answer init' to initialize the required environment.
     - 'answer run' to launch application.`,
     }
 ​

意义:管理子命令

rootCmd 可以理解为一个容器,管理所有子命令

例如 git 命令的根节点是 gitgit commit 是其子命令

可以没有 run 方法

  • 如果 rootCmd 不定义 Run,直接运行程序时会提示错误并要求指定子命令
  • 但如果已经有子命令,可以不定义 run 方法

rootCmd 的 init 函数

1.1 全局命令参数:PersistentFlags

也叫持久化标志

通过 PersistentFlags 定义的参数可被所有子命令继承,常用于全局配置(如日志级别、配置文件路径等)

所有子命令都可以通过 --xxxFlag 标志修改该参数

 func init() {
     // ...
   rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
   rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
   rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
   rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
   rootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
     // 绑定参数到 Viper(便于配置文件覆盖)
   viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
   viper.BindPFlag("projectbase", rootCmd.PersistentFlags().Lookup("projectbase"))
   viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
   viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
   viper.SetDefault("license", "apache")
 }

PersistentFlags returns the persistent FlagSet specifically set in the current command.

这里其实是借助 PFlag 包进行参数解析的

StringVar:字符串变量绑定
 func (f *FlagSet) StringVar(p *string, name string, value string, usage string)
 // p:指向要绑定的字符串变量的指针。
 // name:标志的名称,用户在命令行中使用 --name 的形式来设置该标志。
 // value:标志的默认值。
 // usage:标志的使用说明,会在帮助信息中显示。

示例:

 var (
     configFilePath string
 )
 ​
 // 定义了一个名为 config 的标志,其默认值为 ./config.yaml,并将其绑定到 configFilePath 变量上
 // 用户可以通过 --config 标志来指定不同的配置文件路径
 func init() {
     rootCmd.PersistentFlags().StringVar(&configFilePath, "config", "./config.yaml", "Path to the configuration file")
 }
 // 使用时,因为是变量,直接访问即可
     rootCmd = &cobra.Command{
         Use:   "myapp",
         Short: "A simple app",
         Run: func(cmd *cobra.Command, args []string) {
             fmt.Printf("Config file path: %s\n", configFilePath)
         },
     }
StringVarP:额外支持短名称

StringVar 的扩展版本,额外支持了短名称

可以看到,在 StringVar 的基础上,参数多了一个 shorthand 短名称

 func (f *FlagSet) StringVarP(p *string, name, shorthand string, value string, usage string)
 // p:指向要绑定的字符串变量的指针。
 // name:标志的长名称,用户在命令行中使用 --name 的形式来设置该标志。
 // shorthand:标志的短名称,用户可以使用 -s 的形式来设置该标志。
 // value:标志的默认值。
 // usage:标志的使用说明,会在帮助信息中显示。

示例:

 var (
     configFilePath string
 )
 // 可以使用 --config 或 -c 来设置配置文件的路径
 rootCmd.PersistentFlags().StringVarP(&configFilePath, "config", "c", "./config.yaml", "Path to the configuration file")
StringP:动态获取

不直接绑定到变量,需要在命令执行时通过 cmd.Flag() 方法来获取标志的值,适用于需要动态获取标志值的场景。

 func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string
 // name:标志的长名称,用户在命令行中使用 --name 的形式来设置该标志。
 // shorthand:标志的短名称,用户可以使用 -s 的形式来设置该标志。
 // value:标志的默认值。
 // usage:标志的使用说明,会在帮助信息中显示。
 // 返回一个指向存储标志值的变量的指针

示例:

 func init() {
     rootCmd.PersistentFlags().StringP("config", "c", "./config.yaml", "Path to the configuration file")
 }
 // run 函数中动态获取参数值
 var rootCmd = &cobra.Command{
     Use:   "myapp",
     Short: "A simple app",
     Run: func(cmd *cobra.Command, args []string) {
         configFilePath := cmd.Flag("config").Value.String()
         fmt.Printf("Config file path: %s\n", configFilePath)
     },
 }
其他方法

还支持 :

  • IntVarIntVarPIntP
  • BoolVarBoolVarPBoolPBool
  • Float ...

用法都是类似的

函数名带P说明支持短选项,否则不支持短选项。

函数名带Var说明是将标志的值绑定到变量,否则是将标志的值存储在指针中。

BindPFlag:Viper 配置绑定到标志

这里绑定的意思是:

  • Viper 中对应的 value,就是该标志的默认值
  • 用户在命令行中使用 --xxx 该标志时,自动修改 Viper 的值(不影响配置文件本身)
 func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error
 // key:这是 Viper 中用于存储配置值的键名。你可以通过 viper.Get(key) 等方法来获取该键对应的值。
 // flag:这是一个指向 pflag.Flag 类型的指针,代表 Cobra 定义的命令行标志。
 // 通常可以通过 rootCmd.PersistentFlags().Lookup(flagName) 方法来获取指定名称的标志对象。
 ​
 // 如果绑定成功,函数返回 nil;
 // 如果出现错误,如标志对象为空或绑定过程中发生其他异常,函数将返回相应的错误信息。

示例:

通常搭配 rootCmd.PersistentFlags().Lookup 来获取到对应的标志对象

     // 绑定参数到 Viper(便于配置文件覆盖)
     // 当用户在命令行中使用 --author 标志指定作者姓名时,该值会被自动存储到 Viper 的 author 键中
     // 如果 Viper 从配置文件或其他来源读取到 author 键的值,也会影响到该命令行标志的默认值。
     viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
 ​
     viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")

通过 viper.Get() 获取该标志的值

1.2 结合 Viper 获取配置文件

 func init() {
     cobra.OnInitialize(initConfig)
     // ...
 }
 ​
 func initConfig() {
   // Don't forget to read config either from cfgFile or from home directory!
   if cfgFile != "" {
     // Use config file from the flag.
     viper.SetConfigFile(cfgFile)
   } else {
     // Find home directory.
     home, err := homedir.Dir()
     if err != nil {
       fmt.Println(err)
       os.Exit(1)
     }
 ​
     // Search config in home directory with name ".cobra" (without extension).
     viper.AddConfigPath(home)
     viper.SetConfigName(".cobra")
   }
 ​
   if err := viper.ReadInConfig(); err != nil {
     fmt.Println("Can't read config:", err)
     os.Exit(1)
   }
 }

1.3 预执行逻辑:PersistentPreRunE

 func init() {
     // ... 
     // 添加全局预执行钩子 
     rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
         // 检查必要环境变量 
         if os.Getenv("API_KEY") == "" {
             return fmt.Errorf("必须设置 API_KEY 环境变量")
         }
         
         // 初始化数据库连接池 
         db, err := initDatabase(viper.GetString("db.url")) 
         if err != nil {
             return err 
         }
         // 注入到全局上下文 
         cmd.SetContext(context.WithValue(cmd.Context(), "db", db))
         return nil 
     }
 }

2、Excecute 方法:调用 rootCmd

我们还需要一个main函数来调用rootCmd,通常我们会创建一个main.go文件,在main.go中调用rootCmd.Execute()来执行命令

main.go中不建议放很多代码,通常只需要调用cmd.Execute()即可

 func Main() {
     log.SetLogger(zap.NewLogger(
         log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath)))
     Execute()
 }
 ​
 // Execute adds all child commands to the root command and sets flags appropriately.
 // This is called by main(). It only needs to happen once to the rootCmd.
 func Execute() {
   if err := rootCmd.Execute(); err != nil {
     fmt.Println(err)
     os.Exit(1)
   }
 }

3、添加子命令:AddCommand

除了rootCmd,我们还可以调用AddCommand添加其他命令,通常情况下,我们会把其他命令的源码文件放在cmd/目录下。

示例如下,创建cmd/version.go文件添加一个version命令

 func init() {
   rootCmd.AddCommand(versionCmd)
 }
 ​
 var versionCmd = &cobra.Command{
   Use:   "version",
   Short: "Print the version number of Hugo",
   Long:  `All software has versions. This is Hugo's`,
   Run: func(cmd *cobra.Command, args []string) {
     fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
   },
 }

本地标志:子命令特有的标志 Flags

本地标志只能在它所绑定的命令上使用:

 # 只能在 rootCmd 上使用 --source,子命令也不行
 rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

设置标志为必选:MarkFlagRequired

默认情况下,标志是可选的,我们也可以设置标志为必选,当设置标志为必选,但是没有提供标志时,Cobra会报错。

 rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
 rootCmd.MarkFlagRequired("region")

标志组:约束标志的组合方法

标志组提供多个标志组合使用时的约束方法

 // MarkFlagsRequiredTogether
 // 如果用户提供了 `--username` 标志,则必须同时提供 `--password` 标志
 rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)")
 rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)")
 rootCmd.MarkFlagsRequiredTogether("username", "password")
 ​
 // MarkFlagsMutuallyExclusive
 // 二者冲突,只能提供其一
 rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
 rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
 rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
 ​
 // MarkFlagsOneRequired
 // 二者至少提供一个
 rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON")
 rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
 rootCmd.MarkFlagsOneRequired("json", "yaml")
 // 搭配 MarkFlagsMutuallyExclusive 使用 , 即二者必须提供一个
 rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")

标志小结

  • 在 rootCmd 可以通过 PersistentFlags 设置全局标志,所有命令通用
  • 在子Cmd 可以通过 Flags 设置特有的标志,该子命令独享该标志
  • 标志可以与 Viper 进行绑定
  • 可以通过 MarkFlagRequired 使标志必选
  • 可以通过设置标志组,对多个标志组合进行约束

代码组织结构

1、聚合

所有子命令都放在同一个文件中,如下:

 var (
     // rootCmd represents the base command when called without any subcommands
     rootCmd = &cobra.Command{
         Use:   "answer",
         Short: "Answer is a minimalist open source Q&A community.",
         Long: `Answer is a minimalist open source Q&A community.
 To run answer, use:
     - 'answer init' to initialize the required environment.
     - 'answer run' to launch application.`,
     }
 ​
     // runCmd represents the run command
     runCmd = &cobra.Command{
         Use:   "run",
         Short: "Run the application",
         Long:  `Run the application`,
         Run: func(_ *cobra.Command, _ []string) {
             cli.FormatAllPath(dataDirPath)
             fmt.Println("config file path: ", cli.GetConfigFilePath())
             fmt.Println("Answer is starting..........................")
             runApp()
         },
     }

此时,可以在 init 函数中 AddCommand

 func init() {
     rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time)
 ​
     rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
 ​
     dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
     
     // ...
     // 统一 add
     for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd} {
         rootCmd.AddCommand(cmd)
     }
 }
2、分散

每个文件对应一个子命令

 // 各自在 init 里 AddCommand
 func init() {
   rootCmd.AddCommand(versionCmd)
 }

方法二:Cobra Generator 更简单的方式

For complete details on using the Cobra generator, please refer to The Cobra-CLI Generator README

安装 cobra-cli

 go install github.com/spf13/cobra-cli@latest
 # Go will automatically install it in your $GOPATH/bin directory which should be in your $PATH.

Cobra Generator 只有两个操作

1、init:初始化项目骨架

创建一个新的骨架项目

 cobra-cli init [app]

使用示例:

 cd $HOME/code 
 mkdir myapp
 cd myapp
 go mod init github.com/spf13/myapp
 ​
 cd $HOME/code/myapp
 # automatically setup viper
 # 还支持 author license 
 cobra-cli init --viper
 go run main.go

2、add:添加命令

默认情况下,以 rootCmd 作为父命令

 cobra-cli add greet

这将在 cmd 目录下创建一个名为 greet.go 的文件,该文件包含了 greet 命令的基本结构。

 // $ greet.go
 // greetCmd represents the greet command
 var greetCmd = &cobra.Command{
     Use:   "greet",
     Short: "A brief description of your command",
     Long: `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.`,
     Run: func(cmd *cobra.Command, args []string) {
         fmt.Println("greet called")
     },
 }
 ​
 func init() {
     rootCmd.AddCommand(greetCmd)
 ​
     // Here you will define your flags and configuration settings.
 ​
     // Cobra supports Persistent Flags which will work for this command
     // and all subcommands, e.g.:
     // greetCmd.PersistentFlags().String("foo", "", "A help for foo")
 ​
     // Cobra supports local flags which will only run when this command
     // is called directly, e.g.:
     // greetCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
 }
添加子命令:-p

greet 命令创建一个名为 morning 的子命令

 cobra-cli add morning -p "greetCmd"

这将在 cmd 目录下创建一个名为 morning.go 的文件,并将 morning 命令作为 greet 命令的子命令。

注意事项
  • 命令命名,使用驼峰命名法,比如 cobra-cli add addUser,不能用下划线
  • Generator 实现上,会将 greet 命令代码命为 greetCmd(自动加 Cmd)

配置 Cobra Generator

提供公共信息

 # ~/.cobra.yaml file:
 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.

参考文档

github.com/spf13/cobra…

github.com/spf13/cobra…

Go 语言项目开发实战