Go如何构建CLI工具


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提供了intVarStringVar等方法,可以对命令行参数进行解析和绑定,函数签名如下:

flag.IntVar

各个形参的含义分别为命令行标识位的名称、默认值和帮助信息。

命令行参数支持如下三种命令行标志语法:

  • -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.Parse CommandLine

flag包中存在一个全局变量CommandLine即一个空的命令集,后续命令的添加都是对这个命令集合的扩展。

os.Args[1:] = [-intFlag=10 -boolFlag -stringFlag xxf]

1.2.1.2 FlagSet.Parse

FlagSet.Parse是对解析方法的进一步封装,实际上解析逻辑放在了parseOne中,而解析过程中遇到的一些特殊情况,如重复解析、异常处理等,均直接由FlagSet.Parse进行处理。实际上,这是一个分层明显、结构清晰的方法设计,值得我们参考。

FlagSet.Parse
1.2.1.3 FlagSet.parseOne

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

FlagSet.parseOne

在上述代码中,主要是针对一些不符合命令行参数绑定规则的校验进行处理,大致分为以下四种情况:

  • 命令行参数长度为 0。
  • 长度小于 2 或不满足 flag 标识符“-”。
  • 如果flag标志位为“–”,则中断处理,并跳过该字符,也就是后续会以“-”进行处理。
  • 在处理flag标志位后,如果取到的参数名不符合规则,则也将中断处理。

在定位命令行参数节点上,采用的依据是根据“-”的索引定位解析出上下参数的名(name)和参数的值(value)。在设置参数值时,会对值类型进行判断。若是布尔类型,则调用定制的boolFlag类型进行判断和处理。最后,通过该flag提供的Value.Set方法将参数值设置到对应的flag中。


1.2.2 flag定义流

这部分比较简单,以IntVar为例。

IntVar newIntValue Var

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

FlagSet结构体定义

FlagSet

2 cobra框架

flag作为官方命令行工具用起来很方便,但是也存在功能上的不足,如不支持长短选项,得重复定义比较繁琐。

Cobra是Go的CLI框架。它包含一个用于创建强大的现代CLI应用程序的库,以及一个快速生成基于Cobra的应用程序和命令文件的工具。用起来十分简单方便,有很多著名的框架都是基于cobra的,截止 2021-12-21 在github上已经有 24k 个star了。

Cobra.Dev

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()
}

文章作者: xuxiangfei
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 xuxiangfei !
  目录