简介

Cobra是一个库,提供了能够创建强大现代的CLI的接口。介绍的Viper是为了能够解决配置问题,其中包括命令行选项,是viper的一部分。

Cobra中是使用了pflag来解析命令行选项的,同样可以结合viper使用来读取命令行选项的配置。

Cobra提供了:

  • 简单的基于子命令的CLI:app server,app fetch等等。
  • 完全符合POSIX的flags(包括short和long版本)。
  • 嵌套子命令
  • 全局,局部和级联的flags
  • 智能建议(app srver did you mean app server?)
  • 自动帮助生成命令和标志
  • 自动帮助标志识别-h,-help,等。
  • 为您的应用程序自动生成shell自动完成(bash, zsh, fish, powershell)
  • 自动为你的应用生成帮助手册
  • 命令别名可以让你更改而不会损坏内容
  • 灵活定义help,usage等。
  • 无缝集成viper

概念

Cobra是建立在command、argument和flags的结构上的。

  • 命令是应用程序的中心点。应用程序支持的每个交互都包含在Command中。一个命令可以有子命令,也可以有选择地运行一个操作。
  • 标志是修改命令行为的一种方式。Cobra完全支持posix兼容标志以及Go的flag包。Cobra命令可以定义贯穿子命令的标志,以及仅对该命令可用的标志。

安装

使用go get命令安装包

go get -u github.com/spf13/cobra@latest

导入包后可以使用

import "github.com/spf13/cobra"

cobra-cli

cobra-cli是一个命令行程序,用于生成cobra应用程序和命令文件。它将引导您的应用程序脚手架,以快速开发基于cobra的应用程序。这是将Cobra合并到您的应用程序中最简单的方法。

Cobra提供了自己的程序,可以创建您的应用程序并添加您想要的任何命令。这是将Cobra合并到应用程序中最简单的方法。

首先安装这个工具,会自动安装在$GOPATH/bin

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

$GOPATH/bin是添加到$PATH中的,这样安装后cobra-cli命令就可用了。

cobra-cli init

cobra-cli init [app]命令将会创建并初始化你的应用程序代码。这是个强大的应用,会创建正确的结构无需担心,还可以将你指定的许可证应用到你的应用程序。

随着go的模块的引入,Cobra也利用模块简化了生成器,cobra的生成器需要在模块内使用。如果没有模块则可以初始化一个模块,例如:

cd $HOME/code 
mkdir myapp
cd myapp
go mod init github.com/spf13/myapp

在模块内部执行cobra-cli init就会创建一个新的项目骨架,可供修改。可以直接go run main.go运行这个骨架。

cd $HOME/code/myapp
cobra-cli init
go run main.go

cobra-cli还可以在子目录下运行,这可以方便将应用程序代码与库代码分离开。

可选的选项

  • --author提供作者名,例如cobra-cli init --author "Steve Francia [email protected]
  • --license提供许可证,例如cobra-cli init --license apache
  • --viper会自动安装viper

为项目添加命令

初始化cobra应用程序后,您可以继续使用cobra生成器向应用程序添加其他命令。执行此操作的命令是cobra-cli add。 如果要创建下面三个命令:

  • app server
  • app config
  • app config create 在你的项目目录(main.go所在目录)执行下面命令:
cobra-cli add serve
cobra-cli add config
cobra-cli add create -p 'configCmd'

最后一条命令的-p选项是用来给新增选项分配父命令的,因为create是config的子命令。默认情况下所有命令都是rootCmd的子命令。

默认情况下,cobra-cli会为命令名扩展Cmd。命令的命名风格是驼峰式的。

应用结构如下:

 app/
 cmd/
		config.go
		create.go
		serve.go
		root.go
	main.go

现在,您已经有了一个基本的基于cobra的应用程序并在运行。下一步是在cmd中编辑文件,并为您的应用程序定制它们。详情可以看下一节中对库的使用介绍。

配置cobra生成器

提供一个配置文件,可以让cobra生成器更加易用,可以用配置中的选项替代输入命令行选项。 ~/.cobra.yaml文件示例:

author: Steve Francia <[email protected]>
license: MIT
useViper: true

用户也可以指定自定义的许可证:

author: Steve Francia <[email protected]>
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.

其中的copyright会根据authoryear生成。header可以用作许可头文件,例如一个go文件中:

/*
Copyright © 2020 Steve Francia <[email protected]>
This file is part of CLI application foo.
*/

下面介绍cobra库的使用。

cobra-library

虽然可以提供你自己的组织结构,但是cobra的项目应用都遵循下面的结构:

 appName/
 cmd/
		add.go
		your.go
		commands.go
		here.go
	main.go

在一个cobra应用中,main.go文件是非常简单的,它的目的就只有一个,即初始化cobra。

package main
 
import (
  "{pathToYourApp}/cmd"
)
 
func main() {
  cmd.Execute()
}

初始化项目

首先需要创建一个go项目。具有一个简短的main.go和一个rootCmd文件。即

app/
	/cmd/
		root.go
	main.go

其中main.go就是上面所说的样子。

创建rootCmd

在app/cmd/下创建root.go,创建命令即可。

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 https://gohugo.io/documentation/`,
  Run: func(cmd *cobra.Command, args []string) {
    // Do Stuff Here
  },
}
 
func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

在init中定义选项并处理配置。后缀有Var表示绑定到某个变量,后缀有P表示有短标志

func init() {
 
    // Execute会回调initConfig函数
    cobra.OnInitialize(initConfig)
 
    // 有4个选项,利用cobra内置的pflag来解析命令行选项
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
    rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution")
    rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project")
    rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
 
  
 
    // 将命令行选项与viper结合读取配置
    viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
    viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
    viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
    viper.SetDefault("license", "apache")
 
	//添加子命令
    rootCmd.AddCommand(addCmd)
    rootCmd.AddCommand(initCmd)
}

cobra中的pflags可以用来解析命令行选项,viper也可以借此获得命令行选项的配置。initConfig就是用来初始化配置的,

func initConfig() {
	//选项设置了配置文件则使用选项的配置文件,选项中没有配置则使用默认配置文件
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        cobra.CheckErr(err)
 
        viper.AddConfigPath(home)
        viper.SetConfigType("yaml")
        viper.SetConfigName(".cobra")
    }
    // 环境变量中有匹配已注册的键的,可以加载到viper中
    viper.AutomaticEnv()
	// 读取配置
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    }
}

添加子命令

在上面的代码中,使用了AddCommand添加子命令,其实也可以在子命令对应的文件的init中将自己加入rootCmd,如:

package cmd
 
import (
  "fmt"
 
  "github.com/spf13/cobra"
)
 
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")
  },
}

如果希望向命令的调用者返回一个错误,那么使用RunE

func init() {
  rootCmd.AddCommand(tryCmd)
}
 
var tryCmd = &cobra.Command{
  Use:   "try",
  Short: "Try and possibly fail at something",
  RunE: func(cmd *cobra.Command, args []string) error {
    if err := someFunc(); err != nil {
	return err
    }
    return nil
  },
}

结合Flags

Flags提供了修饰符来控制命令的运行方式。

有两种方式来分配flag。

  1. persistent flags。这种类型的标志对命令和其子命令都是可用的。全部标记放在rootCmd下。
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
  1. local flags。这种类型标志只能在本命令中使用。
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

默认情况下,cobra只解析目标命令的局部命令,忽略父命令的局部标志。通过启用command.traversechildren,COBRA将在执行目标命令之前在每个命令上解析本地标志。

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

结合viper可以将从选项读取配置,可以注意,选项的配置的优先级比配置文件高

var author string
 
func init() {
  rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}

必要选项

选项默认都是可选的,如果希望选项没有设置的时候会报错,需要把选项标记为必选项

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

persistent flags同理。

rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkPersistentFlagRequired("region")

选项组

必须需要一起提供的选项,例如--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")

如果选项互斥,不能同时提供,例如指定输出格式--json或者--yaml,通过下面的方式设置。

rootCmd.Flags().BoolVar(&u, "json", false, "Output in JSON")
rootCmd.Flags().BoolVar(&pw, "yaml", false, "Output in YAML")
rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")

位置和自定义参数

位置参数的验证可以使用Command的Args字段指定。 下面是内置的验证器:

  • NoArgs:如果有任何位置参数,命令将会报告一个错误。
  • ArbitraryArgs :命令会接收任何参数
  • OnlyValidArgs :如果存在不在ValidArgs字段中的参数,命令就会报错。
  • MinimumNArgs(int) :如果没有至少N个位置参数,该命令将报告错误。
  • MaximumNArgs(int) : 如果位置参数超过N,该命令将报告错误。
  • ExactArgs(int) :如果没有确切的N个位置参数,该命令将报告一个错误。
  • ExactValidArgs(int) :如果没有确切的N个位置参数,或者有任何位置参数不在command的ValidArgs字段中,该命令将报告一个错误
  • RangeArgs(min, max) :如果参数的数量不在预期参数的最小和最大数量之间,该命令将报告一个错误。
  • MatchAll(pargs ...PositionalArgs) : 允许将现有的检查与任意的其他检查相结合(例如,你想要检查ExactArgs的长度和其他质量)。

一个自定义验证器的例子如下:

var cmd = &cobra.Command{
  Short: "hello",
  Args: func(cmd *cobra.Command, args []string) error {
    if len(args) < 1 {
      return errors.New("requires a color argument")
    }
    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!")
  },
}

help指令

当您有子命令时,Cobra会自动向您的应用程序添加help命令。此外,help还将支持所有其他命令作为输入。例如,您有一个名为“create”的命令,没有任何额外的配置。当“app help create”被调用时,Cobra将会工作。每个命令都会自动添加’——help标志。

默认的help命令没有具体的逻辑或行为。也可以自定义你想要的help命令。

自定义help

使用下面的函数来为默认命令提供自己的help命令和自己的模板。

cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)

后两个会应用到所有的子命令。

Usage信息

当用户提供无效标志或无效命令时,Cobra会向用户显示“Usage”。 例如下面的例子:

$ cobra --invalid
Error: unknown flag: --invalid
Usage:
  cobra [command]
 
Available Commands:
  add         Add a command to a Cobra Application
  help        Help about any command
  init        Initialize a Cobra Application
 
Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)
 
Use "cobra [command] --help" for more information about a command.

也可以自定义Usage函数和模板,就像help命令一样,通过下面的函数

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

Version选项

如果在rootCmd中设置了version字段,Cobra会添加一个顶级的--version标志。运行带有--version标志的应用程序将使用版本模板将版本打印到标准输出。模板可以通过cmd.SetVersionTemplate(s string)函数定制。

PreRun和PostRun Hooks

可能需要在命令的Run函数执行前或者后执行某些函数。PersistentPreRunPreRun函数会在Run之前执行。PersistentPostRunPostRun会在Run之后运行。Persistent*Run如果子命令没有声明会继承父命令的。执行的先后顺序如下:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

下面是使用所有这些特性的两个命令示例。当执行子命令时,它将运行root命令的PersistentPreRun,而不是root命令的PersistentPostRun。因为子命令没有声明PersistentPreRun,会继承父命令的,而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()
}

未知命令建议

当输入的命令无法识别,产生”unknown commands”错误,会给出建议的命令。

建议是自动基于每个注册的子命令和使用Levenshtein距离的实现。每个匹配最小距离为2(忽略大小写)的注册命令将作为建议显示。

可以禁用此功能,也可以调整距离

// 禁用
command.DisableSuggestions = true
// 调整最小距离
command.SuggestionsMinimumDistance = 1

您还可以使用SuggestFor属性显式地设置指定命令的名称。这允许对字符串距离不接近的字符串提出建议,例如为remote建议delete,虽然两个单词距离不接近,但是仍然很有意义。

$ kubectl remove
Error: unknown command "remove" for "kubectl"
 
Did you mean this?
        delete
 
Run 'kubectl help' for usage.

为命令生成文档

cobra可以基于子命令,flags等等来生成文档。生成的文档中会带有”Auto generated by spf13/cobra…”这样的句子,可以设置cmd.DisableAutoGenTag = true来移除这些句子。

生成Man Pages

设置root命令和man page头,指定生成路径即可,例如:

package main
 
import (
	"log"
 
	"github.com/spf13/cobra"
	"github.com/spf13/cobra/doc"
)
 
func main() {
	cmd := &cobra.Command{
		Use:   "test",
		Short: "my test program",
	}
	header := &doc.GenManHeader{
		Title: "MINE",
		Section: "3",
	}
	err := doc.GenManTree(cmd, header, "/tmp")
	if err != nil {
		log.Fatal(err)
	}
}

生成Markdown文档

使用方法差不多一致。

package main
 
import (
	"log"
 
	"github.com/spf13/cobra"
	"github.com/spf13/cobra/doc"
)
 
func main() {
	cmd := &cobra.Command{
		Use:   "test",
		Short: "my test program",
	}
	err := doc.GenMarkdownTree(cmd, "/tmp")
	if err != nil {
		log.Fatal(err)
	}
}

生成ReStructured Text文档

package main
 
import (
	"log"
 
	"github.com/spf13/cobra"
	"github.com/spf13/cobra/doc"
)
 
func main() {
	cmd := &cobra.Command{
		Use:   "test",
		Short: "my test program",
	}
	err := doc.GenReSTTree(cmd, "/tmp")
	if err != nil {
		log.Fatal(err)
	}
}

生成YAML文档

package main
 
import (
	"log"
 
	"github.com/spf13/cobra"
	"github.com/spf13/cobra/doc"
)
 
func main() {
	cmd := &cobra.Command{
		Use:   "test",
		Short: "my test program",
	}
	err := doc.GenYamlTree(cmd, "/tmp")
	if err != nil {
		log.Fatal(err)
	}
}

生成shell补全

Cobra可以为以下shell生成一个shell完成文件:bash、zsh、fish、PowerShell。这样只需要输入部分命令就可以输入tab获得匹配的完整命令。