Go如何构建CLI工具
参考
flag官方文档
cobra官方文档
cobra github
《Go语言编程之旅》第一章命令行应用
很多Go的应用程序是通过命令行进行交互,即CLI(command-line interface)
命令行接口,go的标准库flag
提供了命令行参数解析的能力,让我们在开发过程中能够非常方便地解析和处理命令行参数。目前也有许多开源项目提供了快速构建CLI应用程序的能力,如Cobra
,下面介绍如何使用这些库构建CLI工具。
1 flag包
1.1 使用方式
1.1.1 基本使用
package main
import (
"flag"
"fmt"
)
var (
intFlag int
boolFlag bool
stringFlag string
)
func init() {
flag.IntVar(&intFlag, "intFlag", 0, "int flag")
flag.BoolVar(&boolFlag, "boolFlag", false, "bool flag")
flag.StringVar(&stringFlag, "stringFlag", "default", "string flag")
flag.Parse()
}
func main() {
fmt.Println(intFlag, boolFlag, stringFlag)
}
flag
提供了intVar
、StringVar
等方法,可以对命令行参数进行解析和绑定,函数签名如下:

各个形参的含义分别为命令行标识位的名称、默认值和帮助信息。
命令行参数支持如下三种命令行标志语法:
-flag
:仅支持布尔类型。-flag x
:仅支持非布尔类型。-flag=x
:都支持。
$ go run main.go -intFlag 10 -boolFlag -stringFlag xxf
output: 10 true xxf
该例子中使用了-flag 和 -flag x,其中-boolFlag表示该布尔值为True
$ go run main.go -intFlag 10 -stringFlag xxf
output: 10 false xxf
该例子中使用了-flag 和 -flag x,其中没有-boolFlag则默认该布尔值为False
$ go run main.go -intFlag=10 -boolFlag=true -stringFlag=xxf
output: 10 true xxf
该例子中使用了-flag=x
1.1.2 长短选项
flag.IntVar(&intFlag, "intFlag", 0, "int flag")
flag.IntVar(&intFlag, "i", 0, "int flag")
$ go run main.go -i 10 -boolFlag=true -stringFlag=xxf
output: 10 true xxf
一个命令行参数的标志位有长短选项是常规需求,使用flag的话可以调用两次绑定操作。
1.1.3 子命令
在日常使用的CLI应用程序中,最常见的功能是子命令的使用。一个工具可能包含了大量相关联的功能命令,以此形成工具集,可以说是刚需,那么这个功能在标准库flag中是如何实现的呢?
package main
import (
"flag"
"fmt"
)
var (
intFlag int
boolFlag bool
stringFlag string
)
func main() {
gocmd := flag.NewFlagSet("go", flag.ExitOnError)
gocmd.IntVar(&intFlag, "intFlag", 0, "int flag")
gocmd.BoolVar(&boolFlag, "boolFlag", false, "bool flag")
phpcmd := flag.NewFlagSet("php", flag.ExitOnError)
phpcmd.StringVar(&stringFlag, "stringFlag", "default", "string flag")
flag.Parse()
args := flag.Args()
if len(args) <= 0 {
return
}
switch args[0] {
case "go":
_ = gocmd.Parse(args[1:])
case "php":
_ = phpcmd.Parse(args[1:])
}
fmt.Println(intFlag, boolFlag, stringFlag)
}
$ go run main.go go -intFlag=10 -boolFlag=true
10 true default
$ go run main.go php -stringFlag=xxf
0 false xxf
由于需要处理子命令,因此调用了flag.NewFlagSet
方法。该方法会返回带有指定名称和错误处理属性的空命令集,相当于创建一个新的命令集去支持子命令。
1.2 实现原理
1.2.1 参数解析流

还是以命令 go run main.go -intFlag=10 -boolFlag -stringFlag xxf
为例介绍参数解析流。
fmt.Println(os.Args)
// [/var/folders/95/d2xwj_5n4l94l392w1p6tsv00000gn/T/go-build2398467023/b001/exe/main -intFlag=10 -boolFlag -stringFlag xxf]
1.2.1.1 flag.Parse
命令行输入参数后,首先会调用flag.Parse
,其功能是解析并绑定命令行参数。


flag
包中存在一个全局变量CommandLine
即一个空的命令集,后续命令的添加都是对这个命令集合的扩展。
os.Args[1:] = [-intFlag=10 -boolFlag -stringFlag xxf]
1.2.1.2 FlagSet.Parse
FlagSet.Parse
是对解析方法的进一步封装,实际上解析逻辑放在了parseOne
中,而解析过程中遇到的一些特殊情况,如重复解析、异常处理等,均直接由FlagSet.Parse
进行处理。实际上,这是一个分层明显、结构清晰的方法设计,值得我们参考。

1.2.1.3 FlagSet.parseOne
FlagSet.parseOne
是命令行解析的核心方法,所有的命令最后都会流转到 FlagSet.parseOne
中进行处理,这个函数有 73 行,这里只放部分代码。

在上述代码中,主要是针对一些不符合命令行参数绑定规则的校验进行处理,大致分为以下四种情况:
- 命令行参数长度为 0。
- 长度小于 2 或不满足 flag 标识符“-”。
- 如果flag标志位为“–”,则中断处理,并跳过该字符,也就是后续会以“-”进行处理。
- 在处理flag标志位后,如果取到的参数名不符合规则,则也将中断处理。
在定位命令行参数节点上,采用的依据是根据“-”的索引定位解析出上下参数的名(name)和参数的值(value)。在设置参数值时,会对值类型进行判断。若是布尔类型,则调用定制的boolFlag类型进行判断和处理。最后,通过该flag提供的Value.Set方法将参数值设置到对应的flag中。
1.2.2 flag定义流
这部分比较简单,以IntVar为例。



首先调用newIntValue
将value
与指针联系起来,然后创建一个Flag
对象并将其绑定到FlagSet对象中,通过map
存储与去重。
FlagSet
结构体定义

2 cobra框架
flag
作为官方命令行工具用起来很方便,但是也存在功能上的不足,如不支持长短选项,得重复定义比较繁琐。
Cobra
是Go的CLI框架。它包含一个用于创建强大的现代CLI应用程序的库,以及一个快速生成基于Cobra的应用程序和命令文件的工具。用起来十分简单方便,有很多著名的框架都是基于cobra的,截止 2021-12-21 在github上已经有 24k 个star了。
https://github.com/spf13/cobra
安装
go get -u github.com/spf13/cobra/cobra
import "github.com/spf13/cobra"
2.1 使用方式
2.1.1 组织结构
一般在项目中,会将命令行指令封装在cmd
目录下,然后在main.go
中初始化
▾ appName/
▾ cmd/
commands.go
here.go
main.go
--------------------------------------------------
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
2.1.2 example
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var (
version string
boolFlag bool
)
// flag在init函数中调用,可以设置长短命令
func init() {
rootCmd.Flags().BoolVarP(&boolFlag, "boolFlag", "b", false, "The bool flag")
rootCmd.Flags().StringVarP(&version, "version", "v", "1.0.1", "The tool version")
}
// 创建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) {
fmt.Println("boolFlag", boolFlag)
fmt.Println("version", version)
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func main() {
Execute()
}