基于golang语言开发命令行工具(command line interfaces,CLI)最常用的框架是Cobra。通过Cobra可以使用简单的接口实现一个强大现代的CLI工具,许多知名的项目比如Docker、Kubernetes等都用Cobra来开发自己的命令行工具。下面我们将对Cobra的基本用法做简要介绍。
- 概览
- 相关概念
- Commands(命令)
- Flags(标志)
- 安装
- 开始使用
- 使用Cobra生成器
- 使用Cobra库
- 使用Flags
- 位置和自定义参数
- 示例
- Help Command
- 展示Usage
- 预处理和后处理等Hooks函数
- 处理 unknown command 的建议
- 生成命令行文档
- 实现命令自动补全
概览
Cobra提供简单的接口来创建强大的现代化CLI接口,比如git与go。Cobra同时也提供一个二进制工具,用于创建命令行程序。
Cobra提供:
- 简单的子命令模式: 如
app server
,app fetch
等 - flags兼容posix模式(包括长、短版本)
- 支持子命令嵌套
- 支持全局、局部以及继承falgs
- 智能提示,如
app srver
,将提示srver子命令不存在,是否为app server
- 自动生成子命令及其flags
- 自动生成
-h
,--help
等flags提醒 - 自动生成命令行docs和man文件
- 支持命令行别名
- 灵活定义help和usage信息
- 可与viper库结合使用,方便参数、配置和环境变量的管理
相关概念
Cobra的构建基于结构化的commands,arguments和flags,即命令、参数和标志。
Commands代表命令行程序执行什么命令,Args(即arguments)和Flags即这些命令的修饰符。
最好的命令行程序应该像读句子那样去使用,用户通过命令行本身就可以自然地知道这段命令执行的是什么操作。
一个典型的命令行设计模式类似APPNAME COMMAND ARG --FLAG
或APPNAME VERB NOUN --ADJECTIVE
。
例如下面的例子,‘server’是一个操作, ‘port’是一个标志:
hugo server --port=1313
下面的另一个例子告诉我们,它要通过Git来执行clone操作拷贝url对应的项目到本地的裸仓库:
git clone URL --bare
Commands(命令)
Command是一个命令行程序最重要的概念。命令行程序支持的每一个交互操作都应该被包含在一个command中。一条command可以有多条子commands并且能够可选地执行它们。
在上面的例子中, ‘server’就是一条command。
Flags(标志)
一个Flag用来控制command的行为。Cobra完全兼容POSIX的flags模式,如同Go自带的标准flag库那样。一条Cobra生成的command可以将它的flags继承给它的子commadn,也可以限定这些flags只能被该command使用。
在上面的例子中,‘port’即一个flag。
更加强大的flag功能可以使用pflag库,它是一个标准flag库的扩展。
安装
使用Cobra十分简单。首先通过go get
安装最新版本的代码库及相关依赖,这条命令同时也会安装cobra
可执行程序:
go get -u github.com/spf13/cobra/cobra
接下来,在golang代码中引用Cobra:
import "github.com/spf13/cobra"
开始使用
通常一个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()
}
使用Cobra生成器
Cobra提供一个命令行程序cobra
来快速生成你想要的任何命令行程序的框架。本文不介绍Cobra生成器的使用,关于该工具的详细信息点击此处。
使用Cobra库
使用Cobra,需要创建一个main.go文件和一个rootCmd文件。当然,你也可以选择别的地方去添加额外的commands。
创建rootCmd
Cobra没有构造函数,所以直接简单地创建一个command对象就行。
这里假设下面的代码位于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 http://hugo.spf13.com`,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
你也可以在init()函数中定义和处理flags和配置。
例如 cmd/root.go:
package cmd
import (
"fmt"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
// Used for flags.
cfgFile string
userLicense string
rootCmd = &cobra.Command{
Use: "cobra",
Short: "A generator for Cobra based Applications",
Long: `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.`,
}
)
// Execute executes the root command.
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
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.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)
}
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
er(err)
}
// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
创建main.go
在cmd/root.go文件中我们已经定义了一个名为rootCmd的command作为根,为了执行该command,还需要将其放入main.go文件中执行。
在一个Cobra程序中,main.go文件通常只干一件事,即初始化Cobra并执行command。
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
添加其他command
不同的command通常分别在不同的go文件中定义,这里假设所有command的实现都处于cmd/目录下不同的文件中。
例如,如果你想创建名为version的command,可以创建cmd/version.go文件,并在文件里这么写:
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")
},
}
使用Flags
Flags提供控制command行为的能力。
给command绑定flags
一般情况下,我们需要预先定义变量来存储flags的值,这样便于我们各处使用它们(Cobra也支持不显式地定义和使用flags,但这种情况我们后面再说)。
如下,我们定义了Verbose和Source两个不同类型的变量来表示和存储flags值。
var Verbose bool
var Source string
Cobra有两种不同的方式给command绑定flags,分别为Persistent Flags和Local Flags。
Persistent Flags( 持久型flags)
Persistent Flags表示flag不仅绑定在一个command上,同时也绑定在了这个command的子command上。Persistent Flags可以被一个command下的所有子command使用。
例如,我们给根command(即rootCmd)绑定了一个名为‘verbose’的Persistent Flag,那么这个flag就成了一个全局flag,可以被所有command使用(因为rootCommand为根command,显然后续所有增加的command都为子command)。
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
Local Flags(本地flags)
Local Flags表示flag仅能被其绑定的command使用。
如下我们将名为'source'的flag绑定给一个名为localCmd的command,除了localCmd外,其他command无法接收到这个flag的值,即只有loalCmd能给‘Source’变量通过指定flag来赋值。
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
command定义的Local Flags只能由绑定了这些flags的command使用,但这里还有一种方便的方式,可以使得一个command所有的子command定的flags都绑定给他们的父command,只要在创建command时,指定TraverseChildren为true即可,这样父command在真正执行前会遍历其所有的子command来绑定flags。
command := cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
TraverseChildren: true,
}
给flags绑定配置
在使用命令行程序时不总会显式地提供flags的值,有时希望程序自己去环境变量、配置文件等地方自动寻找配置参数。这时我们可以通过viper库来实现这一功能,将flags的值绑定给viper。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"))
}
在上面例子中我们将author这个flag绑定给了viper,viper一般会按照flag、环境变量、配置文件和默认值的优先级去寻找环境变量,当用户没有通过--author
给出flag值时,viper会降低优先级去别处寻找匹配的参数值。
Required flags
默认情况下,是否指定flags是可选的,如果你希望当一个flag没有设置时,命令行报错,你可以标记它为必须的
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
位置和自定义参数
验证和限制命令行程序的位置参数可以通过Command
的Args
字段来实现。
Cobra内置有下列验证函数给Args
字段使用:
-
NoArgs
- 不允许有位置参数 -
ArbitraryArgs
- 可以接受任意多个位置参数 -
OnlyValidArgs
- 只允许指定Command
在ValidArgs
字段里指定的位置参数 -
MinumumNArgs(int)
- 限定最少提供多少个位置参数 -
MaximumNArgs(int)
- 限定最多能提供多少个位置参数 -
ExactArgs(int)
- 限定必须提供多少个对应的位置参数 -
ExcatValidArgs(int)
- 限定必须提供多少个对应的位置参数,并且位置参数必须位于ValidArgs
字段 -
RangeArgs(min, max)
- 限定位置参数的个数必须处于某一个区间内
以下为一个限制位置参数的例子:
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!")
},
}
示例
目前为止我们已经介绍了很多Cobra的基本用法,本节会给出一个完整的例子来回顾前面讲过的知识。
在下面的例子中,我们定义了3个commands。两个commands为顶级命令,一个command为顶级命令的子命令。在这个例子中,由于rootCmd
没有为Run字段提供方法,所以单独的root是不能运行的,必须要有子commands。
注意这里我们只为一个名为echoTimes的command 设置了flag。更多flags的用法参考https://github.com/spf13/pflag。
package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
func main() {
var echoTimes int
var cmdPrint = &cobra.Command{
Use: "print [string to print]",
Short: "Print anything to the screen",
Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Print: " + strings.Join(args, " "))
},
}
var cmdEcho = &cobra.Command{
Use: "echo [string to echo]",
Short: "Echo anything to the screen",
Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Echo: " + strings.Join(args, " "))
},
}
var cmdTimes = &cobra.Command{
Use: "times [string to echo]",
Short: "Echo anything to the screen more times",
Long: `echo things multiple times back to the user by providing
a count and a string.`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
var rootCmd = &cobra.Command{Use: "app"}
rootCmd.AddCommand(cmdPrint, cmdEcho)
cmdEcho.AddCommand(cmdTimes)
rootCmd.Execute()
}
如果想要参考更完整和大型的例子,请点击Hugo。
Help Command
当你的程序有子命令时,Cobra 会自动给你程序添加help命令。当你运行app help
,会调用help命令。另外,help同样支持其它输入命令。例如,你有一个没有任何其它配置的命令叫create
,当你调用app help create
Corbra 将会起作用。
例子
下面的输出是Cobra自动生成的,除了command和flag的定义,我们没有对command做任何其他定制。
$ cobra help
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.
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.
help 就跟其它命令一样,并没有特殊的逻辑或行为。事实上,你也可以提供你自己定义的help。
自定义Help
你可以使用下面的函数来定义自己的help:
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)
后两个函数定义的help会被command的子命令继承。
展示Usage
当用户错误的使用命令行程序时(如指定非法的flag和command),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函数或模板给 Cobra 使用。
类似于自定义help,usage的方法和模板都会覆盖默认的公共说明。
cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)
版本Flag
当顶级Command的Version字段被定义后,Cobra会自动为顶级command添加一个--version
flag。当命令行程序指定该flag时,Cobra会调用内置的版本函数打印命令行程序的相关版本信息。当然,你也可以使用cmd.SetVersionTemplate(s string)
函数来自定义版本信息的展示内容。
预处理和后处理等Hooks函数
Cobra提供了多个钩子(hooks)函数的接口,你可以很容易地去决定在command执行Run中的实际函数之前或之后,需要执行哪些方法。
PersistentPreRun
和PreRun
函数会在Run
之前执行。
PersistentPostRun
和PostRun
函数将会在Run
之后执行。
Persistent*Run
这种模式的函数会被子command继承。
钩子函数的执行顺序如下:
PersistentPreRun
PreRun
Run
PostRun
PersistentPostRun
如下的例子使用了上面提到的所有钩子函数。需要注意的是,当子command执行时,它会执行根command的PersistentPreRun
函数而不会执行根command的PersistentPostRun
函数(因为子command自己定义了该函数)。
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]
处理 unknown command 的建议
Cobra在unknown command错误发生时,会自动打印建议。这就让Cobra的处理错误行为的方式类似git
命令那样。例如:
$ hugo srever
Error: unknown command "srever" for "hugo"
Did you mean this?
server
Run 'hugo --help' for usage.
建议会基于注册的子命令自动生成。使用了Levenshtein distance的实现。每一个模糊匹配的命令间隔为2个字符。
如果你希望在你的命令里,禁用建议或减小字符串的距离,使用:
command.DisableSuggestions = true
或
command.SuggestionsMinimumDistance = 1
你也可以通过SuggestFor
来给命令提供明确的名词建议。这个特性允许当字符串不相近,但是意思与你的命令相近时,提供指定的命令建议,比如:
$ kubectl remove
Error: unknown command "remove" for "kubectl"
Did you mean this?
delete
Run 'kubectl help' for usage.
生成命令行文档
Cobra可以基于command、flags等来自动生成文档,支持下面几种格式:
实现命令自动补全
Cobra还提供了自动生成bash或zsh自动补全脚本的功能,这部分参见