cli
是一个用于构建命令行程序的库。咱们以前也介绍过一个用于构建命令行程序的库cobra
。在功能上来讲二者差很少,cobra
的优点是提供了一个脚手架,方便开发。cli
很是简洁,全部的初始化操做就是建立一个cli.App
结构的对象。经过为对象的字段赋值来添加相应的功能。linux
cli
与咱们上一篇文章介绍的negroni
是同一个做者urfave。git
cli
须要搭配 Go Modules 使用。建立目录并初始化:github
$ mkdir cli && cd cli
$ go mod init github.com/darjun/go-daily-lib/cli
复制代码
安装cli
库,有v1
和v2
两个版本。若是没有特殊需求,通常安装v2
版本:golang
$ go get -u github.com/urfave/cli/v2
复制代码
使用:微信
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
应用程序以下:app
func main() {
(&cli.App{}).Run(os.Args)
}
复制代码
可是这个空白程序没有什么用处。咱们的hello world
程序,设置了Name/Usage/Action
。Name
和Usage
都显示在帮助中,Action
是调用该命令行程序时实际执行的函数,须要的信息能够从参数cli.Context
获取。dom
编译、运行(环境:Win10 + Git Bash):ide
$ go build -o hello
$ ./hello
hello world
复制代码
除了这些,cli
为咱们额外生成了帮助信息:函数
$ ./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
的相关方法咱们能够获取传给命令行的参数信息:oop
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😄
个人博客:darjun.github.io
欢迎关注个人微信公众号【GoUpUp】,共同窗习,一块儿进步~