title: "Golang Library Cobra Commanline Tool"
date: 2021-01-12T20:10:31+08:00
draft: true
tags: ['golang','cobra']
author: "dadigang"
author_cn: "大地缸"
personal: "http://www.real007.cn"
关于作者
http://www.real007.cn/about
golang 命令行解析库cobra的使用
关于go语言的命令行解析,标准库flag提供的功能比较少,不能满足我的使用需求,所以就需要寻找第三方解决方案了.我选择cobra是因为它支持
sub command子命令,满足我的使用需求,而且被很多大公司使用(比如github cli),安全性应该没什么问题.
Overview
cobra提供简单的接口用于创建类似于git或者go官方工具的命令行工具
cobra可以快速创建基于cobra的应用的脚手架.
- 简单的基于子命令的命令行接口:
app server
,app fetch
等等 - 完全兼容POSIX的flag(包括短和长版本)
- 嵌套子命令
- 全局,本地和级联flag
- 快速生成应用脚手架
cobra init appname
&cobra add cmdname
- 智能提示功能 (
app srver
... did you meanapp server
?) - 自动基于command和flag生成帮助
- 自动添加帮助flag
-h
,--help
- 自动添加shell自动补全功能(bash, zsh, fish, powershell)
- 自动生成帮助文档
- command 别名功能
- 灵活定制help,usage等等
- 可选 集成viper用于构建12-factor app
Concept
cobra基于结构化的command,arguments和flag进行构建
Commands 代表行动
Args 表示事物
Flags 是行动的修饰符
最好的应用使用起来像读语句一样,用户会自然而然地知道如何使用这个应用
遵循的原则是
APPNAME VERB NOUN --ADJECTIVE.
或者APPNAME COMMAND ARG --FLAG
在接下来的例子里, server
是一个command,port
是一个flag
hugo server --port=1313
下一个命令我们在告诉git,从目标url 克隆一个裸仓库(只拷贝.git子目录)
git clone URL --bare
Commands
command是应用的核心,每次应用的互动都会包含一个command,一个command可以存在可选的子command,在这个例子hugo server --port=1313
里,server就是一个command
More about cobra.Command
Flags
flag是修改command行为的一种方式,cobra提供完全POSIX兼容的flag就像gp官方命令行工具一样.
cobra的command可以定义影响所有子命令的flag或者只影响一个命令的command
.在这个例子里,hugo server --port=1313
port是一个flag
flag功能由pflag库提供.
Installing
使用go get下载最新版
go get -u github.com/spf13/cobra
导入项目:
import "github.com/spf13/cobra"
Getting Started
典型的项目结构
▾ appName/
▾ cmd/
add.go
your.go
commands.go
here.go
main.go
通常main行数比较简单,起到初始化cobra的作用
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
Using the Cobra Generator
cobra自身提供的程序可以帮助你创建app和添加command,这是最简单的添加cobra到你的应用程序的方式
Here you can find more information about it.
Using the Cobra Library
手动实现cobra,你需要创建一个main.go和rootCmd文件,你需要视情况添加额外的command
Create rootCmd
cobra不需要构造函数,简单地创建你的command即可.
通常把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 http://hugo.spf13.com`,
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函数中定义flag或者修改配置
For example cmd/root.go:
package cmd
import (
"fmt"
"os"
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 ")
viper.SetDefault("license", "apache")
rootCmd.AddCommand(addCmd)
rootCmd.AddCommand(initCmd)
}
func er(msg interface{}) {
fmt.Println("Error:", msg)
os.Exit(1)
}
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())
}
}
Create your main.go
定义好root command后,你需要在main函数里执行execute
在cobra app中,通常main.go文件中的内容比较少,只用于初始化cobra
In a Cobra app, typically the main.go file is very bare. It serves, one purpose, to initialize Cobra.
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
Create additional commands
可以添加新的command,通常我们把每个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")
},
}
Returning and handling errors
If you wish to return an error to the caller of a command, RunE
can be used.
如果你想针对command的调用返回一个error,可以使用RunE
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
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
},
}
这样在execute函数调用的时候,error就会被捕获
Working with Flags
通过flag传入参数可以控制comand的执行行为
Assign flags to a command
由于flag定义后,可以在很多不同的地方被使用,我们需要定义一个变量来接受flag
var Verbose bool
var Source string
Persistent Flags
persistent(持久性) flag,可以用于分配给他的命令以及所有子命令.
下面例子里的verbose作用是让命令行输出的信息更详细,所有的子命令都会支持 verbos flag
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
Local Flags
local flag ,本地分配,只会分配给那个特定的command
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
Local Flag on Parent Commands
默认情况下 cobra只会在目标command的基础上解析local flag,它的parent command会忽略解析.开启Command.TraverseChildren
选项后,cobra就会执行目标command之前,在每个command上解析local command
command := cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
TraverseChildren: true,
}
Bind Flags with Config
你也可以使用viper绑定flag
var author string
func init() {
rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}
在这个persistent flag的例子中persistent flag author
,用viper进行了绑定, 注意,当用户没有使用 --author
flag时,就不会从配置文件中读取值
More in viper documentation.
Required flags
Flags are optional by default. If instead you wish your command to
report an error when a flag has not been set, mark it as required:
默认情况下flag时可选的,如果你希望缺失flag的时候报错,可以标记为Required flags,如下
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
或者对于PersistentFlag来说
rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkPersistentFlagRequired("region")
Positional and Custom Arguments
位置参数的验证,可以使用commnad的 args字段
下列验证器是内置的:
-
NoArgs
- 如果没有任何参数,command会报错 -
ArbitraryArgs
- command接受任何参数 -
OnlyValidArgs
- 如果有任何位置参数不在 validArgs的范围里面, command会报错 -
MinimumNArgs(int)
- 当位置参数的个数少于n时,command 会报错 -
MaximumNArgs(int)
- 当位置参数的个数大于n时,,command会报错 -
ExactArgs(int)
- 当位置参数的个数不正好是n个时,command会报错 -
ExactValidArgs(int)
- 当不是正好有n个位置参数或者有任何位置参数不属于ValidArgs,command会报错. -
RangeArgs(min, max)
- 如果参数的个数不是在min和max之间,command会报错
关于自定义验证器的一个例子:
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!")
},
}
Example
在下面的例子里,定义了3个command,其中有两个时最高级别的command,另一个(cmdTimes)时其中一个的子command.在这个例子里,root不能执行意味着需要subcommand,这是通过不给rootCmd提供Run参数来实现的.
More documentation about flags is available at https://github.com/spf13/pflag
执行go run main.go echo dasda dasdaa dadsa
, 执行go run main.go print dasada dasdafesf fasfdasf
times是echo的子命令所以执行命令是这样的go run main.go echo times -t 10 dsadas dasda dasda
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()
}
For a more complete example of a larger application, please checkout Hugo.
Help Command
当你有一个新的subcommand,cobra会自动为你的应用添加help command.当用户执行app help
时,会调用默认的help command.并且help支持所有其他输入的command.每个command都会自动添加--help
flag
Example
下面的输出是由cobra自动生成的. 除了command和flag你不需要其他东西就能生成help 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就和其他command一样,没有特殊的逻辑或行为.如果你想的话也可以自定义help.
Defining your own help
你可以提供自己的和help command或者你自己的模板,只需要使用下面的函数
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)
The latter two will also apply to any children commands.
后两个,还可以提供给子command
Usage Message
当用户提供了一个无效的flag或者command,cobra会回复一份usage
Example
默认的help,也嵌入了一份usage作为输出
下面是遇到无效flag,输出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.
Defining your own usage
你可以提供自己的usage函数或者模板,这样就会覆盖公用模板了.
cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)
Version Flag
如果你的root command上有一个version字段,cobra会添加一个--version
flag. 使用--version
flag会使用version模板打印版本到输出.模板可以通过cmd.SetVersionTemplate(s string)
函数来自定义
PreRun and PostRun Hooks
在你main函数里的run函数运行之前和之后,也是可以运行函数的.PersistentPreRun
和PreRun
函数会在Run
之前执行,PersistentPostRun
和 PostRun
会在Run
之后执行. 符合Persistent*Run
格式的函数如果子命令没有定义自己的Run,就会继承.这些函数的执行顺序如下:
PersistentPreRun
PreRun
Run
PostRun
PersistentPostRun
下面是一个两个comand运用了以上所有feature的例子.当子command执行的时候,会执行root command的PersistentPreRun
函数而不是root command的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()
}
Output:
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]
Suggestions when "unknown command" happens
当unknow command错误发生时,cobra会自动打印建议.这使得cobra拥有和git命令行类似的效果
$ hugo srever
Error: unknown command "srever" for "hugo"
Did you mean this?
server
Run 'hugo --help' for usage.
建议会基于每个子command的注册自动生成,实现基于 Levenshtein distance.每个注册的command匹配到最小两个字母的时候,就会显示建议
如果你想要取消建议功能,或者调整字符的距离:
command.DisableSuggestions = true
or
command.SuggestionsMinimumDistance = 1
你也可以设置可能会被建议的command的名字,使用SuggestFor
属性.这对那些字符串距离不太近的单词可以起到效果,但是注意不要使用那些你会用来作为别名的名字
$ kubectl remove
Error: unknown command "remove" for "kubectl"
Did you mean this?
delete
Run 'kubectl help' for usage.
Generating documentation for your command
cobra会基于你的 subcommand和flag生成文档.Read more about it in the docs generation documentation.
Generating shell completions
cobra能生成shell自动补全,Bash, Zsh, Fish, Powershell.Read more about it in Shell Completions.
关于cobra generator的使用
cobra用于创建命令行应用的脚手架工具
执行下面的命令安装
go get github.com/spf13/cobra/cobra
cobra init
创建应用初始代码,提供正确的项目结构,并且自动应用你给定的license
可以在当前应用运行,或者你可以指定一个相对路径,如果目录不存在,他会自己创建一个.
执行需要提供--pkg-name
,在非空的目录中也能成功执行
mkdir -p newApp && cd newApp
cobra init --pkg-name github.com/spf13/newApp
or
cobra init --pkg-name github.com/spf13/newApp path/to/newApp
cobra add
用于添加新的command,举个例子
- app serve
- app config
- app config create
在项目目录上执行以下命令即可
cobra add serve
cobra add config
cobra add create -p 'configCmd'
command采用驼峰式命名,不然可能会遇到错误. For example, cobra add add-user
is incorrect, but cobra add addUser
is valid.
Once you have run these three commands you would have an app structure similar to the following:
执行以上命令后的项目结构
▾ app/
▾ cmd/
serve.go
config.go
create.go
main.go
配置文件
提供配置文件可以避免每次使用提供一堆信息.
举个例子 ~/.cobra.yaml :
author: Steve Francia
license: MIT
你也可以使用其他内置license,比如 GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD.
也可以不使用证书,把license设置为none即可,或者你也可以自定义license
author: Steve Francia
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部分是用author和year两个属性生成的,
上面的例子生成的内容是:
Copyright © 2020 Steve Francia
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.
header也会在license头部被使用.
/*
Copyright © 2020 Steve Francia
This file is part of CLI application foo.
*/