cobra是一個命令行程序庫,能夠用來編寫命令行程序。同時,它也提供了一個腳手架,
用於生成基於 cobra 的應用程序框架。很是多知名的開源項目使用了 cobra 庫構建命令行,如Kubernetes、Hugo、etcd等等等等。
本文介紹 cobra 庫的基本使用和一些有趣的特性。git
關於做者spf13,這裏多說兩句。spf13 開源很多項目,並且他的開源項目質量都比較高。
相信使用過 vim 的都知道spf13-vim,號稱 vim 終極配置。
能夠一鍵配置,對於我這樣的懶人來講絕對是福音。他的viper是一個完整的配置解決方案。
完美支持 JSON/TOML/YAML/HCL/envfile/Java properties 配置文件等格式,還有一些比較實用的特性,如配置熱更新、多查找目錄、配置保存等。
還有很是火的靜態網站生成器hugo也是他的做品。github
第三方庫都須要先安裝,後使用。下面命令安裝了cobra
生成器程序和 cobra 庫:golang
$ go get github.com/spf13/cobra/cobra
若是出現了golang.org/x/text
庫找不到之類的錯誤,須要手動從 GitHub 上下載該庫,再執行上面的安裝命令。我之前寫過一篇博客搭建 Go 開發環境提到了這個方法。vim
咱們實現一個簡單的命令行程序 git,固然這不是真的 git,只是模擬其命令行。最終仍是經過os/exec
庫調用外部程序執行真實的 git 命令,返回結果。
因此咱們的系統上要安裝 git,且 git 在可執行路徑中。目前咱們只添加一個子命令version
。目錄結構以下:windows
▾ get-started/ ▾ cmd/ helper.go root.go version.go main.go
root.go
:微信
package cmd import ( "errors" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command { Use: "git", Short: "Git is a distributed version control system.", Long: `Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency.`, Run: func(cmd *cobra.Command, args []string) { Error(cmd, args, errors.New("unrecognized command")) }, } func Execute() { rootCmd.Execute() }
version.go
:app
package cmd import ( "fmt" "os" "github.com/spf13/cobra" ) var versionCmd = &cobra.Command { Use: "version", Short: "version subcommand show git version info.", Run: func(cmd *cobra.Command, args []string) { output, err := ExecuteCommand("git", "version", args...) if err != nil { Error(cmd, args, err) } fmt.Fprint(os.Stdout, output) }, } func init() { rootCmd.AddCommand(versionCmd) }
main.go
文件中只是調用命令入口:框架
package main import ( "github.com/darjun/go-daily-lib/cobra/get-started/cmd" ) func main() { cmd.Execute() }
爲了編碼方便,在helpers.go
中封裝了調用外部程序和錯誤處理函數:ide
package cmd import ( "fmt" "os" "os/exec" "github.com/spf13/cobra" ) func ExecuteCommand(name string, subname string, args ...string) (string, error) { args = append([]string{subname}, args...) cmd := exec.Command(name, args...) bytes, err := cmd.CombinedOutput() return string(bytes), err } func Error(cmd *cobra.Command, args []string, err error) { fmt.Fprintf(os.Stderr, "execute %s args:%v error:%v\n", cmd.Name(), args, err) os.Exit(1) }
每一個 cobra 程序都有一個根命令,能夠給它添加任意多個子命令。咱們在version.go
的init
函數中將子命令添加到根命令中。函數
編譯程序。注意,不能直接go run main.go
,這已經不是單文件程序了。若是強行要用,請使用go run .
:
$ go build -o main.exe
cobra 自動生成的幫助信息,very cool:
$ ./main.exe -h Git is a free and open source distributed version control system designed to handle everything from small to very large projects with speed and efficiency. Usage: git [flags] git [command] Available Commands: help Help about any command version version subcommand show git version info. Flags: -h, --help help for git Use "git [command] --help" for more information about a command.
單個子命令的幫助信息:
$ ./main.exe version -h version subcommand show git version info. Usage: git version [flags] Flags: -h, --help help for version
調用子命令:
$ ./main.exe version git version 2.19.1.windows.1
未識別的子命令:
$ ./main.exe clone Error: unknown command "clone" for "git" Run 'git --help' for usage.
編譯時能夠將main.exe
改爲git
,用起來會更有感受🤩。
$ go build -o git $ ./git version git version 2.19.1.windows.1
使用 cobra 構建命令行時,程序的目錄結構通常比較簡單,推薦使用下面這種結構:
▾ appName/ ▾ cmd/ cmd1.go cmd2.go cmd3.go root.go main.go
每一個命令實現一個文件,全部命令文件存放在cmd
目錄下。外層的main.go
僅初始化 cobra。
cobra 提供很是豐富的功能:
app server
,app fetch
等;首先須要明確 3 個基本概念:
下面示例中,server
是一個(子)命令,--port
是選項:
hugo server --port=1313
下面示例中,clone
是一個(子)命令,URL
是參數,--bare
是選項:
git clone URL --bare
在 cobra 中,命令和子命令都是用Command
結構表示的。Command
有很是多的字段,用來定製命令的行爲。
在實際中,最經常使用的就那麼幾個。咱們在前面示例中已經看到了Use/Short/Long/Run
。
Use
指定使用信息,即命令怎麼被調用,格式爲name arg1 [arg2]
。name
爲命令名,後面的arg1
爲必填參數,arg3
爲可選參數,參數能夠多個。
Short/Long
都是指定命令的幫助信息,只是前者簡短,後者詳盡而已。
Run
是實際執行操做的函數。
定義新的子命令很簡單,就是建立一個cobra.Command
變量,設置一些字段,而後添加到根命令中。例如咱們要添加一個clone
子命令:
package cmd import ( "fmt" "os" "github.com/spf13/cobra" ) var cloneCmd = &cobra.Command { Use: "clone url [destination]", Short: "Clone a repository into a new directory", Run: func(cmd *cobra.Command, args []string) { output, err := ExecuteCommand("git", "clone", args...) if err != nil { Error(cmd, args, err) } fmt.Fprintf(os.Stdout, output) }, } func init() { rootCmd.AddCommand(cloneCmd) }
其中Use
字段clone url [destination]
表示子命令名爲clone
,參數url
是必須的,目標路徑destination
可選。
咱們將程序編譯爲mygit
可執行文件,而後將它放到$GOPATH/bin
中。我喜歡將$GOPATH/bin
放到$PATH
中,因此能夠直接調用mygit
命令了:
$ go build -o mygit $ mv mygit $GOPATH/bin $ mygit clone https://github.com/darjun/leetcode Cloning into 'leetcode'...
你們能夠繼續添加命令。可是我這邊只是偷了個懶,將操做都轉發到實際的 git 去執行了。這確實沒什麼實際的用處。
有這個思路,試想一下,咱們能夠結合多個命令實現不少有用的工具,例如打包工具😉。
cobra 中選項分爲兩種,一種是永久選項,定義它的命令和其子命令均可以使用。經過給根命令添加一個選項定義全局選項。
另外一種是本地選項,只能在定義它的命令中使用。
cobra 使用pflag解析命令行選項。pflag
使用上基本與flag
相同,該系列文章有一篇介紹flag
庫的,Go 每日一庫之 flag。
與flag
同樣,存儲選項的變量也須要提早定義好:
var Verbose bool var Source string
設置永久選項:
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
設置本地選項:
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
兩種參數都是相同的,長選項/短選項名、默認值和幫助信息。
下面,咱們經過一個案例來演示選項的使用。
假設咱們要作一個簡單的計算器,支持加、減、乘、除操做。而且能夠經過選項設置是否忽略非數字參數,設置除 0 是否報錯。
顯然,前一個選項應該放在全局選項中,後一個應該放在除法命令中。程序結構以下:
▾ math/ ▾ cmd/ add.go divide.go minus.go multiply.go root.go main.go
這裏展現divide.go
和root.go
,其它命令文件都相似。完整代碼我放在GitHub上了。
divide.go
:
var ( dividedByZeroHanding int // 除 0 如何處理 ) var divideCmd = &cobra.Command { Use: "divide", Short: "Divide subcommand divide all passed args.", Run: func(cmd *cobra.Command, args []string) { values := ConvertArgsToFloat64Slice(args, ErrorHandling(parseHandling)) result := calc(values, DIVIDE) fmt.Printf("%s = %.2f\n", strings.Join(args, "/"), result) }, } func init() { divideCmd.Flags().IntVarP(÷dByZeroHanding, "divide_by_zero", "d", int(PanicOnDividedByZero), "do what when divided by zero") rootCmd.AddCommand(divideCmd) }
root.go
:
var ( parseHandling int ) var rootCmd = &cobra.Command { Use: "math", Short: "Math calc the accumulative result.", Run: func(cmd *cobra.Command, args []string) { Error(cmd, args, errors.New("unrecognized subcommand")) }, } func init() { rootCmd.PersistentFlags().IntVarP(&parseHandling, "parse_error", "p", int(ContinueOnParseError), "do what when parse arg error") } func Execute() { rootCmd.Execute() }
在divide.go
中定義瞭如何處理除 0 錯誤的選項,在root.go
中定義瞭如何處理解析錯誤的選項。選項枚舉以下:
const ( ContinueOnParseError ErrorHandling = 1 // 解析錯誤嘗試繼續處理 ExitOnParseError ErrorHandling = 2 // 解析錯誤程序中止 PanicOnParseError ErrorHandling = 3 // 解析錯誤 panic ReturnOnDividedByZero ErrorHandling = 4 // 除0返回 PanicOnDividedByZero ErrorHandling = 5 // 除0 painc )
其實命令的執行邏輯並不複雜,就是將參數轉爲float64
。而後執行相應的運算,輸出結果。
測試程序:
$ go build -o math $ ./math add 1 2 3 4 1+2+3+4 = 10.00 $ ./math minus 1 2 3 4 1-2-3-4 = -8.00 $ ./math multiply 1 2 3 4 1*2*3*4 = 24.00 $ ./math divide 1 2 3 4 1/2/3/4 = 0.04
默認狀況,解析錯誤被忽略,只計算格式正確的參數的結果:
$ ./math add 1 2a 3b 4 1+2a+3b+4 = 5.00 $ ./math divide 1 2a 3b 4 1/2a/3b/4 = 0.25
設置解析失敗的處理,2 表示退出程序,3 表示 panic(看上面的枚舉):
$ ./math add 1 2a 3b 4 -p 2 invalid number: 2a $ ./math add 1 2a 3b 4 -p 3 panic: strconv.ParseFloat: parsing "2a": invalid syntax goroutine 1 [running]: github.com/darjun/go-daily-lib/cobra/math/cmd.ConvertArgsToFloat64Slice(0xc00004e300, 0x4, 0x6, 0x3, 0xc00008bd70, 0x504f6b, 0xc000098600) D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/helper.go:58 +0x2c3 github.com/darjun/go-daily-lib/cobra/math/cmd.glob..func1(0x74c620, 0xc00004e300, 0x4, 0x6) D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/add.go:14 +0x6d github.com/spf13/cobra.(*Command).execute(0x74c620, 0xc00004e1e0, 0x6, 0x6, 0x74c620, 0xc00004e1e0) D:/code/golang/src/github.com/spf13/cobra/command.go:835 +0x2b1 github.com/spf13/cobra.(*Command).ExecuteC(0x74d020, 0x0, 0x599ee0, 0xc000056058) D:/code/golang/src/github.com/spf13/cobra/command.go:919 +0x302 github.com/spf13/cobra.(*Command).Execute(...) D:/code/golang/src/github.com/spf13/cobra/command.go:869 github.com/darjun/go-daily-lib/cobra/math/cmd.Execute(...) D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/root.go:45 main.main() D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/main.go:8 +0x35
至於除 0 選項你們本身試試。
細心的朋友應該都注意到了,該程序還有一些不完善的地方。例如這裏若是輸入非數字參數,該參數也會顯示在結果中:
$ ./math add 1 2 3d cc 1+2+3d+cc = 3.00
感興趣能夠本身完善一下~
經過前面的介紹,咱們也看到了其實 cobra 命令的框架仍是比較固定的。這就有了工具的用武之地了,可極大地提升咱們的開發效率。
前面安裝 cobra 庫的時候也將腳手架程序安裝好了。下面咱們介紹如何使用這個生成器。
使用cobra init
命令建立一個 cobra 應用程序:
$ cobra init scaffold --pkg-name github.com/darjun/go-daily-lib/cobra/scaffold
其中scaffold
爲應用程序名,後面經過pkg-name
選項指定包路徑。生成的程序目錄結構以下:
▾ scaffold/ ▾ cmd/ root.go LICENSE main.go
這個項目結構與以前介紹的徹底相同,也是 cobra 推薦使用的結構。一樣地,main.go
也僅僅是入口。
在root.go
中,工具額外幫咱們生成了一些代碼。
在根命令中添加了配置文件選項,大部分應用程序都須要配置文件:
func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.scaffold.yaml)") rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") }
在初始化完成的回調中,若是發現該選項爲空,則默認使用主目錄下的.scaffold.yaml
文件:
func initConfig() { if cfgFile != "" { viper.SetConfigFile(cfgFile) } else { home, err := homedir.Dir() if err != nil { fmt.Println(err) os.Exit(1) } viper.AddConfigPath(home) viper.SetConfigName(".scaffold") } viper.AutomaticEnv() if err := viper.ReadInConfig(); err == nil { fmt.Println("Using config file:", viper.ConfigFileUsed()) } }
這裏用到了我前幾天介紹的go-homedir庫。配置文件的讀取使用了 spf13 本身的開源項目viper(毒龍?真是起名天才)。
除了代碼文件,cobra 還生成了一個 LICENSE 文件。
如今這個程序還不能作任何事情,咱們須要給它添加子命令,使用cobra add
命令:
$ cobra add date
該命令在cmd
目錄下新增了date.go
文件。基本結構已經搭好了,剩下的就是修改一些描述,添加一些選項了。
咱們如今實現這樣一個功能,根據傳入的年、月,打印這個月的日曆。若是沒有傳入選項,使用當前的年、月。
選項定義:
func init() { rootCmd.AddCommand(dateCmd) dateCmd.PersistentFlags().IntVarP(&year, "year", "y", 0, "year to show (should in [1000, 9999]") dateCmd.PersistentFlags().IntVarP(&month, "month", "m", 0, "month to show (should in [1, 12]") }
修改dateCmd
的Run
函數:
Run: func(cmd *cobra.Command, args []string) { if year < 1000 && year > 9999 { fmt.Fprintln(os.Stderr, "invalid year should in [1000, 9999], actual:%d", year) os.Exit(1) } if month < 1 && year > 12 { fmt.Fprintln(os.Stderr, "invalid month should in [1, 12], actual:%d", month) os.Exit(1) } showCalendar() }
showCalendar
函數就是利用time
提供的方法實現的,這裏就不贅述了。感興趣能夠去個人 GitHub 上查看實現。
看看程序運行效果:
$ go build -o main.exe $ ./main.exe date Sun Mon Tue Wed Thu Fri Sat 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 $ ./main.exe date --year 2019 --month 12 Sun Mon Tue Wed Thu Fri Sat 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
能夠再爲這個程序添加其餘功能,試一試吧~
cobra 提供了很是豐富的特性和定製化接口,例如:
因爲篇幅限制,就不一一介紹了。有興趣可自行研究。cobra 庫的使用很是普遍,不少知名項目都有用到,前面也提到過這些項目。
學習這些項目是如何使用 cobra 的,能夠從中學習 cobra 的特性和最佳實踐。這也是學習開源項目的一個很好的途徑。
文中全部示例代碼都已上傳至個人 GitHub,Go 每日一庫,https://github.com/darjun/go-daily-lib/tree/master/cobra。
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~
本文由博客一文多發平臺 OpenWrite 發佈!