golang 命令行解析庫cobra的使用
? 關(guān)于go語言的命令行解析,標(biāo)準(zhǔn)庫flag提供的功能比較少,不能滿足我的使用需求,所以就需要尋找第三方解決方案了.我選擇cobra是因為它支持 sub command子命令,滿足我的使用需求,而且被很多大公司使用(比如github cli),安全性應(yīng)該沒什么問題.
Overview
cobra提供簡單的接口用于創(chuàng)建類似于git或者go官方工具的命令行工具
cobra可以快速創(chuàng)建基于cobra的應(yīng)用的腳手架.
- 簡單的基于子命令的命令行接口:
app server
,app fetch
等等 - 完全兼容POSIX的flag(包括短和長版本)
- 嵌套子命令
- 全局,本地和級聯(lián)flag
- 快速生成應(yīng)用腳手架
cobra init appname
&cobra add cmdname
- 智能提示功能 (
app srver
... did you meanapp server
?) - 自動基于command和flag生成幫助
- 自動添加幫助flag
-h
,--help
- 自動添加shell自動補(bǔ)全功能(bash, zsh, fish, powershell)
- 自動生成幫助文檔
- command 別名功能
- 靈活定制help,usage等等
- 可選 集成viper用于構(gòu)建12-factor app
Concept
cobra基于結(jié)構(gòu)化的command,arguments和flag進(jìn)行構(gòu)建
Commands代表行動
Args表示事物
Flags 是行動的修飾符
最好的應(yīng)用使用起來像讀語句一樣,用戶會自然而然地知道如何使用這個應(yīng)用
遵循的原則是
APPNAME VERB NOUN --ADJECTIVE.
或者APPNAME COMMAND ARG --FLAG
在接下來的例子里, server
是一個command,port
是一個flag
hugo server --port=1313
下一個命令我們在告訴git,從目標(biāo)url 克隆一個裸倉庫(只拷貝.git子目錄)
git clone URL --bare
Commands
command是應(yīng)用的核心,每次應(yīng)用的互動都會包含一個command,一個command可以存在可選的子command,在這個例子hugo server --port=1313
里,server就是一個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
導(dǎo)入項目:
import "github.com/spf13/cobra"
Getting Started
典型的項目結(jié)構(gòu)
? appName/
? cmd/
add.go
your.go
commands.go
here.go
main.go
通常main行數(shù)比較簡單,起到初始化cobra的作用
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
Using the Cobra Generator
cobra自身提供的程序可以幫助你創(chuàng)建app和添加command,這是最簡單的添加cobra到你的應(yīng)用程序的方式
Here you can find more information about it.
Using the Cobra Library
手動實現(xiàn)cobra,你需要創(chuàng)建一個main.go和rootCmd文件,你需要視情況添加額外的command
Create rootCmd
cobra不需要構(gòu)造函數(shù),簡單地創(chuàng)建你的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函數(shù)中定義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 <EMAIL ADDRESS>")
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函數(shù)里執(zhí)行execute
在cobra app中,通常main.go文件中的內(nèi)容比較少,只用于初始化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,你可以創(chuàng)建cmd/version.go,然后像下面這個例子一樣填充內(nèi)容
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的調(diào)用返回一個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函數(shù)調(diào)用的時候,error就會被捕獲
Working with Flags
通過flag傳入?yún)?shù)可以控制comand的執(zhí)行行為
Assign flags to a command
由于flag定義后,可以在很多不同的地方被使用,我們需要定義一個變量來接受flag
var Verbose bool
var Source string
Persistent Flags
persistent(持久性) flag,可以用于分配給他的命令以及所有子命令.
下面例子里的verbose作用是讓命令行輸出的信息更詳細(xì),所有的子命令都會支持 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
默認(rèn)情況下 cobra只會在目標(biāo)command的基礎(chǔ)上解析local flag,它的parent command會忽略解析.開啟Command.TraverseChildren
選項后,cobra就會執(zhí)行目標(biāo)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進(jìn)行了綁定, 注意,當(dāng)用戶沒有使用 --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:
默認(rèn)情況下flag時可選的,如果你希望缺失flag的時候報錯,可以標(biāo)記為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
位置參數(shù)的驗證,可以使用commnad的 args字段
下列驗證器是內(nèi)置的:
-
NoArgs
- 如果沒有任何參數(shù),command會報錯 -
ArbitraryArgs
- command接受任何參數(shù) -
OnlyValidArgs
- 如果有任何位置參數(shù)不在 validArgs的范圍里面, command會報錯 -
MinimumNArgs(int)
- 當(dāng)位置參數(shù)的個數(shù)少于n時,command 會報錯 -
MaximumNArgs(int)
- 當(dāng)位置參數(shù)的個數(shù)大于n時,,command會報錯 -
ExactArgs(int)
- 當(dāng)位置參數(shù)的個數(shù)不正好是n個時,command會報錯 -
ExactValidArgs(int)
- 當(dāng)不是正好有n個位置參數(shù)或者有任何位置參數(shù)不屬于ValidArgs,command會報錯. -
RangeArgs(min, max)
- 如果參數(shù)的個數(shù)不是在min和max之間,command會報錯
關(guān)于自定義驗證器的一個例子:
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不能執(zhí)行意味著需要subcommand,這是通過不給rootCmd提供Run參數(shù)來實現(xiàn)的.
More documentation about flags is available at https://github.com/spf13/pflag
執(zhí)行go run main.go echo dasda dasdaa dadsa
, 執(zhí)行go run main.go print dasada dasdafesf fasfdasf
times是echo的子命令所以執(zhí)行命令是這樣的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
當(dāng)你有一個新的subcommand,cobra會自動為你的應(yīng)用添加help command.當(dāng)用戶執(zhí)行app help
時,會調(diào)用默認(rèn)的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或者你自己的模板,只需要使用下面的函數(shù)
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
當(dāng)用戶提供了一個無效的flag或者command,cobra會回復(fù)一份usage
Example
默認(rèn)的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函數(shù)或者模板,這樣就會覆蓋公用模板了.
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)
函數(shù)來自定義
PreRun and PostRun Hooks
在你main函數(shù)里的run函數(shù)運(yùn)行之前和之后,也是可以運(yùn)行函數(shù)的.PersistentPreRun
和PreRun
函數(shù)會在Run
之前執(zhí)行,PersistentPostRun
和 PostRun
會在Run
之后執(zhí)行. 符合Persistent*Run
格式的函數(shù)如果子命令沒有定義自己的Run,就會繼承.這些函數(shù)的執(zhí)行順序如下:
PersistentPreRun
PreRun
Run
PostRun
PersistentPostRun
下面是一個兩個comand運(yùn)用了以上所有feature的例子.當(dāng)子command執(zhí)行的時候,會執(zhí)行root command的PersistentPreRun
函數(shù)而不是root command的PersistentPostRun
函數(shù).
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
當(dāng)unknow command錯誤發(fā)生時,cobra會自動打印建議.這使得cobra擁有和git命令行類似的效果
$ hugo srever
Error: unknown command "srever" for "hugo"
Did you mean this?
server
Run 'hugo --help' for usage.
建議會基于每個子command的注冊自動生成,實現(xiàn)基于 Levenshtein distance.每個注冊的command匹配到最小兩個字母的時候,就會顯示建議
如果你想要取消建議功能,或者調(diào)整字符的距離:
command.DisableSuggestions = true
or
command.SuggestionsMinimumDistance = 1
你也可以設(shè)置可能會被建議的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自動補(bǔ)全,Bash, Zsh, Fish, Powershell.Read more about it in Shell Completions.
關(guān)于cobra generator的使用
cobra用于創(chuàng)建命令行應(yīng)用的腳手架工具
執(zhí)行下面的命令安裝
go get github.com/spf13/cobra/cobra
cobra init
創(chuàng)建應(yīng)用初始代碼,提供正確的項目結(jié)構(gòu),并且自動應(yīng)用你給定的license
可以在當(dāng)前應(yīng)用運(yùn)行,或者你可以指定一個相對路徑,如果目錄不存在,他會自己創(chuàng)建一個.
執(zhí)行需要提供--pkg-name
,在非空的目錄中也能成功執(zhí)行
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
在項目目錄上執(zhí)行以下命令即可
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:
執(zhí)行以上命令后的項目結(jié)構(gòu)
? app/
? cmd/
serve.go
config.go
create.go
main.go
配置文件
提供配置文件可以避免每次使用提供一堆信息.
舉個例子 ~/.cobra.yaml :
author: Steve Francia <spf@spf13.com>
license: MIT
你也可以使用其他內(nèi)置license,比如GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD.
也可以不使用證書,把license設(shè)置為none即可,或者你也可以自定義license
author: Steve Francia <spf@spf13.com>
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兩個屬性生成的,
上面的例子生成的內(nèi)容是:
Copyright ? 2020 Steve Francia <spf@spf13.com>
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 <spf@spf13.com>
This file is part of CLI application foo.
*/