## 簡介linux
cli
是一個用於構建命令行程序的庫。咱們以前也介紹過一個用於構建命令行程序的庫cobra
。在功能上來講二者差很少,cobra
的優點是提供了一個腳手架,方便開發。cli
很是簡潔,全部的初始化操做就是建立一個cli.App
結構的對象。經過爲對象的字段賦值來添加相應的功能。git
cli
與咱們上一篇文章介紹的negroni
是同一個做者urfave。github
cli
須要搭配 Go Modules 使用。建立目錄並初始化:golang
$ mkdir cli && cd cli $ go mod init github.com/darjun/go-daily-lib/cli
安裝cli
庫,有v1
和v2
兩個版本。若是沒有特殊需求,通常安裝v2
版本:微信
$ go get -u github.com/urfave/cli/v2
使用:app
package main import ( "fmt" "log" "os" "github.com/urfave/cli/v2" ) func main() { app := &cli.App{ Name: "hello", Usage: "hello world example", Action: func(c *cli.Context) error { fmt.Println("hello world") return nil }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
使用很是簡單,理論上建立一個cli.App
結構的對象,而後調用其Run()
方法,傳入命令行的參數便可。一個空白的cli
應用程序以下:dom
func main() { (&cli.App{}).Run(os.Args) }
可是這個空白程序沒有什麼用處。咱們的hello world
程序,設置了Name/Usage/Action
。Name
和Usage
都顯示在幫助中,Action
是調用該命令行程序時實際執行的函數,須要的信息能夠從參數cli.Context
獲取。ide
編譯、運行(環境:Win10 + Git Bash):函數
$ go build -o hello $ ./hello hello world
除了這些,cli
爲咱們額外生成了幫助信息:oop
$ ./hello --help NAME: hello - hello world example USAGE: hello [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false)
經過cli.Context
的相關方法咱們能夠獲取傳給命令行的參數信息:
NArg()
:返回參數個數;Args()
:返回cli.Args
對象,調用其Get(i)
獲取位置i
上的參數。示例:
func main() { app := &cli.App{ Name: "arguments", Usage: "arguments example", Action: func(c *cli.Context) error { for i := 0; i < c.NArg(); i++ { fmt.Printf("%d: %s\n", i+1, c.Args().Get(i)) } return nil }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
這裏只是簡單輸出:
$ go run main.go hello world 1: hello 2: world
一個好用的命令行程序怎麼會少了選項呢?cli
設置和獲取選項很是簡單。在cli.App{}
結構初始化時,設置字段Flags
便可添加選項。Flags
字段是[]cli.Flag
類型,cli.Flag
其實是接口類型。cli
爲常見類型都實現了對應的XxxFlag
,如BoolFlag/DurationFlag/StringFlag
等。它們有一些共用的字段,Name/Value/Usage
(名稱/默認值/釋義)。看示例:
func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", }, }, Action: func(c *cli.Context) error { name := "world" if c.NArg() > 0 { name = c.Args().Get(0) } if c.String("lang") == "english" { fmt.Println("hello", name) } else { fmt.Println("你好", name) } return nil }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
上面是一個打招呼的命令行程序,可經過選項lang
指定語言,默認爲英語。設置選項爲非english
的值,使用漢語。若是有參數,使用第一個參數做爲人名,不然使用world
。注意選項是經過c.Type(name)
來獲取的,Type
爲選項類型,name
爲選項名。編譯、運行:
$ go build -o flags # 默認調用 $ ./flags hello world # 設置非英語 $ ./flags --lang chinese 你好 world # 傳入參數做爲人名 $ ./flags --lang chinese dj 你好 dj
咱們能夠經過./flags --help
來查看選項:
$ ./flags --help NAME: flags - A new cli application USAGE: flags [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --lang value language for the greeting (default: "english") --help, -h show help (default: false)
除了經過c.Type(name)
來獲取選項的值,咱們還能夠將選項存到某個預先定義好的變量中。只須要設置Destination
字段爲變量的地址便可:
func main() { var language string app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", Destination: &language, }, }, Action: func(c *cli.Context) error { name := "world" if c.NArg() > 0 { name = c.Args().Get(0) } if language == "english" { fmt.Println("hello", name) } else { fmt.Println("你好", name) } return nil }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
與上面的程序效果是同樣的。
cli
能夠在Usage
字段中爲選項設置佔位值,佔位值經過反引號 ` 包圍。只有第一個生效,其餘的維持不變。佔位值有助於生成易於理解的幫助信息:
func main() { app := & cli.App{ Flags : []cli.Flag { &cli.StringFlag{ Name:"config", Usage: "Load configuration from `FILE`", }, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
設置佔位值以後,幫助信息中,該佔位值會顯示在對應的選項後面,對短選項也是有效的:
$ go build -o placeholder $ ./placeholder --help NAME: placeholder - A new cli application USAGE: placeholder [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --config FILE Load configuration from FILE --help, -h show help (default: false)
選項能夠設置多個別名,設置對應選項的Aliases
字段便可:
func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Aliases: []string{"language", "l"}, Value: "english", Usage: "language for the greeting", }, }, Action: func(c *cli.Context) error { name := "world" if c.NArg() > 0 { name = c.Args().Get(0) } if c.String("lang") == "english" { fmt.Println("hello", name) } else { fmt.Println("你好", name) } return nil }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
使用--lang chinese
、--language chinese
和-l chinese
效果是同樣的。若是經過不一樣的名稱指定同一個選項,會報錯:
$ go build -o aliase $ ./aliase --lang chinese 你好 world $ ./aliase --language chinese 你好 world $ ./aliase -l chinese 你好 world $ ./aliase -l chinese --lang chinese Cannot use two forms of the same flag: l lang
除了經過執行程序時手動指定命令行選項,咱們還能夠讀取指定的環境變量做爲選項的值。只須要將環境變量的名字設置到選項對象的EnvVars
字段便可。能夠指定多個環境變量名字,cli
會依次查找,第一個有值的環境變量會被使用。
func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", EnvVars: []string{"APP_LANG", "SYSTEM_LANG"}, }, }, Action: func(c *cli.Context) error { if c.String("lang") == "english" { fmt.Println("hello") } else { fmt.Println("你好") } return nil }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
編譯、運行:
$ go build -o env $ APP_LANG=chinese ./env 你好
cli
還支持從文件中讀取選項的值,設置選項對象的FilePath
字段爲文件路徑:
func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", FilePath: "./lang.txt", }, }, Action: func(c *cli.Context) error { if c.String("lang") == "english" { fmt.Println("hello") } else { fmt.Println("你好") } return nil }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
在main.go
同級目錄建立一個lang.txt
,輸入內容chinese
。而後編譯運行程序:
$ go build -o file $ ./file 你好
cli
還支持從YAML/JSON/TOML
等配置文件中讀取選項值,這裏就不一一介紹了。
上面咱們介紹了幾種設置選項值的方式,若是同時有多個方式生效,按照下面的優先級從高到低設置:
咱們時常會遇到有多個短選項的狀況。例如 linux 命令ls -a -l
,能夠簡寫爲ls -al
。cli
也支持短選項合寫,只須要設置cli.App
的UseShortOptionHandling
字段爲true
便可:
func main() { app := &cli.App{ UseShortOptionHandling: true, Commands: []*cli.Command{ { Name: "short", Usage: "complete a task on the list", Flags: []cli.Flag{ &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}}, &cli.BoolFlag{Name: "option", Aliases: []string{"o"}}, &cli.BoolFlag{Name: "message", Aliases: []string{"m"}}, }, Action: func(c *cli.Context) error { fmt.Println("serve:", c.Bool("serve")) fmt.Println("option:", c.Bool("option")) fmt.Println("message:", c.Bool("message")) return nil }, }, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
編譯運行:
$ go build -o short $ ./short short -som "some message" serve: true option: true message: true
須要特別注意一點,設置UseShortOptionHandling
爲true
以後,咱們不能再經過-
指定選項了,這樣會產生歧義。例如-lang
,cli
不知道應該解釋爲l/a/n/g
4 個選項仍是lang
1 個。--
仍是有效的。
若是將選項的Required
字段設置爲true
,那麼該選項就是必要選項。必要選項必須指定,不然會報錯:
func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "lang", Value: "english", Usage: "language for the greeting", Required: true, }, }, Action: func(c *cli.Context) error { if c.String("lang") == "english" { fmt.Println("hello") } else { fmt.Println("你好") } return nil }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
不指定選項lang
運行:
$ ./required 2020/06/23 22:11:32 Required flag "lang" not set
默認狀況下,幫助文本中選項的默認值顯示爲Value
字段值。有些時候,Value
並非實際的默認值。這時,咱們能夠經過DefaultText
設置:
func main() { app := &cli.App{ Flags: []cli.Flag{ &cli.IntFlag{ Name: "port", Value: 0, Usage: "Use a randomized port", DefaultText :"random", }, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
上面代碼邏輯中,若是Value
設置爲 0 就隨機一個端口,這時幫助信息中default: 0
就容易產生誤解了。經過DefaultText
能夠避免這種狀況:
$ go build -o default-text $ ./default-text --help NAME: default-text - A new cli application USAGE: default-text [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --port value Use a randomized port (default: random) --help, -h show help (default: false)
子命令使命令行程序有更好的組織性。git
有大量的命令,不少以某個命令下的子命令存在。例如git remote
命令下有add/rename/remove
等子命令,git submodule
下有add/status/init/update
等子命令。
cli
經過設置cli.App
的Commands
字段添加命令,設置各個命令的SubCommands
字段,便可添加子命令。很是方便!
func main() { app := &cli.App{ Commands: []*cli.Command{ { Name: "add", Aliases: []string{"a"}, Usage: "add a task to the list", Action: func(c *cli.Context) error { fmt.Println("added task: ", c.Args().First()) return nil }, }, { Name: "complete", Aliases: []string{"c"}, Usage: "complete a task on the list", Action: func(c *cli.Context) error { fmt.Println("completed task: ", c.Args().First()) return nil }, }, { Name: "template", Aliases: []string{"t"}, Usage: "options for task templates", Subcommands: []*cli.Command{ { Name: "add", Usage: "add a new template", Action: func(c *cli.Context) error { fmt.Println("new task template: ", c.Args().First()) return nil }, }, { Name: "remove", Usage: "remove an existing template", Action: func(c *cli.Context) error { fmt.Println("removed task template: ", c.Args().First()) return nil }, }, }, }, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
上面定義了 3 個命令add/complete/template
,template
命令定義了 2 個子命令add/remove
。編譯、運行:
$ go build -o subcommand $ ./subcommand add dating added task: dating $ ./subcommand complete dating completed task: dating $ ./subcommand template add alarm new task template: alarm $ ./subcommand template remove alarm removed task template: alarm
注意一點,子命令默認不顯示在幫助信息中,須要顯式調用子命令所屬命令的幫助(./subcommand template --help
):
$ ./subcommand --help NAME: subcommand - A new cli application USAGE: subcommand [global options] command [command options] [arguments...] COMMANDS: add, a add a task to the list complete, c complete a task on the list template, t options for task templates help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) $ ./subcommand template --help NAME: subcommand template - options for task templates USAGE: subcommand template command [command options] [arguments...] COMMANDS: add add a new template remove remove an existing template help, h Shows a list of commands or help for one command OPTIONS: --help, -h show help (default: false)
在子命令數量不少的時候,能夠設置Category
字段爲它們分類,在幫助信息中會將相同分類的命令放在一塊兒展現:
func main() { app := &cli.App{ Commands: []*cli.Command{ { Name: "noop", Usage: "Usage for noop", }, { Name: "add", Category: "template", Usage: "Usage for add", }, { Name: "remove", Category: "template", Usage: "Usage for remove", }, }, } err := app.Run(os.Args) if err != nil { log.Fatal(err) } }
編譯、運行:
$ go build -o categories $ ./categories --help NAME: categories - A new cli application USAGE: categories [global options] command [command options] [arguments...] COMMANDS: noop Usage for noop help, h Shows a list of commands or help for one command template: add Usage for add remove Usage for remove GLOBAL OPTIONS: --help, -h show help (default: false)
看上面的COMMANDS
部分。
在cli
中全部的幫助信息文本均可以自定義,整個應用的幫助信息模板經過AppHelpTemplate
指定。命令的幫助信息模板經過CommandHelpTemplate
設置,子命令的幫助信息模板經過SubcommandHelpTemplate
設置。甚至能夠經過覆蓋cli.HelpPrinter
這個函數本身實現幫助信息輸出。下面程序在默認的幫助信息後添加我的網站和微信信息:
func main() { cli.AppHelpTemplate = fmt.Sprintf(`%s WEBSITE: http://darjun.github.io WECHAT: GoUpUp`, cli.AppHelpTemplate) (&cli.App{}).Run(os.Args) }
編譯運行:
$ go build -o help $ ./help --help NAME: help - A new cli application USAGE: help [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false) WEBSITE: http://darjun.github.io WECHAT: GoUpUp
咱們還能夠改寫整個模板:
func main() { cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: {{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} COPYRIGHT: {{.Copyright}} {{end}}{{if .Version}} VERSION: {{.Version}} {{end}} ` app := &cli.App{ Authors: []*cli.Author{ { Name: "dj", Email: "darjun@126.com", }, }, } app.Run(os.Args) }
{{.XXX}}
其中XXX
對應cli.App{}
結構中設置的字段,例如上面Authors
:
$ ./help --help NAME: help - A new cli application USAGE: help [global options] command [command options] [arguments...] AUTHOR: dj <darjun@126.com> COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help (default: false)
注意觀察AUTHOR
部分。
經過覆蓋HelpPrinter
,咱們能本身輸出幫助信息:
func main() { cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { fmt.Println("Simple help!") } (&cli.App{}).Run(os.Args) }
編譯、運行:
$ ./help --help Simple help!
默認狀況下,幫助選項爲--help/-h
。咱們能夠經過cli.HelpFlag
字段設置:
func main() { cli.HelpFlag = &cli.BoolFlag{ Name: "haaaaalp", Aliases: []string{"halp"}, Usage: "HALP", } (&cli.App{}).Run(os.Args) }
查看幫助:
$ go run main.go --halp NAME: main.exe - A new cli application USAGE: main.exe [global options] command [command options] [arguments...] COMMANDS: help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --haaaaalp, --halp HALP (default: false)
默認版本選項-v/--version
輸出應用的版本信息。咱們能夠經過cli.VersionFlag
設置版本選項 :
func main() { cli.VersionFlag = &cli.BoolFlag{ Name: "print-version", Aliases: []string{"V"}, Usage: "print only the version", } app := &cli.App{ Name: "version", Version: "v1.0.0", } app.Run(os.Args) }
這樣就能夠經過指定--print-version/-V
輸出版本信息了。運行:
$ go run main.go --print-version version version v1.0.0 $ go run main.go -V version version v1.0.0
咱們還能夠經過設置cli.VersionPrinter
字段控制版本信息的輸出內容:
const ( Revision = "0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b" ) func main() { cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) } app := &cli.App{ Name: "version", Version: "v1.0.0", } app.Run(os.Args) }
上面程序同時輸出版本號和git
提交的 SHA 值:
$ go run main.go -v version=v1.0.0 revision=0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b
cli
很是靈活,只須要設置cli.App
的字段值便可實現相應的功能,不須要額外記憶函數、方法。另外cli
還支持 Bash 自動補全的功能,對 zsh 的支持也比較好,感興趣可自行探索。
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~