简介

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库,有v1v2两个版本。如果没有特殊需求,一般安装v2版本:

$ go get -u github.com/urfave/cli/v2

使用:

package mainimport ("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应用程序如下:

func main() {(&cli.App{}).Run(os.Args)
}

但是这个空白程序没有什么用处。我们的hello world程序,设置了Name/Usage/ActionNameUsage都显示在帮助中,Action是调用该命令行程序时实际执行的函数,需要的信息可以从参数cli.Context获取。

编译、运行(环境:Win10 + Git Bash):

$ go build -o hello
$ ./hello
hello world

除了这些,cli为我们额外生成了帮助信息:

$ ./hello --help
NAME: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上的参数。

示例:

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 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字段为变量的地址即可:

func main() {var language stringapp := &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 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字段即可:

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 -alcli也支持短选项合写,只需要设置cli.AppUseShortOptionHandling字段为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

需要特别注意一点,设置UseShortOptionHandlingtrue之后,我们不能再通过-指定选项了,这样会产生歧义。例如-langcli不知道应该解释为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 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.AppCommands字段添加命令,设置各个命令的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/templatetemplate命令定义了 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 applicationUSAGE:subcommand [global options] command [command options] [arguments...]COMMANDS:add, a       add a task to the listcomplete, c  complete a task on the listtemplate, t  options for task templateshelp, h      Shows a list of commands or help for one commandGLOBAL OPTIONS:--help, -h  show help (default: false)$ ./subcommand template --help
NAME:subcommand template - options for task templatesUSAGE:subcommand template command [command options] [arguments...]COMMANDS:add      add a new templateremove   remove an existing templatehelp, h  Shows a list of commands or help for one commandOPTIONS:--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 applicationUSAGE:categories [global options] command [command options] [arguments...]COMMANDS:noop     Usage for noophelp, h  Shows a list of commands or help for one commandtemplate:add     Usage for addremove  Usage for removeGLOBAL OPTIONS:--help, -h  show help (default: false)

看上面的COMMANDS部分。

自定义帮助信息

cli中所有的帮助信息文本都可以自定义,整个应用的帮助信息模板通过AppHelpTemplate指定。命令的帮助信息模板通过CommandHelpTemplate设置,子命令的帮助信息模板通过SubcommandHelpTemplate设置。甚至可以通过覆盖cli.HelpPrinter这个函数自己实现帮助信息输出。下面程序在默认的帮助信息后添加个人网站和微信信息:

func main() {cli.AppHelpTemplate = fmt.Sprintf(`%sWEBSITE: http://darjun.github.ioWECHAT: GoUpUp`, cli.AppHelpTemplate)(&cli.App{}).Run(os.Args)
}

编译运行:

$ go build -o help
$ ./help --help
NAME: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

我们还可以改写整个模板:

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 commandGLOBAL 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 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设置版本选项 :

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

Go 每日一库之 cli相关推荐

  1. go get 失败 no go files in_Go 每日一库之 dig

    简介 今天我们来介绍 Go 语言的一个依赖注入(DI)库--dig.dig 是 uber 开源的库.Java 依赖注入的库有很多,相信即使不是做 Java 开发的童鞋也听过大名鼎鼎的 Spring.相 ...

  2. go float64 比较_Go 每日一库之 plot

    Go 每日一库之 plot 简介 本文介绍 Go 语言的一个非常强大.好用的绘图库--plot.plot内置了很多常用的组件,基本满足日常需求.同时,它也提供了定制化的接口,可以实现我们的个性化需求. ...

  3. go get 的不再src目录中_Go 每日一库之 sqlc:根据 sql 生成代码

    简介 在 Go 语言中编写数据库操作代码真的非常痛苦!database/sql标准库提供的都是比较底层的接口.我们需要编写大量重复的代码.大量的模板代码不仅写起来烦,而且还容易出错.有时候字段类型修改 ...

  4. go 默认http版本_【每日一库】超赞的 Go 语言 INI 文件操作

    点击上方蓝色"Go语言中文网"关注我们,领全套Go资料,每天学习 Go 语言 如果你使用 INI 作为系统的配置文件,那么一定会使用这个库吧.没错,它就是号称地表 最强大.最方便  ...

  5. go get如何删除_Go 每日一库之 xorm

    简介 Go 标准库提供的数据库接口database/sql比较底层,使用它来操作数据库非常繁琐,而且容易出错.因而社区开源了不少第三方库,如上一篇文章中的sqlc工具,还有各式各样的 ORM (Obj ...

  6. go 根据输入类型执行对应的方法_Go 每日一库之 sqlc

    简介 在 Go 语言中编写数据库操作代码真的非常痛苦!database/sql标准库提供的都是比较底层的接口.我们需要编写大量重复的代码.大量的模板代码不仅写起来烦,而且还容易出错.有时候字段类型修改 ...

  7. Go 每日一库之 zap

    转载地址:Go 每日一库之 zap - SegmentFault 思否 简介 在很早之前的文章中,我们介绍过 Go 标准日志库log和结构化的日志库logrus.在热点函数中记录日志对日志库的执行性能 ...

  8. 每日一库之Go 强大而灵活的电子邮件库:email

    发送邮件是一个很常见的需求:用户邮箱验证.邮箱召回等.Go 语言标准库自带 net/smtp 库,实现了 smtp 协议,用于发送邮件.然而这个库比较原始,使用不方便,而且官方声明不再增加新功能.于是 ...

  9. Go 每日一库之 xorm

    简介 Go 标准库提供的数据库接口database/sql比较底层,使用它来操作数据库非常繁琐,而且容易出错.因而社区开源了不少第三方库,如上一篇文章中的sqlc工具,还有各式各样的 ORM (Obj ...

最新文章

  1. dask想说爱你不容易
  2. IDC:2015年第四季度全球WLAN市场增幅最高
  3. 深入浅出 CPropertySheet
  4. 单IP无TMG拓扑Lync Server 2013:边缘服务器
  5. TotoiseSVN的基本使用方法
  6. OpenCV在ARM上的移植
  7. rhel4 x86_64 php5.2.17 make安装 支持mysqli
  8. 测试身体素质健康的软件,《体质健康测试与评价》手机应用(App)的开发及应用...
  9. esri-leaflet入门教程(5)- 动态绘制图形
  10. python mockito arg_that_编程高阶用法–开发者高频词汇
  11. 思考的乐趣-Matrix67数学笔记
  12. 行业报告归档 2019.2.8
  13. 10000亿的暴利:数字化营销今生与未来
  14. 计算机辅助翻译与人工智能,2018年机器翻译行业概述与现状,人工智能让人人实现国际化交流...
  15. 小程序源码:经典语录大全微信小程序源码下载多种分类语录-多玩法安装简单
  16. C++的游戏--贪吃蛇
  17. 语音转换成文本 技术实现_职业转换者指南,帮助您实现梦想的技术工作
  18. 2019 ICPC 上海站网络赛 K.Peekaboo (圆上整点)
  19. 字符0、数字0和‘\0’的区别
  20. 如何克服焦虑,不安,紧张

热门文章

  1. Unity3D 修改动画,资源文件设置
  2. Binary Tree 非递归遍历模板小结(经典!)
  3. 毫秒级查询的离线IP地址定位库,太实用了!
  4. 前端已死? 2023 年前端十大 Web 趋势
  5. promise 格式
  6. java+Vue大学生就业求职系统ssm企业职位推荐系统毕业设计源码介绍
  7. PCM 转WAV。 2. 多通道pcm挑出其中
  8. 百度网盘+Visual SVN Server(windows)搭建SVN个人仓库
  9. 寻成都游戏(游戏相关工具)开发工作
  10. dcm文件读取 java_使用Python对Dicom文件进行读取与写入