Go 语言强大的命令行框架 Cobra 详解(kubernetes源码必需)

Cobra 是一个 Go 语言开发的命令行(CLI)框架,它提供了简洁、灵活且强大的方式来创建命令行程序。它包含:

  • 一个用于创建命令行程序的库(Cobra 库)—— k8s中就是引入使用的这个库

  • 一个用于快速生成基于 Cobra 库的命令行程序工具(Cobra 命令)

Cobra 是由 Go 团队成员 spf13 为 Hugo 项目创建的,并已被许多流行的 Go 项目所采用,如 Kubernetes、Helm、Docker (distribution)、Etcd 等。

官方网站:https://cobra.dev/

一、基础概念 

 Cobra 建立在命令、参数和标志这三个结构之上,要使用 Cobra 编写一个命令行程序,需要明确以下三个基本概念:

  • 命令(COMMAND):命令表示要执行的操作。

  • 参数(ARG):是命令的参数,一般用来表示操作的对象。

  • 标志(FLAG):是命令的修饰,可以调整操作的行为。

命令比较容易理解,但是参数和标识这两个特别容易混淆!

一个好的命令行程序在使用时读起来像句子,用户会自然的理解并知道如何使用该程序。

要编写一个好的命令行程序,需要遵循的模式是

APPNAME VERB NOUN --ADJECTIVE

APPNAME COMMAND ARG --FLAG。

在这里 VERB 代表动词,NOUN 代表名词,ADJECTIVE 代表形容词。

1、命令用例一:

$ hugo server --port=1313
# 其中:server 是一个命令(子命令),port 是一个标志(1313 是标志的参数,但不是命令的参数 ARG)。

2、命令用例二:

$ git clone URL --bare
# 其中:clone 是一个命令(子命令),URL 是命令的参数,bare 是标志

二、Cobra的快速开始 & 简单使用

Golang环境的安装就不说了,肯定是前提!

1、创建并初始化一个go项目:

[root@tcosmo-szls01 ~]# cd /jiguiquan/godata/
[root@tcosmo-szls01 godata]# mkdir mycobra
[root@tcosmo-szls01 godata]# cd mycobra/

[root@tcosmo-szls01 mycobra]# go mod init mycobra
go: creating new go.mod: module mycobra

2、安装 cobra 包:

# 根据官网手册直接安装的命令是错的,不知道何时会修改:
[root@tcosmo-szls01 mycobra]# go get -u github.com/spf13/cobra/cobra@v1.0.0

安装好后,就可以像其他 Go 语言库一样导入 Cobra 包并使用了!

import "github.com/spf13/cobra"

3、创建一个命令mycobra: 

 编写一个.go文件 mycobra/cmd/root.go ,其中自定义了一个命令:mycobra

package cmd

import (
  "fmt"
  "os"
  "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
  Use:   "mycobra",
  Short: "mycobra是吉桂权的第一个cobra应用",
  Long: `mycobra是吉桂权的第一个cobra应用,
         熟悉cobra框架,是为了更好地阅读kubernetes源码!`,
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("run mycobra...")
  },
}

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

cobra.Command 是一个结构体,代表一个命令,其各个属性含义如下:

  • Use:命令的名称;

  • Short:代表当前命令的简短描述;

  • Long:表示当前命令的完整描述;

  • Run:属性是一个函数,当执行命令时会调用此函数;

rootCmd.Execute() 是命令的执行入口,其内部会解析 os.Args[1:] 参数列表(默认情况下是这样,也可以通过 Command.SetArgs 方法设置参数),然后遍历命令树,为命令找到合适的匹配项和对应的标志。

4、创建程序入口 main.go:

package main

import (
    "mycobra/cmd"
)

func main() {
    cmd.Execute()
}

main.go 代码实现非常简单,只在 main 函数中调用了 cmd.Execute() 函数,来执行命令

5、编译并运行 mycobra 命令:

# 编译:
[root@tcosmo-szls01 mycobra]# go build -o mycobra

## 运行:
[root@tcosmo-szls01 mycobra]# ./mycobra 
run mycobra...

以上我们编译并执行了 mycobra 程序,输出内容正是 cobra.Command 结构体中 Run 函数内部代码的执行结果!

6、再使用 –help 查看这个程序的使用帮助:

# 查看帮助(--help或者):
[root@tcosmo-szls01 mycobra]# ./mycobra --help
[root@tcosmo-szls01 mycobra]# ./mycobra help
mycobra是吉桂权的第一个cobra应用,
         熟悉cobra框架,是为了更好地阅读kubernetes源码!

Usage:
  mycobra [flags]

Flags:
  -h, --help   help for mycobra
  
# hugo 命令用法为 mycobra [flags],如 mycobra --help。
# 这个命令行程序自动支持了 -h/--help 标志。

这里打印了 cobra.Command 结构体中 Long 属性的内容,如果 Long 属性不存在,则打印 Short 属性内容!

7、查看目前整个项目的代码结构:

[root@tcosmo-szls01 godata]# tree mycobra/
mycobra/
├── cmd
│   └── root.go
├── go.mod
├── go.sum
├── main.go
└── mycobra

总结:

以上就是使用 Cobra 编写一个命令行程序最常见的套路,这也是 Cobra 推荐写法。

Cobra 程序目录结构基本如此,main.go 作为命令行程序的入口,不要写过多的业务逻辑,所有命令都应该放在 cmd/ 目录下,以后不管编写多么复杂的命令行程序都可以这么来设计。

8、为了让我们的命令全局生效,我们在环境变量中添加一下我们命令的位置:

[root@tcosmo-szls01 mycobra]# vim /etc/profile.d/myenv.sh
# 内容如下:
export GOROOT="/usr/local/go19"
export GOPATH=/gopath
export PATH=$PATH:$GOROOT/bin:/jiguiquan/godata/mycobra

# 使生效:
[root@tcosmo-szls01 mycobra]# source /etc/profile

# 检验(任意位置):
[root@tcosmo-szls01 mycobra]# mycobra 
run mycobra...

三、中级用法(添加子命令、自动补齐)

 1、添加第一个子命令: 

 再创建一个 cmd/version.go 文件,内容如下:

package cmd

import (
  "fmt"
  "github.com/spf13/cobra"
)

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "版本号",
    Long:  `所有的软件都应该有版本信息,这是吉桂权的mycobra`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("v0.0.1")
    },
}

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

与定义 rootCmd 一样,我们可以使用 cobra.Command 定义其他命令,并通过 rootCmd.AddCommand() 方法将其添加为 rootCmd 的一个子命令。

重新编译运行:

[root@tcosmo-szls01 mycobra]# go build -o mycobra
[root@tcosmo-szls01 mycobra]# ./mycobra version
v0.0.1

我们再来看看 mycobra 的 help 信息(很明显多了个version命令):

[root@tcosmo-szls01 mycobra]# ./mycobra --help
mycobra是吉桂权的第一个cobra应用,
         熟悉cobra框架,是为了更好地阅读kubernetes源码!

Usage:
  mycobra [flags]
  mycobra [command]

Available Commands:
  help        Help about any command
  version     版本号

Flags:
  -h, --help   help for mycobra

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

我们进一步看看 version 这个子命令的 help 信息:

[root@tcosmo-szls01 mycobra]# ./mycobra version --help
所有的软件都应该有版本信息,这是吉桂权的mycobra

Usage:
  mycobra version [flags]

Flags:
  -h, --help   help for version

2、自动补全:

其实所谓的自动补全,是创建一个自动补全的命令,然后根据这个命令的提示,完成真正的自动补全的安装!

创建一个 cmd/completion.go 文件,内容如下(自己可以在官网或者github上找):

package cmd

import (
	"os"

	"github.com/abiosoft/colima/cmd/root"
	"github.com/spf13/cobra"
)

// completionCmd represents the completion command
func completionCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "completion [bash|zsh|fish|powershell]",
		Short: "Generate completion script",
		Long: `To load completions:
Bash:

  $ source <(colima completion bash)

  # To load completions for each session, execute once:
  # Linux:
  $ colima completion bash > /etc/bash_completion.d/colima
  # macOS:
  $ colima completion bash > /usr/local/etc/bash_completion.d/colima

Zsh:

  # If shell completion is not already enabled in your environment,
  # you will need to enable it.  You can execute the following once:

  $ echo "autoload -U compinit; compinit" >> ~/.zshrc

  # To load completions for each session, execute once:
  $ colima completion zsh > "${fpath[1]}/_colima"

  # You will need to start a new shell for this setup to take effect.

fish:

  $ colima completion fish | source

  # To load completions for each session, execute once:
  $ colima completion fish > ~/.config/fish/completions/colima.fish

PowerShell:

  PS> colima completion powershell | Out-String | Invoke-Expression

  # To load completions for every new session, run:
  PS> colima completion powershell > colima.ps1
  # and source this file from your PowerShell profile.
`,
		DisableFlagsInUseLine: true,
		ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
		Args:                  cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
		Run: func(cmd *cobra.Command, args []string) {
			switch args[0] {
			case "bash":
				_ = cmd.Root().GenBashCompletion(os.Stdout)
			case "zsh":
				_ = cmd.Root().GenZshCompletion(os.Stdout)
			case "fish":
				_ = cmd.Root().GenFishCompletion(os.Stdout, true)
			case "powershell":
				_ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
			}
		},
	}
	return cmd
}

func init() {
	root.Cmd().AddCommand(completionCmd())
}

当然,中间的英文描述,我们也可以自己汉化一下!

重新编译一下:

[root@tcosmo-szls01 mycobra]# go build -o mycobra
[root@tcosmo-szls01 mycobra]# ./mycobra -h
mycobra是吉桂权的第一个cobra应用,
         熟悉cobra框架,是为了更好地阅读kubernetes源码!

Usage:
  mycobra [flags]
  mycobra [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  version     版本号

Flags:
  -h, --help   help for mycobra

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

当然,此时还是没有实现自动补全的,只是多了个completion命令,不信你输入个 ./mycobra v 然后 tab 键看看是否实现了自动补全!^_^

3、如何使用自动补全功能? 

 好坏先执行一下 completion 子命令看看这个命令到底是干嘛的:

[root@tcosmo-szls01 mycobra]# ./mycobra completion
Generate the autocompletion script for mycobra for the specified shell.
See each sub-command's help for details on how to use the generated script.

Usage:
  mycobra completion [command]

Available Commands:
  bash        Generate the autocompletion script for bash
  fish        Generate the autocompletion script for fish
  powershell  Generate the autocompletion script for powershell
  zsh         Generate the autocompletion script for zsh

Flags:
  -h, --help   help for completion

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

哦,这个命令告诉了我们,可以支持 bash/fish/powershell/zsh 等类型的自动补全!

那我们肯定得弄清楚我们是哪一类?

[root@tcosmo-szls01 mycobra]# echo $0
-bash

原来我们是bash!

那我们就进一步地查看帮助文档:

[root@tcosmo-szls01 mycobra]# ./mycobra completion bash -h
Generate the autocompletion script for the bash shell.

This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.

To load completions in your current shell session:

	source <(mycobra completion bash)

To load completions for every new session, execute once:

#### Linux:

	mycobra completion bash > /etc/bash_completion.d/mycobra

#### macOS:

	mycobra completion bash > $(brew --prefix)/etc/bash_completion.d/mycobra

You will need to start a new shell for this setup to take effect.

Usage:
  mycobra completion bash

Flags:
  -h, --help              help for bash
      --no-descriptions   disable completion descriptions

哦豁,清除啦,我们还需要执行以下的一系列命令:

# 上面的提示说了,脚本是依赖于bash-completion的
yum install -y bash-completion

## 当前会话生效
source <(mycobra completion bash)

### 此时当前会话就已经生效了,但是我们想每一次都能支持,那么就还需要执行下面的命令:
mycobra completion bash > /etc/bash_completion.d/mycobra

#### 下面命令是我加的,刷新了下bash_completion
source /usr/share/bash-completion/bash_completion

此时,我们再来验证自动补齐是否生效:验证已生效!


四、关于命令行标识Flag

  • 持久标识:该标志将可用于它所分配的命令以及该命令下的所有子命令;(对于全局标志,可以定义在根命令 rootCmd 上)

  • 本地标识:该标志只适用于当前命令本身;

  • 必选标识:必须提供,如果不提供,则直接报错;(默认情况下,标志都是可选的)

  • 父命令的本地标识:默认情况下,Cobra 仅解析目标命令上的本地标志,忽略父命令上的本地标志;

    • 那如果我们想使用父命令的本地标识怎么办?—— 可以通过父命令的 TraverseChildren 属性为 true 实现传递。

    关于上面这几点,我将在下面的代码中一一解释!

1、修改 cmd/root.go ,在根命令中增加持久标识、本地标识、必选标识:

package cmd

import (
  "fmt"
  "os"
  "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
  Use:   "mycobra",
  TraverseChildren: true,
  Short: "mycobra是吉桂权的第一个cobra应用",
  Long: `mycobra是吉桂权的第一个cobra应用,
         熟悉cobra框架,是为了更好地阅读kubernetes源码!`,
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("run mycobra...")
    fmt.Printf("Verbose: %v\n", Verbose)
    fmt.Printf("Source: %v\n", Source)
    fmt.Printf("Region: %v\n", Region)
  },
}

// 持久标识
var Verbose bool
// 本地标识
var Source string
// 必选标识
var Region string

func init()  {
  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", "", "AWS region (required)")
  rootCmd.MarkFlagRequired("region")
}

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

在根命令中,我们定义了:

  • Verbose:持久标识

  • Source:本地标识

  • Region:必选标识

  • TraverseChildren:true(允许向子命令传递)

2、我们再定义一个 cmd/print.go 子命令:

package cmd

import (
  "fmt"
  "github.com/spf13/cobra"
)

var printCmd = &cobra.Command{
  Use: "print [OPTIONS] [COMMANDS]",
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("run print...")
    fmt.Printf("本地的printFlag: %v\n", printFlag)
    fmt.Printf("父命令中的Verbose: %v\n", Verbose)
    fmt.Printf("父命令中的Source: %v\n", Source)
  },
}

// 本地标志
var printFlag string

func init() {
  rootCmd.AddCommand(printCmd)
  printCmd.Flags().StringVarP(&printFlag, "flag", "f", "", "print flag for local")
}

在print子命令中,我们定义了:

  • printFlag:print子命令的本地标识

  • 期望打印出Source这个“父命令的本地标识”

3、执行编译,先看看新命令和标识都是否生效了:

[root@tcosmo-szls01 mycobra]# go build -o mycobra
[root@tcosmo-szls01 mycobra]# mycobra -h
mycobra是吉桂权的第一个cobra应用,
         熟悉cobra框架,是为了更好地阅读kubernetes源码!

Usage:
  mycobra [flags]
  mycobra [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  print       
  version     版本号

Flags:
  -h, --help            help for mycobra
  -r, --region string   AWS region (required)
  -s, --source string   Source directory to read from
  -v, --verbose         verbose output

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

可以看到,现在有4个命令,和3个新标识。(–help不算,是cobra自带的)

4、验证:必选标识 region 不提供的话会报错:

# 提供region标识,运行正常
[root@tcosmo-szls01 mycobra]# mycobra -v -s test-source -r test-region
run mycobra...
Verbose: true
Source: test-source
Region: test-region

# 不提供region标识,运行报错
[root@tcosmo-szls01 mycobra]# mycobra -v -s test-source
Error: required flag(s) "region" not set

5、在 TraverseChildren=true 时,父命令中的 持久标识/本地标识 都可以正常传递给子命令:

# 先看看print子命令的手册
[root@tcosmo-szls01 mycobra]# mycobra print -h
Usage:
  mycobra print [OPTIONS] [COMMANDS] [flags]

Flags:
  -f, --flag string   print flag for local
  -h, --help          help for print

Global Flags:
  -v, --verbose   verbose output
  
## 父命令中的 Verbose 和 Source 都可以传递给 print 子命令
[root@tcosmo-szls01 mycobra]# mycobra -v -s test-source print -f test-sub-flag
本地的printFlag: test-sub-flag
父命令中的Verbose: true
父命令中的Source: test-source

6、在 TraverseChildren=false 时,父命令中的 持久标识可以正常传递给子命令,但是本地标识不可以: 

 我们需要将根命令中的 TraverseChildren 属性值改为false,然后重新编译:

# 首先持久标识Verbose是可以正常传递的
[root@tcosmo-szls01 mycobra]# mycobra -v print -f test-sub-flag
本地的printFlag: test-sub-flag
父命令中的Verbose: true
父命令中的Source: 

## 但是父命令本地标识Resource是没办法传递给子命令的
[root@tcosmo-szls01 mycobra]# mycobra -v -s test-source print -f test-sub-flag
Error: unknown shorthand flag: 's' in -s


五、参数验证与钩子函数Hooks

1、参数验证:

在执行命令行程序时,我们可能需要对命令参数进行合法性验证,cobra.Command 中的 Args 属性提供了此功能;

Args 属性类型为一个函数:func(cmd *Command, args []string) error,可以用来验证参数;

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 之间,该命令将报错。

内置验证参数的用法如下:

// 修改 cmd/version.go 文件为例 
package cmd

import (
  "fmt"
  "github.com/spf13/cobra"
)

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "版本号",
    Long:  `所有的软件都应该有版本信息,这是吉桂权的mycobra`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("v0.0.1")
    },
    Args: cobra.MaximumNArgs(2), // 使用内置的验证函数,位置参数多于 2 个则报错
}

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

重新编译后测试:

# 传入2个参数,没有问题
[root@tcosmo-szls01 mycobra]# mycobra version a b
v0.0.1

## 传入大于2个参数,则报以下错误
[root@tcosmo-szls01 mycobra]# mycobra version a b c
Error: accepts at most 2 arg(s), received 3

当然,我们还可以自定义验证参数:

// 简单修改 cmd/print.go 为例
package cmd

import (
  "errors"
  "fmt"
  "github.com/spf13/cobra"
)

var printCmd = &cobra.Command{
  Use: "print [OPTIONS] [COMMANDS]",
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("run print...")
    // 命令行位置参数列表:例如执行 `hugo print a b c d` 将得到 [a b c d]
    fmt.Printf("args: %v\n", args)
  },
  // 使用自定义验证函数
  Args: func(cmd *cobra.Command, args []string) error {
    if len(args) < 1 {
      return errors.New("requires at least one arg")
    }
    if len(args) > 4 {
      return errors.New("the number of args cannot exceed 4")
    }
    if args[0] != "a" {
      return errors.New("first argument must be 'a'")
    }
    return nil
  },
}

// 本地标志
var printFlag string

func init() {
  rootCmd.AddCommand(printCmd)
  printCmd.Flags().StringVarP(&printFlag, "flag", "f", "", "print flag for local")
}

重新编译后测试:

# 正常输入参数在1~4之间,且以a开头,没有问题
[root@tcosmo-szls01 mycobra]# mycobra print a b c d
run print...
args: [a b c d]

## 如果参数不满足以上条件,则报错
[root@tcosmo-szls01 mycobra]# mycobra print b c d a
Error: first argument must be 'a'

2、钩子函数Hooks:

在执行 Run 函数前后,我么可以执行一些钩子函数,其作用和执行顺序如下:

  • PersistentPreRun:在 PreRun 函数执行之前执行,对此命令的子命令同样生效

  • PreRun:在 Run 函数执行之前执行。

  • Run:执行命令时调用的函数,用来编写命令的业务逻辑。

  • PostRun:在 Run 函数执行之后执行。

  • PersistentPostRun:在 PostRun 函数执行之后执行,对此命令的子命令同样生效

我们直接修改rootCmd来演示:

package cmd

import (
  "fmt"
  "os"
  "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
  Use:   "mycobra", 
  TraverseChildren: false, 
  Short: "mycobra是吉桂权的第一个cobra应用",
  Long: `mycobra是吉桂权的第一个cobra应用,
         熟悉cobra框架,是为了更好地阅读kubernetes源码!`,
  PersistentPreRun: func(cmd *cobra.Command, args []string) {
    fmt.Println("mycobra PersistentPreRun")
  },
  PreRun: func(cmd *cobra.Command, args []string) {
    fmt.Println("mycobra PreRun")
  },
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("run mycobra...")
    // fmt.Printf("Verbose: %v\n", Verbose)
    // fmt.Printf("Source: %v\n", Source)
    // fmt.Printf("Region: %v\n", Region)
  },
  PostRun: func(cmd *cobra.Command, args []string) {
    fmt.Println("mycobra PostRun")
  },
  PersistentPostRun: func(cmd *cobra.Command, args []string) {
    fmt.Println("mycobra PersistentPostRun")
  },
}

// 持久标识
var Verbose bool
// 本地标识
var Source string
// 必选标识
var Region string

func init()  {
  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", "", "AWS region (required)")
  //rootCmd.MarkFlagRequired("region")
}

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

重新编译后测试:

# 首先验证主命令本身
[root@tcosmo-szls01 mycobra]# mycobra 
mycobra PersistentPreRun
mycobra PreRun
run mycobra...
mycobra PostRun
mycobra PersistentPostRun

## 其中 PersistentPreRun、PersistentPostRun 两个函数对子命令同样生效。
[root@tcosmo-szls01 mycobra]# mycobra version 
mycobra PersistentPreRun
v0.0.1
mycobra PersistentPostRun

3、Hooks函数对应的<Hooks>E版本:

以上所有的Hooks函数都有对应的<Hooks>E,其中E代表Error,即可以支持抛出Error:

  • PersistentPreRunE

  • PreRunE

  • RunE

  • PostRunE

  • PersistentPostRunE

在rootCmd中增加 PersistentPreRunE 和 PreRunE 函数:

var rootCmd = &cobra.Command{
  Use:   "mycobra",
  TraverseChildren: false,
  Short: "mycobra是吉桂权的第一个cobra应用",
  Long: `mycobra是吉桂权的第一个cobra应用,
         熟悉cobra框架,是为了更好地阅读kubernetes源码!`,
  PersistentPreRun: func(cmd *cobra.Command, args []string) {
    fmt.Println("mycobra PersistentPreRun")
  },
  PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
    fmt.Println("mycobra PersistentPreRunE")
    return nil
  },
  PreRun: func(cmd *cobra.Command, args []string) {
    fmt.Println("mycobra PreRun")
  },
  PreRunE: func(cmd *cobra.Command, args []string) error {
    fmt.Println("mycobra PreRunE")     
    return errors.New("PreRunE err")
  },
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("run mycobra...")
    //fmt.Printf("Verbose: %v\n", Verbose)
    //fmt.Printf("Source: %v\n", Source)
    //fmt.Printf("Region: %v\n", Region)
  },
  PostRun: func(cmd *cobra.Command, args []string) {
    fmt.Println("mycobra PostRun")
  },
  PersistentPostRun: func(cmd *cobra.Command, args []string) {
    fmt.Println("mycobra PersistentPostRun")
  },
}

重新编译后测试:

[root@tcosmo-szls01 mycobra]# mycobra 
mycobra PersistentPreRunE
mycobra PreRunE
Error: PreRunE err
Usage:
  mycobra [flags]
  mycobra [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  print       
  version     版本号

Flags:
  -h, --help            help for mycobra
  -r, --region string   AWS region (required)
  -s, --source string   Source directory to read from
  -v, --verbose         verbose output

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

PreRunE err

根据以上的测试结果,我们可以得出以下结论:

  • 同时定义了 Hooks 和 它对应的 <Hooks>E 函数时,只会走 <Hooks>E 函数逻辑;

  • 当 <Hooks>E 函数不抛出 Error 而是 nil 时,和原 Hooks 函数效果一样;

  •  <Hooks>E 函数抛出 Error 后,程序会终止运行并打印错误信息;

补充说明:如果子命令 中定义了自己的 Hooks 或者 <Hooks>E,则可以覆盖父命令传递下来的 PersistentHooks


六、定义自己的help命令 + Usage Message

1、定义自己的help命令:

默认情况下,我们可以使用 hugo help command 语法查看子命令的帮助信息,也可以使用 hugo command -h/–help 查看!

# 三者效果基本相同
## 唯一的区别是,使用 help 命令查看帮助信息时会执行PersistentHooks钩子函数(因为help也是mycobra的子命令)
mycobra help version
mycobra version -h
mycobra version --help

如果我们对 Cobra 自动生成的帮助命令不满意,我们可以通过以下方法自定义帮助命令或模板:

// 后两者也适用于任何子命令
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)

我们可以使用 rootCmd.SetHelpCommand 来控制 help 命令输出,使用 rootCmd.SetHelpFunc 来控制 -h/–help 输出。

func Execute() {
  // 自定义了一个Command,赋值给HelpCommand,作为 help 命令的输出
  rootCmd.SetHelpCommand(&cobra.Command{
    Use:    "help",
    Short:  "Custom help command",
    Hidden: true,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("自定义的help命令输出")
    },
  })

  // 作为 -h/--help 的输出
  rootCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
    fmt.Println(strings) // 把参数都打印出来看看是啥
  })

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

重新编译后测试:

# help命令是会打印出父命令(root命令)的PersistentHooks的
[root@tcosmo-szls01 mycobra]# mycobra help version
mycobra PersistentPreRunE
自定义的help命令输出
mycobra PersistentPostRun

## -h/--help 标识,传入的strings其实是 命令 + 标识 + 参数列表(除主命令后的所有)
[root@tcosmo-szls01 mycobra]# mycobra version -h a b c d
[version -h a b c d]

2、使用 rootCmd.SetHelpTemplate 设置帮助信息模板:

// {{.UseLine}}: 获取当前命令的Use值
// {{.Short}}:   获取当前命令的Short值
// Commands:     遍历当前命令的子命令
func Execute() {
  rootCmd.SetHelpTemplate(`Custom Help Template:
    Usage:
      {{.UseLine}}
    Description:
      {{.Short}}
    Commands:
      {{- range .Commands}}
        {{.Name}}: {{.Short}}
      {{- end}}
  `)

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

重新编译后测试:

# 使用 -h/--help 标识
[root@tcosmo-szls01 mycobra]# mycobra -h
Custom Help Template:
    Usage:
      mycobra [flags]
    Description:
      mycobra是吉桂权的第一个cobra应用
    Commands:
        completion: Generate the autocompletion script for the specified shell
        help: Help about any command
        print: 
        version: 版本号

## 使用help子命令
[root@tcosmo-szls01 mycobra]# mycobra help
mycobra PersistentPreRunE
Custom Help Template:
    Usage:
      mycobra [flags]
    Description:
      mycobra是吉桂权的第一个cobra应用
    Commands:
        completion: Generate the autocompletion script for the specified shell
        help: Help about any command
        print: 
        version: 版本号
mycobra PersistentPostRun
  
### 使用help子命令打印version的帮助信息
[root@tcosmo-szls01 mycobra]# mycobra help version
mycobra PersistentPreRunE
Custom Help Template:
    Usage:
      mycobra version [flags]
    Description:
      版本号
    Commands:
mycobra PersistentPostRun

根据以上测试,我们得出以下结论:

  • 无论使用 help 命令查看帮助信息,还是使用 -h 查看帮助信息,其输出内容都遵循我们自定义的模版格式;

3、自定义自己的Usage Massage:

当用户提供无效标志或无效命令时,Cobra 通过向用户显示 Usage 来提示用户如何正确的使用命令;

这个输出格式默认与 help 信息一样,我们也可以进行自定义。Cobra 提供了如下两个方法,来控制输出:

cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)

直接在上面的代码中增加一个SetUsageFunc函数:

func Execute() {
  rootCmd.SetHelpTemplate(`Custom Help Template:
    Usage:
      {{.UseLine}}
    Description:
      {{.Short}}
    Commands:
      {{- range .Commands}}
        {{.Name}}: {{.Short}}
      {{- end}}
  `)

  rootCmd.SetUsageFunc(func(command *cobra.Command) error {
    return errors.New("自定义 Usage Message")
  })

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

重新编译后测试:执行一个不存在的标识:

[root@tcosmo-szls01 mycobra]# mycobra --demo
Error: unknown flag: --demo
Error: 自定义 Usage Message

另外的 SetUsageTemplate() 就不演示了,和上面的help类似!


七、未知命令提示

在我们使用 git 等完善的命令时,有一个非常好用的功能,能够对用户输错的未知命令智能提示:

[root@tcosmo-szls01 mycobra]# git stat
git: 'stat' is not a git command. See 'git --help'.

Did you mean one of these?
	status
	stage
	stash

这个功能非常实用,幸运的是,Cobra 自带了此功能!

不需要我们做任何事情,这个智能提示功能是自动开启的:

[root@tcosmo-szls01 mycobra]# mycobra vers
Error: unknown command "vers" for "mycobra"

Did you mean this?
	version

[root@tcosmo-szls01 mycobra]# mycobra pri
Error: unknown command "pri" for "mycobra"

Did you mean this?
	print

但是,如果你想彻底关闭此功能,可以使用如下设置:

command.DisableSuggestions = true

我觉得咱们应该不会主动去关闭这个优秀的功能吧!


八、生成文档

Cobra 支持生成 Markdown、ReStructured Text、Man Page 三种格式文档;

这里以生成 Markdown 格式文档为例,来演示下 Cobra 这一强大功能;

我们可以通过定义一个标志 md-docs 来决定是否生成文档。

// 普通本地标识,决定是否需要生成markdown文档
var MarkdownDocs bool

func init()  {
  rootCmd.Flags().BoolVarP(&MarkdownDocs, "md-docs", "m", false, "gen Markdown docs")
  ...
}

// 定义生成markdown文档的方法
func GenDocs() {
  if MarkdownDocs {
    if err := doc.GenMarkdownTree(rootCmd, "./docs/md"); err != nil {
      fmt.Println(err)
      os.Exit(1)
    }
  }
}

这些都完成后,在 main.go 入口函数中调用 GenDocs() 方法即可:

import (
  "fmt"
  "os"
  "github.com/spf13/cobra/doc"
  "errors"
   "github.com/spf13/cobra"
)

package main

import (
    "mycobra/cmd"
)

func main() {
    cmd.Execute()
    cmd.GenDocs()
}

重新编译后测试:

# 编译时,可能会遇到缺包问题,我是遇到以下两个(记录一下):
go get github.com/spf13/cobra/doc@v1.6.1
go get github.com/abiosoft/colima/cmd/root

## 以下两个命令选一个执行
[root@tcosmo-szls01 mycobra]# mycobra -m
[root@tcosmo-szls01 mycobra]# mycobra --md-docs
run mycobra...

将生成主命令及所有子命令的MarkDown文档:

[root@tcosmo-szls01 mycobra]# tree docs/md
docs/md
├── mycobra_completion_bash.md
├── mycobra_completion_fish.md
├── mycobra_completion.md
├── mycobra_completion_powershell.md
├── mycobra_completion_zsh.md
├── mycobra.md
├── mycobra_print.md
└── mycobra_version.md

jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐