如何構建現代化的 Go 命令行工具?
在 Go 項目開發中,我們需要編寫 main 函數,並編譯成爲二進制文件,部署啓動服務。有多種方式可以開發一個 main 函數。例如你可以手擼一個 main 函數,並在 main 函數中處理命令行參數,配置文件解析,應用初始化等操作。如下所示:
package main
import (
"flag"
"fmt"
)
func main() {
// 解析命令行參數
option1 := flag.String("option1", "default_value", "Description of option 1")
option2 := flag.Int("option2", 0, "Description of option 2")
flag.Parse()
// 執行簡單的業務邏輯
fmt.Println("Option 1 value:", *option1)
fmt.Println("Option 2 value:", *option2)
// 在這裏添加您的業務邏輯代碼
}
雖然可以手擼一個應用,但是 開發效率低下,要處理各種場景,開發出來的應用還不怎麼優雅。爲了,解決這些問題,社區湧現出了大批優秀的應用開發框架,例如:kingpin、cli、cobra 等。開發者可以直接複用這些應用開發框架,來構建優秀的命令行工具,這也是當前開發應用時,採用最多的方式。
當前社區最受歡迎的應用開發框架是 cobra。本文就來詳細介紹下 cobra 框架的功能及使用方式。
Cobra 包介紹
Cobra 是一個可以創建強大的現代 CLI 應用程序的庫,它還提供了一個可以生成應用和命令文件的程序的命令行工具:cobra-cli
。有許多大型項目都是用 cobra 來構建他們的應用程序,例如:kubernetes、Docker、Etcd、Rkt、Hugo 等。Cobra 具有很多特性,一些核心特性如下:
-
可以構建基於子命令的 CLI,並支持支持嵌套子命令。例如:
app server
,app fetch
。 -
可以通過
cobra-cli init appname
&cobra-cli add cmdname
輕鬆生成應用和子命令。 -
智能化命令建議 (app srver... did you mean app server?)。
-
自動生成命令和標誌的 help 文本,並能自動識別
-h
,--help
等標誌。 -
自動爲你的應用程序生成 bash、zsh、fish 和 powershell 自動補全腳本。
-
支持命令別名、自定義幫助、自定義用法等。
-
可以與 viper、pflag 緊密集成,用於構建 12-factor 應用程序。
Cobra 建立在 commands、arguments 和 flags 結構之上。commands 代表命令,arguments 代表非選項參數,flags 代表選項參數(也叫標誌)。一個好的應用程序應該是易懂的,用戶可以清晰的知道如何去使用這個應用程序。應用程序通常遵循如下模式:APPNAME VERB NOUN --ADJECTIVE
或者 APPNAME COMMAND ARG --FLAG
,例如:
git clone URL --bare # clone 是一個命令,URL 是一個非選項參數,bare 是一個選項參數
這裏,VERB
代表動詞,NOUN
代碼名詞,ADJECTIVE
代表形容詞。
cobra-cli
命令安裝
Cobra 提供了一個 cobra-cli
命令,用來初始化一個應用程序併爲其添加命令,方便我們開發基於 Cobra 的應用。cobra-cli
命令安裝方法如下:
$ go install github.com/spf13/cobra-cli@latest
cobra-cli
命令提供了 4 個子命令:
-
init
:初始化一個 cobra 應用程序; -
add
:給通過 cobra init 創建的應用程序添加子命令; -
completion
:爲指定的 shell 生成命令自動補全腳本; -
help
:打印任意命令的幫助信息。
cobra-cli
命令還提供了一些全局的參數:
-
-a
,--author
:指定 Copyright 版權聲明中的作者; -
--config
:指定 cobra 配置文件的路徑; -
-l
,--license
:指定生成的應用程序所使用的開源協議,內置的有:GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD; -
--viper
:使用viper
作爲命令行參數解析工具,默認爲true
。
Cobra 使用方法
在構建 cobra 應用時,我們可以自行組織代碼目錄結構,但 cobra 建議如下目錄結構:
▾ appName/
▾ cmd/
add.go
your.go
commands.go
here.go
main.go
main.go
文件目的只有一個,初始化 cobra 應用:
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
Cobra 包常用功能
Cobra 包提供了多種功能,可以讓你構建一個優秀的應用。本小節,來詳細介紹 cobra 包支持的核心功能,及使用方式。
使用 cobra-cli
命令生成應用程序並添加子命令
我們可以選擇使用 cobra-cli
命令行工具,來快速生成一個應用程序,併爲其添加子命令,然後基於生成的代碼進行二次開發,提高開發效率,具體步驟如下:
- 生成應用程序
可以使用 cobra-cli init
命令初始化一個應用程序,然後我們就可以基於這個 Demo 程序做二次開發,提高開發效率。如下命令可以初始化一個新的應用程序:
$ mkdir -p cobrademo && cd cobrademo && go mod init
$ cobra-cli init --license=MIT --viper
$ ls
cmd go.mod go.sum LICENSE main.go
提示:如果遇到錯誤
Error: invalid character '{' after top-level value)'}'
,可參考:https://github.com/spf13/cobra-cli/issues/26。
當一個應用程序被初始化之後,就可以給這個應用程序添加一些命令:
$ cobra-cli add serve
$ cobra-cli add config
$ cobra-cli add create -p 'configCmd' # 此命令的父命令的變量名(默認爲 'rootCmd')
$ ls cmd/
config.go create.go root.go serve.go
執行 cobra-cli add
之後,會在 cmd
目錄下生成命令源碼文件。cobra-cli add
不僅可以添加命令,也可以添加子命令,例如上面的例子,通過 cobra-cli add create -p 'configCmd'
給 config
命令添加了 create
子命令,-p
指定子命令的父命令:<父命令>Cmd
。
- 編譯並執行
在生成完命令後,可以直接執行 go build
命令編譯應用程序:
$ go build -v .
$ go work use .
$ ./cobrademo -h
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:
cobrademo [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
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/.cobrademo.yaml)
-h, --help help for cobrademo
-t, --toggle Help message for toggle
Use "cobrademo [command] --help" for more information about a command.
$ ./cobrademo config -h
......
Usage:
cobrademo config [flags]
cobrademo config [command]
Available Commands:
create A brief description of your command
Flags:
-h, --help help for config
Global Flags:
--config string config file (default is $HOME/.cobrademo.yaml)
Use "cobrademo config [command] --help" for more information about a command.
這裏需要注意:命令名稱要是 camelCase
格式,而不是 snake_case
/ snake-case
格式,如果不是駝峯格式,cobra 會報錯。
- 配置 cobra
cobra 在生成應用程序時,也會在當前目錄下生成 LICENSE
文件,並且會在生成的 Go 源碼文件中,添加 LICENSE Header,LICENSE 和 LICENSE Header 的內容可以通過 cobra 配置文件進行配置,默認的配置文件爲:~/.cobra.yaml
,例如:
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
生成,根據此配置生成的 LICENSE 文件內容爲:
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.
LICENSE Header爲 :
/*
Copyright © 2020 Steve Francia <spf@spf13.com>
This file is part of CLI application foo.
*/
我們也可以使用內建的 licenses,內建的 licenses 有:GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD。例如,我們使用 MIT license:
$ cobra-cli init --license=MIT
使用 cobra 庫創建命令
如果要用 cobra 庫編碼實現一個應用程序,需要首選創建一個空的 main.go
文件和一個 rootCmd 文件,之後可以根據需要添加其它命令。具體步驟如下;
- 創建 rootCmd
$ mkdir -p cobrademolib && cd cobrademolib
通常情況下,我們會將 rootCmd
放在文件 cmd/root.go
中:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
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.Println(err)
os.Exit(1)
}
}
還可以在 init()
函數中定義標誌和處理配置,例如:cmd/helper.go
:
package cmd
import (
"fmt"
"os"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
cfgFile string
projectBase string
userLicense string
)
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $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")
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 initConfig() {
// Don't forget to read config either from cfgFile or from home directory!
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
if err := viper.ReadInConfig(); err != nil {
fmt.Println("Can't read config:", err)
os.Exit(1)
}
}
- 創建
main.go
我們還需要一個 main 函數來 調用 rootCmd,通常我們會創建一個 main.go
文件,在 main.go
中調用 rootCmd.Execute()
來執行命令:
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
需要注意,main.go
中不建議放很多代碼,通常只需要調用 cmd.Execute()
即可。
- 添加命令
除了 rootCmd
,我們還可以調用 AddCommand
添加其它命令,通常情況下,我們會把其它命令的源碼文件放在 cmd/
目錄下,例如,我們添加一個 version
命令,可以創建 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")
},
}
本示例中,我們通過調用 rootCmd.AddCommand(versionCmd)
給 rootCmd
命令添加了一個 versionCmd
命令。
- 編譯並運行
替換 main.go
中 {pathToYourApp}
爲對應的路徑,例如本示例中 pathToYourApp
爲 github.com/nosbelm/miniblogdemo/cobrademolib
。
$ go mod init
$ go work use .
$ go mod tidy
$ go build -v .
$ ./cobrademolib -h
通過步驟 1、2、3 我們就成功創建和添加了 cobra 應用程序和其命令。
使用標誌
cobra 可以跟 pflag 結合使用,實現強大的標誌功能。使用步驟如下:
- 使用持久化的標誌
標誌可以是 “持久的”,這意味着該標誌可用於它所分配的命令以及該命令下的每個子命令。可以在 rootCmd
上定義持久標誌:
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
- 使用本地標誌
也可以分配一個本地標誌,本地標誌只能在其所綁定的命令上使用:
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
--source
標誌只能在 rootCmd
上引用,而不能在 rootCmd
的子命令上引用。
- 將標誌綁定到 viper
我們可以將標誌綁定到 viper,這樣就可以使用 viper.Get()
獲取標誌的值。
var author string
func init() {
rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}
- 設置標誌爲必選
默認情況下,標誌是可選的,我們也可以設置標誌位必選,當設置標誌位必選,但是沒有提供標誌時,cobra 會報錯。
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
- 非選項參數驗證
在使用命令的過程中,經常會傳入非選項參數,並且需要對這些非選項參數進行驗證,cobra 提供了機制來對非選項參數進行驗證。可以使用 Command
的 Args
字段來驗證非選項參數。cobra 也內置了一些驗證函數,具體見下表:
使用自定義驗證函數,示例如下:
var cmd = &cobra.Command{
Short: "hello",
Args: cobra.MinimumNArgs(1), // 使用內置的驗證函數
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, World!")
},
}
也可以自定義驗證函數,示例如下:
var cmd = &cobra.Command{
Short: "hello",
// Args: cobra.MinimumNArgs(10), // 使用內置的驗證函數
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!")
},
}
Help 命令
在使用應用程序時,我們需要知道該應用程序的調用方法,所以需要有一個 Help 命令或者選項參數,Cobra 的強大之處也在於所有我們需要的功能 cobra 都已經幫我們實現好了。在用 cobra 構建應用程序時,cobra 會自動爲應用程序添加一個幫助命令,當用戶運行 app help
時會調用此方法。此外,當 help
的輸入爲其它命令時,會打印該命令的用法,比如有一個叫做 create
的命令,當調用 app help create
時,會打印 create
的幫助信息。cobra 也會給每個命令自動添加 --help
標誌。例如:
$ cobra-cli 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-cli [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
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-cli
-l, --license string name of license for the project
--viper use Viper for configuration
Use "cobra-cli [command] --help" for more information about a command.
我們也可以定義自己的 help
命令。使用如下函數,可以定義 help
命令:
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)
使用信息
當用戶提供無效標誌或無效命令時,cobra 會打印出 usage 信息。例如:
$ cobra-cli --invalid
Error: unknown flag: --invalid
Usage:
cobra-cli [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
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-cli
-l, --license string name of license for the project
--viper use Viper for configuration
Use "cobra-cli [command] --help" for more information about a command.
像 help
一樣,我們也可以自定義 usage,通過如下看函數可以自定義 usage:
cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)
version 標誌
如果在 rootCmd
命令上設置了 Version
字段,Cobra 會添加持久的 --version
標誌。運行應用程序時,指定了 --version
標誌,應用程序會使用 Version
模板將版本打印到 stdout。可以使用 cmd.SetVersionTemplate(s string)
函數自定義 Version
模板。
PreRun and PostRun Hooks
在運行 Run
函數時我們可以運行一些鉤子函數,比如 PersistentPreRun
和 PreRun
函數在 Run
函數之前執行。PersistentPostRun
和 PostRun
在 Run
函數之後執行。如果子命令沒有指定 Persistent*Run
函數,則子命令將會繼承父命令的 Persistent*Run
函數。這些函數的運行順序如下:
-
PersistentPreRun
; -
PreRun
; -
Run
; -
PostRun
; -
PersistentPostRun
。
注意父級的 PreRun
只會在父級命令運行時調用,子命令時不會調用的。
下面是使用所有這些函數的兩個命令的示例。執行子命令時,它將運行 rootCmd
命令的 PersistentPreRun
,但不運行 rootCmd
命令的 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()
}
執行後,輸出如下:
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]
命令建議
Cobra 還有很多其它有用的特性,比如當我們輸入的命令有誤時,cobra 會根據註冊的命令,推算出可能的命令。例如,當 unknown command
錯誤發生時,Cobra 將自動打印建議的命令:
$ hugo srever
Error: unknown command "srever" for "hugo"
Did you mean this?
server
Run 'hugo --help' for usage.
根據註冊的每個子命令自動建議並使用 Levenshtein distance 實現。每個匹配最小距離爲 2(忽略大小寫)的註冊命令將顯示爲建議。如果需要在命令中禁用建議或調整字符串距離,可以使用:
command.DisableSuggestions = true
或者:
command.SuggestionsMinimumDistance = 1
需要注意,Levenshtein distance(編輯距離)是針對二個字符串(例如英文字)的差異程度的量化量測,量測方式是看至少需要多少次的處理才能將一個字符串變成另一個字符串。
還可以使用 SuggestFor
屬性顯式設置要爲其指定命令的名稱,例如:
// configCmd represents the config command
var configCmd = &cobra.Command{
Use: "config",
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.`,
SuggestFor: []string{"cfg", "conf"},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("config called")
},
}
執行 newApp cfg
:
$ ./newApp cfg
Error: unknown command "cfg" for "newApp"
Did you mean this?
config
Run 'newApp --help' for usage.
unknown command "cfg" for "newApp"
Did you mean this?
config
總結
Go 語言開發中,都需要構建一個可執行的二進制文件。我們可以通過不同的方式來構建這個二進制程序。例如:可以首先 main 函數、可以藉助一些第三方的命令行框架包來實現。當前用的最多的是使用第三方的命令行框架來實現。命令行框架有很多,當前最受環境的是 cobra 包。
本文詳細介紹了 cobra 包的常見功能和使用方式。這些功能足以支撐你開發一個優秀的命令行工具或者 Go 應用。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3w9_W9X69jv1UsfyosgepA