简介
cli是一个用于构建命令行程序的库。我们之前也介绍过一个用于构建命令行程序的库cobra。在功能上来说两者差不多,cobra的优势是提供了一个脚手架,方便开发。cli非常简洁,所有的初始化操作就是创建一个cli.App结构的对象。通过为对象的字段赋值来添加相应的功能。
cli与我们上一篇文章介绍的negroni是同一个作者urfave。

cli需要搭配 Go Modules 使用。创建目录并初始化:
$ 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
使用:
packagemainimport("fmt""log""os""github.com/urfave/cli/v2")funcmain(){app:=&cli.App{Name:"hello",Usage:"helloworldexample",Action:func(ccli.Context)error{fmt.Println("helloworld")returnnil},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
使用非常简单,理论上创建一个cli.App结构的对象,然后调用其Run()方法,传入命令行的参数即可。一个空白的cli应用程序如下:
funcmain(){(&cli.App{}).Run(os.Args)}
但是这个空白程序没有什么用处。我们的hello world程序,设置了Name/Usage/Action。Name和Usage都显示在帮助中,Action是调用该命令行程序时实际执行的函数,需要的信息可以从参数cli.Context获取。
编译、运行(环境:Win10 + Git Bash):
$ go build -o hello$ ./hellohello world
除了这些,cli为我们额外生成了帮助信息:
$ ./hello --helpNAME: hello - hello world exampleUSAGE: hello [global options] command [command options] [arguments...]COMMANDS: help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --help, -h show help (default: false)
参数
通过cli.Context的相关方法我们可以获取传给命令行的参数信息:
NArg():返回参数个数;Args():返回cli.Args对象,调用其Get(i)获取位置i上的参数。示例:
funcmain(){app:=&cli.App{Name:"arguments",Usage:"argumentsexample",Action:func(ccli.Context)error{fori:=0;i<c.NArg();i++{fmt.Printf("%d:%s\n",i+1,c.Args().Get(i))}returnnil},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
这里只是简单输出:
$ go run main.go hello world1: hello2: world
选项
一个好用的命令行程序怎么会少了选项呢?cli设置和获取选项非常简单。在cli.App{}结构初始化时,设置字段Flags即可添加选项。Flags字段是[]cli.Flag类型,cli.Flag实际上是接口类型。cli为常见类型都实现了对应的XxxFlag,如BoolFlag/DurationFlag/StringFlag等。它们有一些共用的字段,Name/Value/Usage(名称/默认值/释义)。看示例:
funcmain(){app:=&cli.App{Flags:[]cli.Flag{&cli.StringFlag{Name:"lang",Value:"english",Usage:"languageforthegreeting",},},Action:func(ccli.Context)error{name:="world"ifc.NArg()>0{name=c.Args().Get(0)}ifc.String("lang")=="english"{fmt.Println("hello",name)}else{fmt.Println("你好",name)}returnnil},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
上面是一个打招呼的命令行程序,可通过选项lang指定语言,默认为英语。设置选项为非english的值,使用汉语。如果有参数,使用第一个参数作为人名,否则使用world。注意选项是通过c.Type(name)来获取的,Type为选项类型,name为选项名。编译、运行:
$ go build -o flags# 默认调用$ ./flagshello world# 设置非英语$ ./flags --lang chinese你好 world# 传入参数作为人名$ ./flags --lang chinese dj你好 dj
我们可以通过./flags --help来查看选项:
$ ./flags --helpNAME: flags - A new cli applicationUSAGE: flags [global options] command [command options] [arguments...]COMMANDS: help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --lang value language for the greeting (default: "english") --help, -h show help (default: false)
存入变量
除了通过c.Type(name)来获取选项的值,我们还可以将选项存到某个预先定义好的变量中。只需要设置Destination字段为变量的地址即可:
funcmain(){varlanguagestringapp:=&cli.App{Flags:[]cli.Flag{&cli.StringFlag{Name:"lang",Value:"english",Usage:"languageforthegreeting",Destination:&language,},},Action:func(ccli.Context)error{name:="world"ifc.NArg()>0{name=c.Args().Get(0)}iflanguage=="english"{fmt.Println("hello",name)}else{fmt.Println("你好",name)}returnnil},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
与上面的程序效果是一样的。
占位值cli可以在Usage字段中为选项设置占位值,占位值通过反引号 ` 包围。只有第一个生效,其他的维持不变。占位值有助于生成易于理解的帮助信息:
funcmain(){app:=&cli.App{Flags:[]cli.Flag{&cli.StringFlag{Name:"config",Usage:"Loadconfigurationfrom`FILE`",},},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
设置占位值之后,帮助信息中,该占位值会显示在对应的选项后面,对短选项也是有效的:
$ go build -o placeholder$ ./placeholder --helpNAME: placeholder - A new cli applicationUSAGE: placeholder [global options] command [command options] [arguments...]COMMANDS: help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --config FILE Load configuration from FILE --help, -h show help (default: false)
别名
选项可以设置多个别名,设置对应选项的Aliases字段即可:
funcmain(){app:=&cli.App{Flags:[]cli.Flag{&cli.StringFlag{Name:"lang",Aliases:[]string{"language","l"},Value:"english",Usage:"languageforthegreeting",},},Action:func(ccli.Context)error{name:="world"ifc.NArg()>0{name=c.Args().Get(0)}ifc.String("lang")=="english"{fmt.Println("hello",name)}else{fmt.Println("你好",name)}returnnil},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
使用--lang chinese、--language chinese和-l chinese效果是一样的。如果通过不同的名称指定同一个选项,会报错:
$gobuild-oaliase$./aliase--langchinese你好world$./aliase--languagechinese你好world$./aliase-lchinese你好world$./aliase-lchinese--langchineseCannotusetwoformsofthesameflag:llang
环境变量
除了通过执行程序时手动指定命令行选项,我们还可以读取指定的环境变量作为选项的值。只需要将环境变量的名字设置到选项对象的EnvVars字段即可。可以指定多个环境变量名字,cli会依次查找,第一个有值的环境变量会被使用。
funcmain(){app:=&cli.App{Flags:[]cli.Flag{&cli.StringFlag{Name:"lang",Value:"english",Usage:"languageforthegreeting",EnvVars:[]string{"APP_LANG","SYSTEM_LANG"},},},Action:func(ccli.Context)error{ifc.String("lang")=="english"{fmt.Println("hello")}else{fmt.Println("你好")}returnnil},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
编译、运行:
$ go build -o env$ APP_LANG=chinese ./env你好
文件
cli还支持从文件中读取选项的值,设置选项对象的FilePath字段为文件路径:
funcmain(){app:=&cli.App{Flags:[]cli.Flag{&cli.StringFlag{Name:"lang",Value:"english",Usage:"languageforthegreeting",FilePath:"./lang.txt",},},Action:func(ccli.Context)error{ifc.String("lang")=="english"{fmt.Println("hello")}else{fmt.Println("你好")}returnnil},}err:=app.Run(os.Args)iferr!=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即可:
funcmain(){app:=&cli.App{UseShortOptionHandling:true,Commands:[]cli.Command{{Name:"short",Usage:"completeataskonthelist",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(ccli.Context)error{fmt.Println("serve:",c.Bool("serve"))fmt.Println("option:",c.Bool("option"))fmt.Println("message:",c.Bool("message"))returnnil},},},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
编译运行:
$ go build -o short$ ./short short -som "some message"serve: trueoption: truemessage: true
需要特别注意一点,设置UseShortOptionHandling为true之后,我们不能再通过-指定选项了,这样会产生歧义。例如-lang,cli不知道应该解释为l/a/n/g 4 个选项还是lang 1 个。--还是有效的。
必要选项如果将选项的Required字段设置为true,那么该选项就是必要选项。必要选项必须指定,否则会报错:
funcmain(){app:=&cli.App{Flags:[]cli.Flag{&cli.StringFlag{Name:"lang",Value:"english",Usage:"languageforthegreeting",Required:true,},},Action:func(ccli.Context)error{ifc.String("lang")=="english"{fmt.Println("hello")}else{fmt.Println("你好")}returnnil},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
不指定选项lang运行:
$ ./required2020/06/23 22:11:32 Required flag "lang" not set
帮助文本中的默认值
默认情况下,帮助文本中选项的默认值显示为Value字段值。有些时候,Value并不是实际的默认值。这时,我们可以通过DefaultText设置:
funcmain(){app:=&cli.App{Flags:[]cli.Flag{&cli.IntFlag{Name:"port",Value:0,Usage:"Usearandomizedport",DefaultText:"random",},},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
上面代码逻辑中,如果Value设置为 0 就随机一个端口,这时帮助信息中default: 0就容易产生误解了。通过DefaultText可以避免这种情况:
$ go build -o default-text$ ./default-text --helpNAME: default-text - A new cli applicationUSAGE: default-text [global options] command [command options] [arguments...]COMMANDS: help, h Shows a list of commands or help for one commandGLOBAL 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字段,即可添加子命令。非常方便!
funcmain(){app:=&cli.App{Commands:[]cli.Command{{Name:"add",Aliases:[]string{"a"},Usage:"addatasktothelist",Action:func(ccli.Context)error{fmt.Println("addedtask:",c.Args().First())returnnil},},{Name:"complete",Aliases:[]string{"c"},Usage:"completeataskonthelist",Action:func(ccli.Context)error{fmt.Println("completedtask:",c.Args().First())returnnil},},{Name:"template",Aliases:[]string{"t"},Usage:"optionsfortasktemplates",Subcommands:[]cli.Command{{Name:"add",Usage:"addanewtemplate",Action:func(ccli.Context)error{fmt.Println("newtasktemplate:",c.Args().First())returnnil},},{Name:"remove",Usage:"removeanexistingtemplate",Action:func(ccli.Context)error{fmt.Println("removedtasktemplate:",c.Args().First())returnnil},},},},},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
上面定义了 3 个命令add/complete/template,template命令定义了 2 个子命令add/remove。编译、运行:
$ go build -o subcommand$ ./subcommand add datingadded task: dating$ ./subcommand complete datingcompleted task: dating$ ./subcommand template add alarmnew task template: alarm$ ./subcommand template remove alarmremoved task template: alarm
注意一点,子命令默认不显示在帮助信息中,需要显式调用子命令所属命令的帮助(./subcommand template --help):
$ ./subcommand --helpNAME: subcommand - A new cli applicationUSAGE: 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 commandGLOBAL OPTIONS: --help, -h show help (default: false)$ ./subcommand template --helpNAME: subcommand template - options for task templatesUSAGE: 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 commandOPTIONS: --help, -h show help (default: false)
分类
在子命令数量很多的时候,可以设置Category字段为它们分类,在帮助信息中会将相同分类的命令放在一起展示:
funcmain(){app:=&cli.App{Commands:[]cli.Command{{Name:"noop",Usage:"Usagefornoop",},{Name:"add",Category:"template",Usage:"Usageforadd",},{Name:"remove",Category:"template",Usage:"Usageforremove",},},}err:=app.Run(os.Args)iferr!=nil{log.Fatal(err)}}
编译、运行:
$ go build -o categories$ ./categories --helpNAME: categories - A new cli applicationUSAGE: 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 removeGLOBAL OPTIONS: --help, -h show help (default: false)
看上面的COMMANDS部分。
自定义帮助信息在cli中所有的帮助信息文本都可以自定义,整个应用的帮助信息模板通过AppHelpTemplate指定。命令的帮助信息模板通过CommandHelpTemplate设置,子命令的帮助信息模板通过SubcommandHelpTemplate设置。甚至可以通过覆盖cli.HelpPrinter这个函数自己实现帮助信息输出。下面程序在默认的帮助信息后添加个人网站和微信信息:
funcmain(){cli.AppHelpTemplate=fmt.Sprintf(`%sWEBSITE:http://darjun.github.ioWECHAT:GoUpUp`,cli.AppHelpTemplate)(&cli.App{}).Run(os.Args)}
编译运行:
$ go build -o help$ ./help --helpNAME: help - A new cli applicationUSAGE: help [global options] command [command options] [arguments...]COMMANDS: help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --help, -h show help (default: false)WEBSITE: http://darjun.github.ioWECHAT: GoUpUp
我们还可以改写整个模板:
funcmain(){cli.AppHelpTemplate=`NAME:{{.Name}}-{{.Usage}}USAGE:{{.HelpName}}{{if.VisibleFlags}}[globaloptions]{{end}}{{if.Commands}}command[commandoptions]{{end}}{{if.ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{iflen.Authors}}AUTHOR:{{range.Authors}}{{.}}{{end}}{{end}}{{if.Commands}}COMMANDS:{{range.Commands}}{{ifnot.HideHelp}}{{join.Names","}}{{"\t"}}{{.Usage}}{{"\n"}}{{end}}{{end}}{{end}}{{if.VisibleFlags}}GLOBALOPTIONS:{{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 --helpNAME: help - A new cli applicationUSAGE: help [global options] command [command options] [arguments...]AUTHOR: dj <darjun@126.com>COMMANDS: help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --help, -h show help (default: false)
注意观察AUTHOR部分。
通过覆盖HelpPrinter,我们能自己输出帮助信息:
funcmain(){cli.HelpPrinter=func(wio.Writer,templstring,datainterface{}){fmt.Println("Simplehelp!")}(&cli.App{}).Run(os.Args)}
编译、运行:
$ ./help --helpSimple help!
内置选项帮助选项
默认情况下,帮助选项为--help/-h。我们可以通过cli.HelpFlag字段设置:
funcmain(){cli.HelpFlag=&cli.BoolFlag{Name:"haaaaalp",Aliases:[]string{"halp"},Usage:"HALP",}(&cli.App{}).Run(os.Args)}
查看帮助:
$ go run main.go --halpNAME: main.exe - A new cli applicationUSAGE: main.exe [global options] command [command options] [arguments...]COMMANDS: help, h Shows a list of commands or help for one commandGLOBAL OPTIONS: --haaaaalp, --halp HALP (default: false)
版本选项
默认版本选项-v/--version输出应用的版本信息。我们可以通过cli.VersionFlag设置版本选项 :
funcmain(){cli.VersionFlag=&cli.BoolFlag{Name:"print-version",Aliases:[]string{"V"},Usage:"printonlytheversion",}app:=&cli.App{Name:"version",Version:"v1.0.0",}app.Run(os.Args)}
这样就可以通过指定--print-version/-V输出版本信息了。运行:
$ go run main.go --print-versionversion version v1.0.0$ go run main.go -Vversion version v1.0.0
我们还可以通过设置cli.VersionPrinter字段控制版本信息的输出内容:
const(Revision="0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b")funcmain(){cli.VersionPrinter=func(ccli.Context){fmt.Printf("version=%srevision=%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 -vversion=v1.0.0 revision=0cebd6e32a4e7094bbdbf150a1c2ffa56c34e91b
总结
cli非常灵活,只需要设置cli.App的字段值即可实现相应的功能,不需要额外记忆函数、方法。另外cli还支持 Bash 自动补全的功能,对 zsh 的支持也比较好,感兴趣可自行探索。
大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue
参考cli GitHub:https://github.com/urfave/cliGo 每日一库 GitHub:https://github.com/darjun/go-daily-lib