golang常用庫:cli命令行/應用程序生成工具-cobra使用
一、Cobra 介紹
我前面有一篇文章介紹了配置文件解析庫 Viper 的使用,這篇介紹 Cobra 的使用,你猜的沒錯,這 2 個庫都是同一個作者 spf13,他開發了很多與 golang 相關的庫,他目前在 google 領導着 golang 產品相關開發工作。
Cobra 是關於 golang 的一個命令行解析庫,用它能夠快速創建功能強大的 cli 應用程序和命令行工具。
它被很多知名的項目使用,比如 Kubernetes,Github CLI,Etcd 等。更多應用此庫的項目列表。
我們平常用到命令:git commit -m "message",docker containter start 等都可以用 cobra 來實現。
Cobra 相關文檔地址:
Cobra 的logo:
(from:https://github.com/spf13/cobra)
二、功能特性介紹
- 很多子命令的CLIS: 比如 app server、app fetch 等
- 支持嵌套子命令(sub-command)
- 輕松完成應用程序和命令的創建:cobra init appname 和 cobra add cmdname
- 為應用程序生成 man 手冊
- 全局、本地和級聯 flag
- 為 shell 程序完成自動提示(bash,zsh,fish, powershell etc.)
- 支持命令行別名,可以幫助你更容易更改內容而不破壞他們
- 靈活定義自己的help、usage信息
- 可選集成 viper 配置管理工具庫
更多功能特性請查看: cobra文檔介紹
三、Cobra cli 命令結構說明
Cobra 命令結構由3部分組成:
commands、arguments 和 flags
-
commands:
命令行,代表行為動作,要執行的一個動作。每個命令還可以包含子命令。分為:rootCmd 和 subCmd。程序中具體對象是 cobra.Command{},這個是根命令;子命令(subCmd)用 rootCmd.AddCommand() 添加,子命令通常也會單獨存一個文件,
並通過一個全局變量讓 rootCmd 可以 add 它。
-
arguments:
命令行參數,通常是 []string 表示
-
flags:
命令行選項。對 command 進一步的控制。通常用一短橫
-
或者兩短橫--
標識。程序中讀取存儲在變量中。
cobra 命令行格式:
APPNAME VERB NOUN --ADJECTIVE
APPNEM COMMAND ARG --FLAG
例子說明:
hugo server --port=1313 #server 代表 command, port 代表 flag。
git clone URL --bare #clone 代表 command,URL 代表操作的物-argument,bare 代表 flag。
四、Cobra 基本使用方法
golang v1.15, cobra v1.2.1
安裝 cobra:
go get -u github.com/spf13/cobra
可以用 cobra -h
來查看 cobra 命令的一些用法。
Usage:
cobra [command]
Available Commands:
add Add a command to a Cobra Application
completion generate the autocompletion script for the specified shell
help Help about any command
init Initialize a Cobra Application
init 命令初始化應用程序
安裝 cobra generator:go get -u github.com/spf13/cobra/cobra
使用命令 cobra init 來創建第一個應用程序,這個命令也是初始化一個應用程序的項目框架:
cobra init firstappname
Error: required flag(s) "pkg-name" not set
報錯了,錯誤信息截圖如下:
錯誤信息:需要設置 --pkg-name 參數。
因為我們項目不存在。先創建名為 firstappname 文件夾,然后進入目錄 firstappname,在命令行下運行:cobra init --pkg-name firstappname
。
自動生成了如下目錄和程序:
下面程序我去掉了英文注釋部分。
main.go
package main
import "firstappname/cmd"
func main() {
cmd.Execute()
}
cmd/root.go
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// 構建根 command 命令。前面我們介紹它還可以有子命令,這個command里沒有構建子命令
var rootCmd = &cobra.Command{
Use: "firstappname",
Short: "A brief description of your application",
Long: `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.`
}
// 執行 rootCmd 命令並檢測錯誤
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
func init() {
// 加載運行初始化配置
cobra.OnInitialize(initConfig)
// rootCmd,命令行下讀取配置文件,持久化的 flag,全局的配置文件
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.firstappname.yaml)")
// local flag,本地化的配置
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
// 初始化配置的一些設置
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile) // viper 設置配置文件
} else {// 上面沒有指定配置文件,下面就讀取 home 下的 .firstappname.yaml文件
// 配置文件參數設置
home, err := os.UserHomeDir()
cobra.CheckErr(err)
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".firstappname")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {// 讀取配置文件
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
其實上面的錯誤在 cobra generator 文檔里有提示了,所以要多看官方文檔。
這個 root.go 里的 cobra.Command 就是設置命令行格式的地方。如果要執行相關的命令,可以在 Long:
...下面加一行:
Run: func(cmd *cobra.Command, args []string) { },
運行程序:go run main.go , 報錯了:
我用的是 go v1.15,GO111MODULE="on"。
用 go mod 來創建 module,進入firstappname目錄,命令: go mod init firstappname
,生成一個 go.mod,
module firstappname
go 1.15
require (
github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.9.0
)
在運行 go run main.go,第一次運行會下載文件到 go.mod, go.sum 里。再次運行,就會出現 rootCmd 下的 Long 信息。
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.
可以看出,用 cobra init 命令初始化的項目, 生成了一個初始化的應用框架,但是沒有任何邏輯功能。僅僅輸出一些描述性信息。
這個程序里,最重要的是 cmd/root.go 里的 rootCmd = &cobra.Command{} 這行程序,這里定義命令動作。
程序里的 init() 和 initConfig() 函數都是對命令行的配置。
為 rootCmd 添加功能:
var rootCmd = &cobra.Command{
Use: "firstappname",
Short: "A brief description of your application",
Long: `(root)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.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("root called")
},
}
測試運行 go run main.go
,輸出:
$ go run main.go
root called
運行:go run main.go --help
會輸出上面的 Long 信息和完整的幫助信息。
也可以把上面命令編譯:go build -o demo.exe,在運行
完整生成一個cobra應用框架的命令:
$ mkdir firstappname
$ cd firstappname
$ cobra init --pkg-name firstappname
$ go mod init firstappname
完整例子在 github 上,golang-library-learning/cobra
add 生成子命令subCmd
上面我們用 cobra init
創建了應用程序框架,在程序 cmd/root.go 里有一個根命令 rootCmd,也就是說 init 命令創建了一個根命令。執行 command 命令是 &cobra.Command{} 里的 Run 方法。
用 cobra add
來為 rootCmd 創建一個子命令。這個子命令通常在一個單獨的文件里。
- 用 add 命令生成子命令代碼:
// cd 進入firstappname
$ cd ./firstappname
$ cobra add demo
demo created at D:\work\mygo\common_pkg\cobra\firstappname
在 cmd 目錄下生成了 demo.go 文件:
-
為子命令添加簡單功能
add 命令已經為我們生成了一個簡單的應用代碼,程序文件通常存放在cmd目錄下,demo.go 程序:
package cmd import ( "fmt" "github.com/spf13/cobra" ) // demoCmd represents the demo command // 子命令 var demoCmd = &cobra.Command{ Use: "demo", Short: "A brief description of your command", Long: `A longer description that spans multiple lines and likely contains examples and usage of using your command. 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.`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("demo called") fmt.Println("cmd demo") }, } func init() { rootCmd.AddCommand(demoCmd) }
到現在為止,為 firstappdemo 添加了 2 個 Command 了,分別是根命令 rootCmd 和子命令 demoCmd。
子命令和根命令的關系一般通過程序 rootCmd.AddCommand()
方法確定。在程序 demo.go 里可以看到它在 init() 函數里。
Run 方法里添加程序:fmt.Println("cmd demo")。一般這里的程序都是其他 package 里完成了具體邏輯,然后 Run 方法里在調用這些程序。
測試運行:go run main.go demo
輸出:
demo called
cmd demo
也可以編譯項目 go build -o xxx 在運行。
Flags使用-給Command添加flags
flag 命令行選項,也叫標識,對command命令行為的進一步指示操作。
用這個標識可以給 command 添加一些可選項。
根據 flag 選項作用范圍不同,可以分為 2 類:
-
Persistent Flags,持久化的flag,全局范圍
如果設置全局范圍的flag,可以用這個來設置。它既可以給根命令設置選項值,也可以給子命令設置選項值。
下面例子里的 rootCmd 和 demoCmd 都可以調用 flag。
-
Local Flags,局部flag,只對指定的command生效。比如某個子命令的 flag。
因為 flag 標識是在命令行后面不同位置使用,所以我們要在方法外定義一個變量,來分配存儲使用這個標識符。下面例子會說明。
Persistent Flags 全局flag例子
-
在 cmd/root.go 文件中添加一個變量 name
var cfgFile string // 添加 name var name string
然后在 root.go:init() 函數中添加全局 persistent flag,把 flag 值存儲到變量 name 中。也就是讀取命令行--name這個flag設置的值,然后賦值給程序里變量name
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.firstappname.yaml)") // 添加全局 flag rootCmd.PersistentFlags().StringVar(&name, "name", "", "Set one name")
-
在文件 cmd/demo.go 中的 demoCmd(子命令) 里 Run 方法輸出 name 值
Run: func(cmd *cobra.Command, args []string) { fmt.Println("demo called") fmt.Println("cmd demo") // 打印輸出 name fmt.Println("print persistent flag name: ", name) },
-
測試運行程序
$ go run main.go demo --name setname demo called cmd demo print persistent flag name: setname
當然也可以先編譯 go build -o cobrademo.exe(我用的win),然后在運行測試程序
$.\cobrademo.exe demo --name setname1 demo called cmd demo print persistent flag name: setname1
Persistent flag 的讀取方法:
// arg1:存儲變量,
// arg2:設置長flag名,這里 name 顯示 --name,
// arg3:設置短flag名,這里 n 顯示 -n,一般與上面對應
// arg4:默認值, 這里設置為 ""
// arg5:flag的一些說明信息
PersistentFlags().StringVarP(&name, "name", "n", "", "Set one name")
// 與上面用法基本相同,只是沒有短 flag 設置
PersistentFlags().StringVar(&name, "name", "", "Set one name")
// 直接設置flag名,arg1:flag 名,arg2:默認值,arg3:說明
PersistentFlags().String("foo", "", "A help for foo")
Local Flags例子
一個 flag 賦值給本地變量,只能對指定的command生效。
我們在 demo.go 中測試 local flag。
- 在 cmd/demo.go 文件中定義變量 dsn,存儲這個 flag 值
// 定義 local flag
var dsn string
-
在 demo.go 中的 init() 中添加下面代碼,把值存儲到 dsn上
demoCmd.Flags().StringVarP(&dsn, "dsn", "d", "", "dsn file")
-
在 demoCmd.Command{} 獲取該值
Run: func(cmd *cobra.Command, args []string) { fmt.Println("demo called") fmt.Println("cmd demo") // 打印輸出 name fmt.Println("print persistent flag name: ", name) // 打印輸出local flag: dsn fmt.Println("(local flag)print dsn: ", dsn) },
-
測試運行
$ go run .\main.go demo --dsn setdsn1 demo called cmd demo print persistent flag name: (local flag)print dsn: setdsn1
輸出了 setdsn1。
測試下其它子命令可以不可以獲取這個 dsn,添加一個新的子命令 demo2,
$ cobra add demo2
在目錄 cmd 下添加了文件 demo2.go, 在 Run 下添加:
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("demo2 called")
// 添加輸出 dsn
fmt.Println("test get local flag(dsn): ", dsn)
},
測試:
$ go run .\main.go demo2 --dsn testdsn
Error: unknown flag: --dsn
報錯了,程序終止運行了。
說明:local flag 局部選項,只能作用於指定的 command。本例子中作用於 demoCmd,而不能作用於 demo2Cmd。
local flag 的讀取方法:
// arg1:存儲變量,
// arg2:設置長flag名,這里 name 顯示 --name,
// arg3:設置短flag名,這里 n 顯示 -n,一般與上面對應
// arg4:默認值, 這里設置為 ""
// arg5:flag的一些說明信息
// 方法(1)
Flags().StringVarP(&name, "name", "n", "", "Set one name")
// 與上面方法(1)用法基本相同,只是沒有短 flag 設置
Flags().StringVar(&name, "name", "", "Set one name")
// 直接設置flag名,arg1:flag 名,arg2:默認值,arg3:說明
Flags().String("foo", "", "A help for foo")
// 與上面方法(1)用法基本相同,除了第一個沒有變量讀取參數
Flags().StringP("toggle", "t", false, "Help message for toggle")
完整例子在 github 上,golang-library-learning/cobra
設置flag必填項
比如給 demo.go 的 dsn 這個 flag 設置必選項
demoCmd.Flags().StringVarP(&dsn, "dsn", "d", "", "dsn file")
// 把 dsn 設置為必選項
demoCmd.MarkFlagRequired("dsn")
flag 不設置dsn,運行程序:go run main.go demo
, 報錯:Error: required flag(s) "dsn" not set
$ go run .\main.go demo
Error: required flag(s) "dsn" not set
Usage:
firstappname demo [flags]
Flags:
-d, --dsn string dsn file
-h, --help help for demo
Global Flags:
--config string config file (default is $HOME/.firstappname.yaml)
--name string Set one name
Error: required flag(s) "dsn" not set
exit status 1
加上 dsn 運行,go run main.go demo --dsn setdsn
,正常輸出:
$ go run main.go demo --dsn setdsn
demo called
cmd demo
print persistent flag name:
(local flag)print dsn: setdsn
綁定配置
還可以綁定配置到 flags 上,用 viper
在 cmd/root.go 里,有一個 initConfig()
方法,這個就是初始化配置方法。加載執行是在 init()
方法里,
func init() {
cobra.OnInitialize(initConfig)
... ...
}
我們可以在 init() 方法中添加綁定 flag 程序,
rootCmd.PersistentFlags().StringVar(&name, "name", "", "Set one name")
viper.BindPFlag("name", rootCmd.PersistentFlags().Lookup("name"))
這樣就將 viper 配置和 flag 綁定,如果用戶不設置 --name,將從配置中查找。
更多方法請查看 viper flag doc
arguments 命令行參數設置
可以用Command 的 Args 字段指定參數效驗規則。
Cobra 也內置了一些規則:
-
NoArgs:如果有任何命令行args參數,將會報錯
-
ArbitraryArgs:該命令接受任何參數
-
OnlyValidArgs:如果該命令參數不在 Command 的 ValidArgs 中,將會報錯
-
MinimumArgs(int): 如果命令參數數目少於N個,將會報錯
-
MaximumArgs(int): 如果命令參數數目多於N個,將會報錯
-
ExactArgs(int): 如果命令參數數目不是N個,將會報錯
-
RangeArgs(min, max):如果命令參數數目范圍不在(min, max),將會報錯
內置效驗規則的例子:
var rootCmd = &cobra.Command{
Use: "dmeo",
Short: "demo short",
Long: `let's do it, demo!`,
Args: cobra.MinimumNArgs(5),
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("hello chenqionghe")
},
}
自定義驗證規則的例子:
var cmd = &cobra.Command {
Short: "demo",
Args: func(cmd *cobra.Command, args[] string) error {
if len(args) > 0 {
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, demo!")
},
}
鈎子函數 PreRun and PostRun Hooks
可以在執行命令之前或之后運行鈎子函數。如果子命令未聲明自己的 Persistent * Run
函數,則子命令將繼承父命令的鈎子函數。
函數的執行順序為:
- PersistentPreRun
- PreRun
- Run
- PostRun
- 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)
},
}
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()
}
運行程序:
$ go run .\hookdemo.go
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]
錯誤處理函數和鈎子函數
與上面的鈎子函數功能一樣,只不過這里可以返回錯誤,處理
RunE 功能的執行先后順序如下:
- PersistentPreRunE
- PreRunE
- RunE
- PostRunE
- PersistentPostRunE
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("Inside subCmd Run with args: %v\n", args)
return nil
},
為你的命令生成文檔
Cobra 可以基於子命令、標志等生成文檔。具體的使用方法和生產格式文檔請點擊下面鏈接:
你可以設置 cmd.DisableAutoGenTag = true
從而把文檔中 "Auto generated by spf13/cobra..." 等字樣刪掉。
help 命令
命令: cobra help
,可以清楚顯示出對使用 cobra 有用的信息,比如命令提示
你還可以定義自己的 help 命令或模板
cmd.SetHelpCommand(cmd *Command)
cmd.setHelpCommand(f func(*Command, []string))
cmd.setHelpTemplate(s string)
五、完整demo例子
- 新建一個名為 student 的項目
$ mkdir student
$ cd ./student
// 創建 go.mod
$ go mod init student
用 cobra init 初始化項目
$ cobra init --pkg-name student
然后生成如下圖:
- 設置顯示學生信息
在 student 下新建目錄 info:
$ mkdir info
在里面新建 student.go 文件:
student.go 程序如下:
package info
import "fmt"
type Student struct {
Name string
Age int
Grade string
}
func GetInfo(stu *Student) {
fmt.Printf("student info: name=%s, age=%d, grade=%s \n", stu.Name, stu.Age, stu.Grade)
}
func SetInfo(name string, age int, grade string) *Student {
var stu Student
stu.Name = name
stu.Age = age
stu.Grade = grade
return &stu
}
- 根命令里設置、獲取 student 里字段 name,age
在 cmd/root.go 里設置變量 name 和 age:
var cfgFile string
// 設置 student 的name和age
var name string
var age int
讀取 name和 age:
在 cmd/root.go: init() 函數里加上如下代碼
// 讀取、存儲name ag值
rootCmd.Flags().StringVarP(&name, "name", "n", "", "student`s name")
rootCmd.Flags().IntVarP(&age, "age", "a", 0, "student`s age")
先 import "student/info", 然后在 rootCmd=&cobra.Command{} 增加如下代碼:
Run: func(cmd *cobra.Command, args []string) {
stuInfo := info.SetInfo(name, age, "no3") // 這里讀取了name,age值賦值給SetInfo方法
info.GetInfo(stuInfo)
},
把&cobra.Command{}定義的 Long 刪掉,把 root.go 里 initConfig() 函數注釋掉,init() 里除了設置name和age的程序都注釋掉,便於測試看清楚主要信息。
在運行測試程序:
先運行help命令看看命令信息,go run main.go -h
$ go run .\main.go -h
A brief description of your application
Usage:
student [flags]
student [command]
Available Commands:
completion generate the autocompletion script for the specified shell
help Help about any command
stu A brief description of your command
Flags:
-a, --age int student`s age
-h, --help help for student
-n, --name string student`s name
Use "student [command] --help" for more information about a command.
可以看到 Flags 下面的 -a,--age;-n,--name 已經出現了,在運行命令:
$ go run main.go -a 23 --name Timmy
student info: name=Timmy, age=23, grade=no3
顯示出正確信息
- 增加子命令cli程序
$ cobra add stu
生成文件如下
在 stu.go 里設置變量 grade:
var grade string
讀取命令行下的grade設置:
在 stu.go 里 init() 函數里添加代碼
stuCmd.Flags().StringVarP(&grade, "grade", "g", "", "grade`s info")
然后在stuCmd = &cobra.Command{}添加代碼:
Run: func(cmd *cobra.Command, args []string) {
stuInfo := info.SetInfo("immmy", 27, grade)
info.GetInfo(stuInfo)
},
測試運行:
運行子命令程序的 -h 命令
$ go run .\main.go stu -h
A brief description of your command
Usage:
student stu [flags]
Flags:
-g, --grade string grade`s info
-h, --help help for stu
在運行獲取 --grade 命令
$ go run .\main.go stu --grade no4
student info: name=immmy, age=27, grade=no4
正確輸出值
完整程序在github上:student
也可以到我的公眾號 九卷技術錄-golang常用庫包:cli命令行工具-cobra使用 討論