Go应用构建工具(3)--cobra

Go应用构建工具(3)–cobra

1. 概述

Cobra是一个可以创建强大的现代化命令行应用的库,许多应用都使用了Cobra库构建应用,比如k8s,Hugo等;
Cobra同时也提供了一个脚手架,用来生成基于Cobra的应用框架。
Cobra是建立在commands,arguments和flags的结构上的,commands代表命令,args代表非选项参数,flags代表标志。
一个好的应用应该是易懂的,用户能够直观的与其交互,应用通常遵循这种模式:APPNAME VERB NOUN --ADJECTIVE或者APPNAME COMMAND ARG --FLAG
比如以下示例:

hugo server --port=1313

git clone URL --bare

这里VERB表示动词,NOUN表示名词,ADJECTIVE表示形容词

Cobra的使用提供了两种方式:使用Cobra库或者cobra-cli脚手架
两种方式都需要先安装:

  • cobra库: go install github.com/spf13/cobra/cobra@latest
  • cobra-cli:go install github.com/spf13/cobra-cli@latest

2. 快速使用

  1. 通用结构
    通常基于cobra的应用程序遵循以下的组织结构:
  ▾ appName/
    ▾ cmd/
        add.go
        your.go
        commands.go
        here.go
      main.go

其中main.go文件是很简洁的,它只做一件事,就是初始化cobra:

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}
  1. 快速使用
  • 首先按照上面的组织结构创建自己的项目
    Go应用构建工具(3)--cobra_第1张图片

  • 在cmd/root.go添加以下代码

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "mycli",
	Short: "mycli is a cobra test code",
	Long:  "mycli is a cobra test code,this is long description",
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	
  • 添加main.go
package main

import "cobrafast/cmd"

func main() {
	cmd.Execute()
}

  • 添加另外的命令,这里加一个version,在version.go写入以下代码:
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print the version number of mycli",
	Long:  "All software has versions. This is mycli's",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("V1.0.01")
	},
}

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

每个cobra都有一个根命令,然后它下面有很多子命令,这里的version就是其中一个子命令,使用AddCommand添加到根明了中

  • 测试验证
  1. cobra自动生成帮助信息
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ go run main.go -h
mycli is a cobra test code,this is long description

Usage:
  mycli [flags]
  mycli [command]

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

Flags:
  -h, --help   help for mycli

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

单个命令的帮助信息也自动生成了:

lucas@Z-NB-0406:~/workspace/test/cobra_fast$ go run main.go version -h
All software has versions. This is mycli's

Usage:
  mycli version [flags]

Flags:
  -h, --help   help for version

运行子命令:

lucas@Z-NB-0406:~/workspace/test/cobra_fast$  go run main.go version
V1.0.01

运行未定义的命令:

lucas@Z-NB-0406:~/workspace/test/cobra_fast$ go run main.go get
Error: unknown command "get" for "mycli"
Run 'mycli --help' for usage.
unknown command "get" for "mycli"
exit status 1
  1. 如果希望调用命令时将错误返回,可以使用RunE方法,错误信息会被Execute方法捕获
    这里将version.go的versionCmd的Run字段改为RunE:
var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print the version number of mycli",
	Long:  "All software has versions. This is mycli's",
	RunE: func(cmd *cobra.Command, args []string) error {
		// fmt.Println("V1.0.01")
		return errors.New("version error")
	},
}

运行子命令:

lucas@Z-NB-0406:~/workspace/test/cobra_fast$ go run main.go version
Error: version error
Usage:
  mycli version [flags]

Flags:
  -h, --help   help for version

version error
exit status 1

3. 核心特性

3.1 使用flag

Cobra使用pflag解析命令行选项,Cobra中有两种类型的flag:一种是持久化的flag,一种是本地flag。

  • 持久化flag
    定义它的命令和其子命令都可以使用这个flag。
  • 本地flag
    只能在定义它的命令中使用

以下用示例来学习这个特性:
root.go新增以下代码:

var output string

func init() {
	// 根命令定义一个持久化flag
	rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "output file name")
}

output这个flag是持久化的,子命令version可以使用

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print the version number of mycli",
	Long:  "All software has versions. This is mycli's",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("version: V1.0.01, output: %s\n", output)

	},
}

测试:

lucas@Z-NB-0406:~/workspace/test/cobra_fast$ go run main.go version -o test.txt
version: V1.0.01, output: test.txt
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ go run main.go version -h
All software has versions. This is mycli's

Usage:
  mycli version [flags]

Flags:
  -h, --help   help for version

Global Flags:
  -o, --output string   output file name

通过-h查看帮助信息能看到,version命令多了一个Global Flags
接下来,将root.go的持久化flag改为本地flag:

func init() {
	// 根命令定义一个本地flag
	rootCmd.Flags().StringVarP(&output, "output", "o", "", "output file name")
}

此时再查看version命令的帮助信息,就会发现没有output这个flag了:

lucas@Z-NB-0406:~/workspace/test/cobra_fast$ go run main.go version -h
All software has versions. This is mycli's

Usage:
  mycli version [flags]

Flags:
  -h, --help   help for version
  • cobra还提供了一种在解析子命令前能够先解析父命令的本地flag的方式
    默认情况是本地flag只能在定义它的命令上使用,但可以通过设置字段TraverseChildren,让cobra在执行目标命令先解析父命令的本地flag。
    文字描述的有点拗口,官方文档说明也比较简短,下面通过实验来说明会比较容易明白。
  1. 首先修改下代码,新增一个get命令,version为它的子命令,get命令定义一个本地flag
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var output string

var getCmd = &cobra.Command{
	Use:              "get",
	Short:            "Print output file name of mycli",
	Long:             "All software has name. This is mycli's",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("name: %s\n", output)

	},
}

func init() {
	getCmd.Flags().StringVarP(&output, "output", "o", "mycli", "output file name")

	rootCmd.AddCommand(getCmd)
}
  1. version命令代码,将version命令挂在get命令下:
var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "Print the version number of mycli",
	Long:  "All software has versions. This is mycli's",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("version: V1.0.01, output: %s\n", output)

	},
}
func init() {
	getCmd.AddCommand(versionCmd)
}
  1. 先测试下正常情况
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ ./cobrafast get version -o hello
Error: unknown shorthand flag: 'o' in -o
Usage:
  mycli get version [flags]

Flags:
  -h, --help   help for version

unknown shorthand flag: 'o' in -o
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ ./cobrafast get -o hello version
Error: unknown shorthand flag: 'o' in -o
Usage:
  mycli get version [flags]

Flags:
  -h, --help   help for version

unknown shorthand flag: 'o' in -o

常规情况下,-o是父命令get的本地flag,子命令version无法使用

  1. 修改代码(这里搞不懂跟我理解的不同,先看看代码)
    修改root.go,将TraverseChildren设置为true
var rootCmd = &cobra.Command{
	Use:              "mycli",
	Short:            "mycli is a cobra test code",
	Long:             "mycli is a cobra test code,this is long description",
	TraverseChildren: true,
}

然后再测试:

lucas@Z-NB-0406:~/workspace/test/cobra_fast$ go build .
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ ./cobrafast get  -o hello version
version: V1.0.01, output: hello
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ ./cobrafast get version -o hello
Error: unknown shorthand flag: 'o' in -o
Usage:
  mycli get version [flags]

Flags:
  -h, --help   help for version

unknown shorthand flag: 'o' in -o

这时,使用命令如./cobrafast get -o hello version,version命令就能先解析了get命令的本地flag了。
::: danger 注意
在这一块,我有几个点不是很能理解~

  1. 按照我的理解,应该是子命令里将TraverseChildren设置为true后,执行该命令时能解析父命令的flag才是,但实质上并不是,而是在root命令里添加;
  2. 基于上一点的问题,查看源码中判断TraverseChildren的地方,是在方法:ExecuteC中,有以下代码:
var flags []string
	if c.TraverseChildren {
		cmd, flags, err = c.Traverse(args)
	} else {
		cmd, flags, err = c.Find(args)
	}

这个方法的调用链是这样的:Execute()-> ExecuteC(),因为我们只在root.go中调用了Execute()方法,因而才需要在rootCmd中添加

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

也许是我个人理解的不对,希望有知道的朋友能告诉我哈
3. 对于命令的执行我也是有点不理解,我以为是可以将-o写在version命令之后,实则是不行,必须要在父命令后跟上父命令的本地flag,在写上子命令才可以成功执行。
:::

3.2 将flag绑定到配置

cobra使用的配置包是viper,我们可以将flag绑定到viper,这一部分与在viper中绑定flag是一样的了。

  1. 首先新建一个配置文件,config/config.yaml
the_author: zhangsan
  1. 修改root.go,添加viper的初始化
func initViper() {
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath("./config")

	if err := viper.ReadInConfig(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}
func init() {
	cobra.OnInitialize(initViper)
}
  1. 在get命令中绑定flag到viper,在执行命令时打印
var output string
var author string

var getCmd = &cobra.Command{
	Use:   "get",
	Short: "Print output file name of mycli",
	Long:  "All software has name. This is mycli's",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("name: %s, author: %s\n", output, viper.GetString("the_author"))
	},
}

func init() {
	getCmd.Flags().StringVarP(&output, "output", "o", "mycli", "output file name")
	getCmd.PersistentFlags().StringVar(&author, "author", "lucas", "Author name for copyright attribution")

	viper.BindPFlag("the_author", getCmd.PersistentFlags().Lookup("author"))

	rootCmd.AddCommand(getCmd)
}
  1. 运行测试
  • 没有指定flag时,读取的是配置文件的数据
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ ./cobrafast get
name: mycli, author: zhangsan
  • 指定–author时,读取的是命令行的值
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ ./cobrafast get --author lisi
name: mycli, author: lisi
  • 将配置文件的数据注释,也不指定–author,读取的就是定义flag设置的默认值了
lucas@Z-NB-0406:~/workspace/test/cobra_fast$ ./cobrafast get
name: mycli, author: lucas
3.3 设置flag为必选

默认情况下,flag是可选的,如果希望在某个flag没有被设置时候报错,则可以将它标记为必选的,使用MarkFlagRequired()MarkPersistentFlagRequired()
比如将output标记为必选的:

func init() {
	getCmd.Flags().StringVarP(&output, "output", "o", "mycli", "output file name")
	getCmd.PersistentFlags().StringVar(&author, "author", "lucas", "Author name for copyright attribution")

	getCmd.MarkFlagRequired("output")
    // getCmd.MarkPersistentFlagRequired("author")

	viper.BindPFlag("the_author", getCmd.PersistentFlags().Lookup("author"))

	rootCmd.AddCommand(getCmd)
}

那么在运行时,没有指定–output或-o,则会报错:

lucas@Z-NB-0406:~/workspace/test/cobra_fast$ ./cobrafast get
Error: required flag(s) "output" not set
Usage:
  mycli get [flags]
  mycli get [command]

Available Commands:
  version     Print the version number of mycli

Flags:
      --author string   Author name for copyright attribution (default "lucas")
  -h, --help            help for get
  -o, --output string   output file name (default "mycli")

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

required flag(s) "output" not set
3.4 flags组
  • 如果有不同的标志,必须一起提供(例如,–username标志,必须提供–password标志),那么Cobra可以强制要求,使用MarkFlagsRequiredTogether()
  • 如果有不同的标志,是互斥的,不能同时提供,比如输出格式只能是–json或–yaml,不能同时设置的情况,可以使用MarkFlagsMutuallyExclusive()

对于这两种情况:

  • 所有的本地flag和持久化flag都可以使用
  • 一个flag可以出现在多个组中
  • 一个组可以包含任意个flag
3.5 位置参数和自定义参数

在命令中,除了flag标志外,通常也会有参数Arg,并且有时需要对这些参数进行验证,cobra提供了一些内置的验证函数:

  1. NoArgs: 如果存在任何的参数,将会报错
  2. ArbitaryArgs:接受任意参数
  3. MinimumNArgs(int):接受至少N个参数,否则报错
  4. MaximumNArgs(int):接受至多N个参数,否则报错
  5. ExactArgs(int):只接收N个参数,如果个数不对,报错
  6. RangeArgs(min, max):参数个数在min和max之间,否则报错
  7. OnlyValidArgs:如果没有一个参数是符合command的ValidArgs字段指定的参数的话,就报错;这个要配合ValidArgs使用
  8. MatchAll:这个方法可以组合上面几种验证函数,比如可以要求参数个数必须为2个,而且需要满足指定的参数
var getCmd = &cobra.Command{
	Use:   "get",
	Short: "Print output file name of mycli",
	Long:  "All software has name. This is mycli's",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("name: %s, author: %s\n", output, viper.GetString("the_author"))

	},
	ValidArgs: []string{"hello", "word"},
	Args:      cobra.MatchAll(cobra.ExactArgs(2), cobra.OnlyValidArgs),
}
  1. 也可以自定义验证函数,实现方法:func(cmd *cobra.Command, args []string) error
    比如:
var cmd = &cobra.Command{
  Short: "hello",
  Args: func(cmd *cobra.Command, args []string) error {
    // Optionally run one of the validators provided by cobra
    if err := cobra.MinimumNArgs(1)(cmd, args); err != nil {
        return err
    }
    // Run the custom validation logic
    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!")
  },
}
3.6 钩子函数:PreRun和PostRun

cobra提供了一些钩子函数,在运行命令的Run函数时,PersistentPreRunPreRun函数会在Run函数执行之前执行,PersistentPostRunPostRun函数会在Run函数执行之后执行。
对于带有Persistent的函数,如果它的子命令没有实现这个函数,那么子命令将会继承父命令的Persistent*Run方法
这些钩子函数的执行顺序如下:

  1. PersistentPreRun
  2. PreRun
  3. Run
  4. PostRun
  5. PersistentPostRun
package main

import (
  "fmt"

  "github.com/spf13/cobra"
)

func main() {

  var rootCmd = &cobra.Command{
    Use:   "root [sub]",
    Short: "My root command",
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args)
    },
    PreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd Run with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args)
    },
  }

  var subCmd = &cobra.Command{
    Use:   "sub [no options!]",
    Short: "My subcommand",
    PreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd Run with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PersistentPostRun with args: %v\n", args)
    },
  }

  rootCmd.AddCommand(subCmd)

  rootCmd.SetArgs([]string{""})
  rootCmd.Execute()
  fmt.Println()
  rootCmd.SetArgs([]string{"sub", "arg1", "arg2"})
  rootCmd.Execute()
}

输出如下:

Inside rootCmd PersistentPreRun with args: []
Inside rootCmd PreRun with args: []
Inside rootCmd Run with args: []
Inside rootCmd PostRun with args: []
Inside rootCmd PersistentPostRun with args: []

Inside rootCmd PersistentPreRun with args: [arg1 arg2]
Inside subCmd PreRun with args: [arg1 arg2]
Inside subCmd Run with args: [arg1 arg2]
Inside subCmd PostRun with args: [arg1 arg2]
Inside subCmd PersistentPostRun with args: [arg1 arg2]
3.7 其他特性

cobra还支持其他比较有用的特性,比如:自定义Help命令,自定义帮助信息,添加–version标志,输入无效命令时的建议等,还可以生成命令的相关文档…

你可能感兴趣的:(Golang,golang)