Golang 之使用 Cobra
【導讀】go 命令行工具 cobra 非常強大易用,Kubernetes 源碼閱讀過程中也能見到這個框架。本文介紹了基於 cobra 的 cli 工具開發詳細說明。
Cobra 介紹
Cobra 是一個庫,其提供簡單的接口來創建強大現代的 CLI 接口,類似於 git 或者 go 工具。同時,它也是一個應用,用來生成個人應用框架,從而開發以 Cobra 爲基礎的應用。Docker 源碼中使用了 Cobra。
概念
Cobra 基於三個基本概念commands
,arguments
和flags
。其中 commands 代表行爲,arguments 代表數值,flags 代表對行爲的改變。
基本模型如下:
APPNAME VERB NOUN --ADJECTIVE
或者APPNAME COMMAND ARG --FLAG
例如:
# server是commands,port是flag
hugo server --port=1313
# clone是commands,URL是arguments,brae是flags
git clone URL --bare
- Commands
Commands 是應用的中心點,同樣 commands 可以有子命令 (children commands),其分別包含不同的行爲。
Commands 的結構體如下:
type Command struct {
Use string // The one-line usage message.
Short string // The short description shown in the 'help' output.
Long string // The long message shown in the 'help <this-command>' output.
Run func(cmd *Command, args []string) // Run runs the command.
}
- Flags
Flags 用來改變 commands 的行爲。其完全支持 POSIX 命令行模式和 Go 的 flag 包。這裏的 flag 使用的是 spf13/pflag 包,具體可以參考 Golang 之使用 Flag 和 Pflag.
安裝與導入
- 安裝
go get -u github.com/spf13/cobra/cobra
- 導入
import "github.com/spf13/cobra"
Cobra 文件結構
cjapp 的基本結構
▾ cjapp/
▾ cmd/
add.go
your.go
commands.go
here.go
main.go
main.go
其目的很簡單,就是初始化 Cobra。其內容基本如下:
package main
import (
"fmt"
"os"
"{pathToYourApp}/cmd"
)
func main() {
if err := cmd.RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
使用 cobra 生成器
windows 系統下使用:
go get github.com/spf13/cobra/cobra
或者在文件夾 github.com/spf13/cobra/cobra 下使用go install
在$GOPATH/bin
路徑下生成cobra.exe
可執行命令。
cobra init
命令cobra init [yourApp]
將會創建初始化應用,同時提供正確的文件結構。同時,其非常智能,你只需給它一個絕對路徑,或者一個簡單的路徑。
cobra.exe init cjapp
<<'COMMENT'
Your Cobra application is ready at
/home/chenjian/gofile/src/cjapp.
Give it a try by going there and running `go run main.go`.
Add commands to it by running `cobra add [cmdname]`.
COMMENT
ls -Ra /home/chenjian/gofile/src/cjapp
<<'COMMENT'
/home/chenjian/gofile/src/cjapp:
. .. cmd LICENSE main.go
/home/chenjian/gofile/src/cjapp/cmd:
. .. root.go
COMMENT
cobra add
在路徑C:\Users\chenjian\GoglandProjects\src\cjapp下分別執行:
cobra add serve
<<'COMMENT'
serve created at /home/chenjian/gofile/src/cjapp/cmd/serve.go
COMMENT
cobra add config
<<'COMMENT'
config created at /home/chenjian/gofile/src/cjapp/cmd/config.go
COMMENT
cobra add create -p 'configCmd'
<<'COMMENT'
create created at /home/chenjian/gofile/src/cjapp/cmd/create.go
COMMENT
ls -Ra /home/chenjian/gofile/src/cjapp
<<'COMMENT'
/home/chenjian/gofile/src/cjapp:
. .. cmd LICENSE main.go
/home/chenjian/gofile/src/cjapp/cmd:
. .. config.go create.go root.go serve.go
COMMENT
此時你可以使用:
go run main.go
<<'COMMENT'
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
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:
cjapp [command]
Available Commands:
config A brief description of your command
help Help about any command
serve A brief description of your command
Flags:
--config string config file (default is $HOME/.cjapp.yaml)
-h, --help help for cjapp
-t, --toggle Help message for toggle
Use "cjapp [command] --help" for more information about a command.
COMMENT
go run main.go config
<<'COMMENT'
config called
COMMENT
go run main.go serve
<<'COMMENT'
serve called
COMMENT
go run main.go config create
<<'COMMENT'
create called
COMMENT
cobra 生成器配置
Cobra 生成器通過~/.cjapp.yaml(Linux 下) 或者 $HOME/.cjapp.yaml(windows) 來生成 LICENSE。
一個. cjapp.yaml 格式例子如下:
author: Chen Jian <chenjian158978@gmail.com>
license: MIT
或者可以自定義 LICENSE:
license:
header: This file is part of {{ .appName }}.
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.
人工構建 Cobra 應用
人工構建需要自己創建main.go
文件和RootCmd
文件。例如創建一個 Cobra 應用cjappmanu
RootCmd 文件
路徑爲cjappmanu/cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var RootCmd = &cobra.Command{
Use: "chenjian",
Aliases: []string{"cj", "ccccjjjj"},
Short: "call me jack",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at https://o-my-chenjian.com`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("OK")
},
}
var cfgFile, projectBase, userLicense string
func init() {
cobra.OnInitialize(initConfig)
// 在此可以定義自己的flag或者config設置,Cobra支持持久標籤(persistent flag),它對於整個應用爲全局
// 在StringVarP中需要填寫`shorthand`,詳細見pflag文檔
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (defalut in $HOME/.cobra.yaml)")
RootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
RootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
RootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
RootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
// Cobra同樣支持局部標籤(local flag),並只在直接調用它時運行
RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
// 使用viper可以綁定flag
viper.BindPFlag("author", RootCmd.PersistentFlags().Lookup("author"))
viper.BindPFlag("projectbase", RootCmd.PersistentFlags().Lookup("projectbase"))
viper.BindPFlag("useViper", RootCmd.PersistentFlags().Lookup("viper"))
viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
viper.SetDefault("license", "apache")
}
func Execute() {
RootCmd.Execute()
}
func initConfig() {
// 勿忘讀取config文件,無論是從cfgFile還是從home文件
if cfgFile != "" {
viper.SetConfigName(cfgFile)
} else {
// 找到home文件
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 在home文件夾中搜索以“.cobra”爲名稱的config
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
// 讀取符合的環境變量
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
fmt.Println("Can not read config:", viper.ConfigFileUsed())
}
}
main.go
main.go
的目的就是初始化 Cobra
package main
import (
"fmt"
"os"
"cjappmanu/cmd"
)
func main() {
if err := cmd.RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
附加命令
附加命令可以在/cmd/
文件夾中寫,例如一個版本信息文件,可以創建/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 ChenJian",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Chen Jian Version: v1.0 -- HEAD")
},
}
同時,可以將命令添加到父項中,這個例子中 RootCmd 便是父項。只需要添加:
RootCmd.AddCommand(versionCmd)
處理 Flags
Persistent Flags
persistent 意思是說這個 flag 能任何命令下均可使用,適合全局 flag:
RootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
Local Flags
Cobra 同樣支持局部標籤 (local flag),並只在直接調用它時運行
RootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
Bind flag 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"))
}
Positional and Custom Arguments
Positional Arguments
Leagacy arg validation 有以下幾類:
-
NoArgs: 如果包含任何位置參數,命令報錯
-
ArbitraryArgs: 命令接受任何參數
-
OnlyValidArgs: 如果有位置參數不在 ValidArgs 中,命令報錯
-
MinimumArgs(init): 如果參數數目少於 N 個後,命令行報錯
-
MaximumArgs(init): 如果參數數目多餘 N 個後,命令行報錯
-
ExactArgs(init): 如果參數數目不是 N 個話,命令行報錯
-
RangeArgs(min, max): 如果參數數目不在範圍 (min, max) 中,命令行報錯
Custom Arguments
var cmd = &cobra.Command{
Short: "hello",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires at least one arg")
}
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!")
},
}
實例
將 root.go 修改爲以下:
package cmd
import (
"fmt"
"os"
"strings"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
var echoTimes int
var RootCmd = &cobra.Command{
Use: "app",
}
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("Print: " + strings.Join(args, " "))
},
}
var cmdTimes = &cobra.Command{
Use: "times [# 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, " "))
}
},
}
func init() {
cobra.OnInitialize(initConfig)
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
// 兩個頂層的命令,和一個cmdEcho命令下的子命令cmdTimes
RootCmd.AddCommand(cmdPrint, cmdEcho)
cmdEcho.AddCommand(cmdTimes)
}
func Execute() {
RootCmd.Execute()
}
func initConfig() {
// 勿忘讀取config文件,無論是從cfgFile還是從home文件
if cfgFile != "" {
viper.SetConfigName(cfgFile)
} else {
// 找到home文件
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// 在home文件夾中搜索以“.cobra”爲名稱的config
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
// 讀取符合的環境變量
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
fmt.Println("Can not read config:", viper.ConfigFileUsed())
}
}
操作如下:
go run main.go
<<'COMMENT'
Usage:
app [command]
Available Commands:
echo Echo anything to the screen
help Help about any command
print Print anything to the screen
version Print the version number of ChenJian
Flags:
-h, --help help for app
Use "app [command] --help" for more information about a command.
COMMENT
go run main.go echo -h
<<'COMMENT'
echo is for echoing anything back.
Echo works a lot like print, except it has a child command.
Usage:
app echo [string to echo] [flags]
app echo [command]
Available Commands:
times Echo anything to the screen more times
Flags:
-h, --help help for echo
Use "app echo [command] --help" for more information about a command.
COMMENT
go run main.go echo times -h
<<'COMMENT'
echo things multiple times back to the user by providing
a count and a string.
Usage:
app echo times [# times] [string to echo] [flags]
Flags:
-h, --help help for times
-t, --times int times to echo the input (default 1)
COMMENT
go run main.go print HERE I AM
<<'COMMENT'
Print: HERE I AM
COMMENT
go run main.go version
<<'COMMENT'
Chen Jian Version: v1.0 -- HEAD
COMMENT
go run main.go echo times WOW -t 3
<<'COMMENT'
Echo: WOW
Echo: WOW
Echo: WOW
COMMENT
自定義 help 和 usage
- help 默認的 help 命令如下:
func (c *Command) initHelp() {
if c.helpCommand == nil {
c.helpCommand = &Command{
Use: "help [command]",
Short: "Help about any command",
Long: `Help provides help for any command in the application.
Simply type ` + c.Name() + ` help [path to command] for full details.`,
Run: c.HelpFunc(),
}
}
c.AddCommand(c.helpCommand)
}
可以通過以下來自定義 help:
command.SetHelpCommand(cmd *Command)
command.SetHelpFunc(f func(*Command, []string))
command.SetHelpTemplate(s string)
- usage 默認的 help 命令如下:
return func(c *Command) error {
err := tmpl(c.Out(), c.UsageTemplate(), c)
return err
}
可以通過以下來自定義 help:
command.SetUsageFunc(f func(*Command) error)
command.SetUsageTemplate(s string)
先執行與後執行
Run 功能的執行先後順序如下:
-
PersistentPreRun
-
PreRun
-
Run
-
PostRun
-
PersistentPostRun
錯誤處理函數
RunE 功能的執行先後順序如下:
-
PersistentPreRunE
-
PreRunE
-
RunE
-
PostRunE
-
PersistentPostRunE
對不明命令的建議
當遇到不明命令,會有提出一定的建議,其採用最小編輯距離算法 (Levenshtein distance)。例如:
hugo srever
<<'COMMENT'
Error: unknown command "srever" for "hugo"
Did you mean this?
server
Run 'hugo --help' for usage.
COMMENT
如果你想關閉智能提示,可以:
command.DisableSuggestions = true
// 或者
command.SuggestionsMinimumDistance = 1
或者使用 SuggestFor 屬性來自定義一些建議,例如:
kubectl remove
<<'COMMENT'
Error: unknown command "remove" for "kubectl"
Did you mean this?
delete
Run 'kubectl help' for usage.
COMMENT
轉自:陳健
鏈接:o-my-chenjian.com/2017/09/20/Using-Cobra-With-Golang/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/876he6U8kintLnmIKuVVhQ