Cobra使用详解

280 阅读9分钟

介绍

Cobra 是 Go 的 CLI 框架。它包含一个用于创建功能强大的现代 CLI 应用程序的库,以及一个用于快速生成基于 Cobra 的应用程序和命令文件的工具。

Cobra 由 Go 项目成员和 hugo 作者 spf13 创建,已经被许多流行的 Go 项目采用,比如 GitHub CLIDocker CLI

快速开始

按照官方文档,以hugo为例,快速构建一个简单的命令行程序,首先目录层次结构如下

➜  study_cobra tree .
.
├── cmd
│   └── hugo.go
├── go.mod
├── go.sum
└── main.go

cmd/hugo.go

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "hugo",
	Short: "short 描述",
	Long: "long 描述",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Run命令执行了!")
	},
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

这里针对上面的命令做一下说明

  • Use:表示的是命令的名称,一般在使用-h或者--help的帮助文档中的标识。当然我这里因为其实名称不太对,因为我的package包名并不是hugo(我打出来的叫study_cobra),所以默认build出来的二进制文件是和包名一致的,实际使用过程中,要保持一致。
  • Short:命令的简短的描述
  • Long:命令的完整描述,如果没有提供Long完整描述,则采用Short简短描述,如果说Long和Short都没写的话,那么在查看帮助文档的时候就不进行展示,当然不建议这么做。
  • Run:值为一个函数,这个函数会在我们执行主程序的时候进行调用。
  • Execute():主程序的入口,内部会针对os.Args[1:]进行解析,然后遍历命令树,为命令找到合适的匹配项和对应的flag标志。

那么这样的一个cmd就创建好了,接下来我们就在main函数中调用即可。main.go内容如下

package main

import "study_cobra/cmd"

func main() {
	cmd.Execute()
}

内容非常的简单,我们来简单运行一下

➜  study_cobra go build
➜  study_cobra ./study_cobra 
Run命令执行了!

# 这里可以看到其实同时支持-h和--help
➜  study_cobra ./study_cobra -h
long 描述

Usage:
  hugo [flags]

Flags:
  -h, --help   help for hugo
  
➜  study_cobra ./study_cobra --help
long 描述

Usage:
  hugo [flags]

Flags:
  -h, --help   help for hugo

添加一个子命令

在cmd目录下我们新建一个文件version.go

package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

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")
	},
}

func init() {
	rootCmd.AddCommand(versionCmd)
}

build以后运行

➜  study_cobra ./study_cobra -h     
long 描述

Usage:
  hugo [flags]
  hugo [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  version     Print the version number of Hugo

Flags:
  -h, --help   help for hugo

Use "hugo [command] --help" for more information about a command.

➜  study_cobra ./study_cobra version
Hugo Static Site Generator v0.9 -- HEAD

本次查看帮助信息,可以看到Usage除了之前的hugo [flags]之外又多了一个hugo [command]。下面为我们列出了可用的command。

  • completion:可以为制定的Shell生成自动补全的脚本,详见[[Cobra#Shell补全|Shell补全]]
  • help:有点类似-h/--help但是,还可以查看某个命令的使用方式,比如说hugo help version
  • version:刚刚添加的子命令,执行会执行我们在versionCmd的Run属性中定义的函数。

集成Flag标志

Cobra完美适配pflag,毕竟是一个人写的。可以结合pflag灵活的使用标志的功能

标志的作用是提供修饰符以控制命令的操作方式,如何理解?

比如我现在有一个终端工具叫dnssync,它的功能是刷新对应区域的dns缓存,那么它有一个标志位【flag】名称为region,简短写法是-r,那么根据-r传入的参数的不同,我可以起到刷新不同区域dns缓存的功能。比如`dnssync -r beijing`, `dnssync -r tianjin`等等。那么这里的-r仅仅是一个示例,还可以引入更多的标志位以提供更多更精准的修饰来控制命令的操作方式,比如说,引入`-d`,用来制定刷新某个域名的缓存,`-a`用来刷新哪一台DNS服务器的缓存等等。

因为标志可以在不同位置定义和使用,标志大概分为几类:

  • 持久标志:什么是持久的标志?持久标志除了可以作用于它分配的命令上,除此之外,还可以透传到该命令的所有子命令上。这意味着,你的子命令也可以使用这个标志。针对全局标志,可以将标志分配为根命令上的持久标志
  • 本地标志:本地标志只适用于该指定的命令。
  • 父命令上的本地标志:默认情况下,Cobra仅解析目标命令上的本地标志,而忽略父命令上的任何本地标志,通过启用Command.TraverseChildren,Cobra将在执行目标命令之前,解析每个命令上的本地标志
  • 必选标志:默认情况下,标志是可选的,即你可以选择不填,但是必填标志是必填,必填会报错。

接下来我们分别就这几个类型的标志进行演示。我们将version.go中的init初始化函数拿出来放到hugo.go中。

持久标志

var (
	Verbose bool
	rootCmd = &cobra.Command{
		Use:   "hugo",
		Short: "short 描述",
		Long:  "long 描述",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Run命令执行了!")
			fmt.Printf("verbose: %v\n", Verbose)
		},
	}
)

func init() {
	rootCmd.AddCommand(versionCmd)
	// 持久标志
	rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
}

如上代码所示,我们给rootCmd绑定了一个名为verbose的持久标志

➜  study_cobra go run main.go           
Run命令执行了!
verbose: false
➜  study_cobra go run main.go -v
Run命令执行了!
verbose: true

根据前文中定义,持久标志不仅可以作用于当前命令,也可以作用于该命令下的所有子命令,此时rootCmd有一个子命令为version,这个时候,我们来看一下子命令的执行结果

➜  study_cobra go run main.go version -h
All software has versions. This is Hugo's

Usage:
  hugo version [flags]

Flags:
  -h, --help   help for version

Global Flags:
  -v, --verbose   verbose output

这个时候查看帮助信息,我们可以在帮助内容中,看带一个Global Flags,这个就是我们通过持久标志带过来的一个标志。现在我们简单改写一下version的Run对应的函数。

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")
		fmt.Println("Verbose is", Verbose)
	},
}

然后再执行子命令

➜  study_cobra go run main.go version -v
Hugo Static Site Generator v0.9 -- HEAD
Verbose is true
➜  study_cobra go run main.go version   
Hugo Static Site Generator v0.9 -- HEAD
Verbose is false

就可以看到这个持久命令其实子命令也是可以使用的。

本地标志

本地标志默认就只用于绑定的命令了,这里我们再定义一个source的标志

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

var (
	Verbose bool
	Source  string

	rootCmd = &cobra.Command{
		Use:   "hugo",
		Short: "short 描述",
		Long:  "long 描述",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Run命令执行了!")
			fmt.Printf("verbose: %v\n", Verbose)
			fmt.Printf("source: %v\n", Source)
		},
	}
)

func init() {
	rootCmd.AddCommand(versionCmd)

	// 持久标志
	rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
	// 本地标志
	rootCmd.Flags().StringVarP(&Source, "source", "s", "", "source directory to read from")
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

再次执行帮助函数,我们就能看到标志位中,就多了一个-s/--source的标志位

➜ go run main.go -h      
long 描述

Usage:
  hugo [flags]
  hugo [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  version     Print the version number of Hugo

Flags:
  -h, --help            help for hugo
  -s, --source string   source directory to read from
  -v, --verbose         verbose output

Use "hugo [command] --help" for more information about a command.

➜ go run main.go -s mysource
Run命令执行了!
verbose: false
source: mysource

但是这个标志位在子的命令中是看不到的,因为这是一个本地标志,并且无法使用。

# 首先,这个flag标志在子命令中你是看不到的。
➜  go run main.go version -h
All software has versions. This is Hugo's

Usage:
  hugo version [flags]

Flags:
  -h, --help   help for version

Global Flags:
  -v, --verbose   verbose output

# 其次,你也无法使用
➜ go run main.go -s test-source version    
Error: unknown shorthand flag: 's' in -s
Usage:
  hugo version [flags]

Flags:
  -h, --help   help for version

Global Flags:
  -v, --verbose   verbose output

unknown shorthand flag: 's' in -s
exit status 1

上面的结果是Cobra的一个默认行为,但是我们可以通过在父命令上启用Command.TraverseChildren属性,允许Cobra在执行目标命令之前,解析每个命令的本地标志。

rootCmd = &cobra.Command{
	Use:   "hugo",
	Short: "short 描述",
	Long:  "long 描述",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Run命令执行了!")
		fmt.Printf("verbose: %v\n", Verbose)
		fmt.Printf("source: %v\n", Source)
	},
	TraverseChildren: true,
}

开启TraverseChildren以后,其实在使用帮助文档的时候,你依然是子命令中看不到这个参数

go run main.go -s test-source version -h 
All software has versions. This is Hugo's

Usage:
  hugo version [flags]

Flags:
  -h, --help   help for version

Global Flags:
  -v, --verbose   verbose output

但是此时已经不会报错了,而是可以直接使用了,我们对version命令稍作修改。

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")
    fmt.Println("Verbose is", Verbose)
    fmt.Println("Source is", Source) // 我改动了这里
  },
}

然后再执行

➜ go run main.go -s test-source version   
Hugo Static Site Generator v0.9 -- HEAD
Verbose is false
Source is test-source

如果versionCmd还有子命令的话,和这个原理其实是一样的。

必选标志

必选标志其实很好理解,必选标志其实就是要求你个标志是必填的,如果你没写就给你报错。来看如下这个例子

package cmd

import (
  "fmt"
  "os"

  "github.com/spf13/cobra"
)

var (
  Verbose bool
  Source  string
  Region  string // 新增一个region属性

  rootCmd = &cobra.Command{
    Use:   "hugo",
    Short: "short 描述",
    Long:  "long 描述",
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Run命令执行了!")
      fmt.Printf("verbose: %v\n", Verbose)
      fmt.Printf("source: %v\n", Source)
      fmt.Printf("region: %v\n", Region) // 在执行rootCmd的时候,打印这个region
    },
    TraverseChildren: true,
  }
)

func init() {
  rootCmd.AddCommand(versionCmd)

  // 持久标志
  rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
  // 本地标志
  rootCmd.Flags().StringVarP(&Source, "source", "s", "", "source directory to read from")
  // 必选标志,多了一个标记必填的步骤,注意这里的最后一个参数,一般如果是必填参数,我们最好是
  // 在帮助中说清楚,不然可能会给使用人造成混淆。
  rootCmd.Flags().StringVarP(&Region, "region", "r", "", "region to run in (required)")
  // 这里会多一步mark打标记的步骤
  rootCmd.MarkFlagRequired("region")
}

func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

接下来我们可以执行做一下测试

➜ go run main.go 
Error: required flag(s) "region" not set
Usage:
  hugo [flags]
  hugo [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  version     Print the version number of Hugo

Flags:
  -h, --help            help for hugo
  -r, --region string   region to run in (required)
  -s, --source string   source directory to read from
  -v, --verbose         verbose output

Use "hugo [command] --help" for more information about a command.

required flag(s) "region" not set
exit status 1

直接执行会告诉你说region not set

待续