本博客通过构建命令行工具to-do-list来介绍cobra的基本使用方式。工具的功能参考Task Tracker

获取cobra

1
go get -u github.com/spf13/cobra@latest

cobra.Command

创建工作目录,结构如下:

1
2
3
4
5
6
.
├── cmd
│ └── root.go
├── go.mod
├── go.sum
└── main.go

cmd目录下编写和命令行处理相关的模块,其中cobra中的核心就是cobra.Command结构体,以下是其最基本的的几个字段

  • Use:当前命令的用法(根命令不需要),并取其第一个单词作为命令的标识符
  • Short:当前命令的简短说明
  • Long:当前命令的长说明
  • Run:当前命令的处理函数,类型为func(cmd *cobra.Command,args []string)
  • RunE:当前命令的处理函数,类型为func(cmd *cobra.Command, args []string) error,返回的错误能够被Execute()方法返回

常用的方法:

  • Execute() error,解析命令行,并在cmd数中找到匹配的命令进行处理,返回可能的错误
  • AddCommand(cmd *cobra.Command),在当前命令下添加子命令

其他常用的字段和方法也会在后文中介绍到。

初始化项目

cmd/root.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cmd

import (
"fmt"
"github.com/spf13/cobra"
"os"
)

var rootCmd = &cobra.Command{
Short: "Todo App",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello ToDo_JSON! v1.0.0")
},
}

// 封装root命令
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

main.go中调用命令行处理接口。

1
2
3
4
5
6
7
8
9
package main

import (
"todo_json/cmd"
)

func main() {
cmd.Execute()
}

在工作根目录下执行以下命令验证:

1
2
3
4
5
6
7
8
9
10
11
$ go run .                                 
> Hello ToDo! v1.0.0

$ go run . -h
> Todo App

Usage:
[flags]

Flags:
-h, --help help for this command

当然也能先调用go build编译成可执行文件再进行命令行操作。

增加子命令

ToDo工具主要需要五个操作:addupdatedeletelistmark;可以将这些操作设计为五个子命令

项目目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── cmd
│ ├── add
│ │ └── add.go
│ ├── delete
│ │ └── delete.go
│ ├── list
│ │ └── list.go
│ ├── mark
│ │ └── mark.go
│ ├── root.go
│ └── update
│ └── update.go
├── go.mod
├── go.sum
└── main.go

cobra官方推荐通过新建子目录管理子命令模块

cmd/add/add.go为例,创建新的子命令addCmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package add

import (
"github.com/spf13/cobra"
)

var AddCmd = &cobra.Command{
Use: "add {task}",
Short: "add a new task",
Run: addFunc, // 通过`go run . add`可以调用
}

func addFunc(cmd *cobra.Command, args []string) {
// TO DO:
fmt.Println("using todo.add ...")
}

cmd/root.go中加入:

1
2
3
4
5
6
7
func init() {
rootCmd.AddCommand(add.AddCmd)
rootCmd.AddCommand(delete.DeleteCmd)
rootCmd.AddCommand(update.UpdateCmd)
rootCmd.AddCommand(list.ListCmd)
rootCmd.AddCommand(mark.MarkCmd)
}

命令行验证一下:

1
2
$ go run . add
> using todo.add ...

位置参数 Positional Args

在该部分,我们将实现各个命令的位置参数验证,以list子命令为例,我们规定:

1
2
3
4
todo_json list all 列出所有任务
todo_json list todo 列出todo状态的任务
todo_json list in-progress 列出in-progress状态的任务
todo_json list done 列出done状态的任务

Command中相关字段

cobra.Command中关于位置参数有两个常用的字段:

  • Args:类型为cobra.PositionalArgs,位置参数的验证器,可以指定位置参数的数量,内容等
  • ValidArgs:类型为[]string,指定合法的位置参数,可用作命令补全中

参数数量验证器

cobra提供了一些内置的参数验证器,如:

  • NoArgs,表明当前命令不需要位置参数,命令后第一个参数会尝试解析成子命令
    1
    2
    3
    4
    5
    6
    var AddCmd = &cobra.Command{
    Use: "add [task]",
    Short: "add a new task",
    Args: cobra.NoArgs,
    Run: addFunc,
    }
    测试结果:
    1
    2
    $ go run . add "something"
    > Error: unknown command "something" for " add" ...

其他参数数量验证器有:

  • ArbitraryArgs,表明可接受任意个参数,为Args的默认值(即Args为nil时仍使用ArbitraryArgs)
  • MinimumNArgs(n),表示最少接受n个参数,少于n个参数会报错
  • MaximumNArgs(n),表示最多接受n个参数,多于n个参数会报错
  • ExactArgs(n),表示只接受n个参数,多或少于n个参数会报错
  • RangeArgs(min, max),表示接受[min, max]个参数

参数内容验证器

还有参数内容验证其有:

  • OnlyValidArgs,会检测参数是否在ValidArgs

组合参数验证器

想要使用多个验证器时,可以使用MatchAll(pargs ...PositionalArgs)

这里完成ListCmd的参数校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const (
TODO = "todo"
IN_PROGRESS = "in-progress"
DONE = "done"
ALL = "all"
)

var ListCmd = &cobra.Command{
Use: "list {todo|in-progress|done}",
Short: "list the tasks of specified type",
ValidArgs: []string{TODO, IN_PROGRESS, DONE, ALL},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: ListFunc,
}

func ListFunc(cmd *cobra.Command, args []string) {
switch args[0] {
case TODO:
fmt.Println("List of todo:")
case IN_PROGRESS:
fmt.Println("List of in-progress:")
case DONE:
fmt.Println("List of done:")
case ALL:
fmt.Println("List of all:")
}
}

测试结果:

1
2
3
4
5
$ go run . list todo
> List of todo:

$ go run . list undone
> Error: invalid argument "undone" for " list"

自定义参数验证器

也可以通过自定义函数func(cmd *cobra.Command, args []string) error来实现自定义验证器,以update为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// cmd/update/update.go
var UpdateCmd = &cobra.Command{
Use: "update {id} {task}",
Short: "update specified task content",
Args: cobra.MatchAll(cobra.ExactArgs(2), utils.CheckNum(1)),
Run: UpdateFunc,
}

// utils/validation.go
func CheckNum(n int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
_, err := strconv.Atoi(args[n-1])
return err
}
}

选项/标志位 Flags

在这一部分,我们将使用选项来实现mark子命令,我们规定:

1
2
todo_json mark {id} --inprogress 将任务标记为in-progress 
todo_json mark {id} --done 将任务标记为done

Command中不同的FlagSet

在cobra中,每个命令(Command)都拥有几个Flags Set

  • Persistent Flags Set:在该标志集中定义的flag能够被子命令所继承
  • Inherit Flags Set:存放所有继承自父命令Persistent Flags SetInherit Flags Set的标志
  • Local Flags Set:等同于NonInherited Flags Set,存放命令本身定义的标记(包括本身的Persistent Flags Set),不包括继承的标记。
  • Flags Set:上述所有标记的并集。

获取对应标志集的方法:

  • cmd.PersistentFlags()
  • cmd.InheritedFlags()
  • cmd.LocalFlags()cmd.NonInheritedFlags()
  • cmd.Flags()

定义标记

向标志集定义标志/选项的方法,以String类型标记为例:

  • flagSet.String(tagName, defaultValue, usage),定义一个--tagName的String类型标签,并返回存放解析结果的变量指针*String
  • flagSet.StringP(tagName, shortHand, defaultValue, usage),与flagSet.String类似,但增加了短选项别名-shortHand,其中shortHand是一个字符
  • flagSet.StringVar(p *String, tagName, defaultValue, usage),会将解析结果保存在存入的p指针指向的变量中。
  • flagSet.StringVarP(p *String, tagName, shortHand, defaultValue, usage),与flagSet.StringVar类似,但增加了短选项别名-shortHand,其中shortHand是一个字符

类似的定义其他基本类型标记的方法有:

  • 整型类型(Int, Uint, Uint16等):如flagSet.Int(...), flagSet.Uint16VarP(...)
  • 浮点类型(Float32,Float64等):如flagSet.Float32(...)
  • 布尔类型:如:flagSet.Bool(...)

还有定义基本类型的切片类型标记的方法,如:flagSet.IntSlice(...), flagSet.IntSliceVar(...), flagSet.IntSliceVarP(...)

  1. 标志的定义必须在命令行解析前完成,一般在所在包的init函数中定义;在Command.RunCommand.RunE的函数体中定义会产生错误!
  2. flagSet.Flags()能获取所有继承和本地的标记集,但通过flag.Flags()定义的标记只会是本地标记,不会传给子命令

获取标记值

在命令行解析后获取相应的标记/选项的值,除了使用标记定义方法返回/传入的指针变量外,还可以通过flagSet.GetXXX(tagName)获取,其中XXX位相应标记的类型,如

1
2
3
4
5
if user, err := cmd.Flags().GetString("name"); err != nil {
fmt.Println(err.Error())
} else {
fmt.Println(user)
}

标记分组

cobra还允许开发者使用分组来实现不同标记之间的关系,如:

  • cmd.MarkFlagsRequiredTogether(tagName...), 要求某些标记必须在命令行同时出现
  • cmd.MarkFlagsMutuallyExclusive(tagName...),要求某些标记不能同时出现
  • cmd.MarkFlagsOneRequired(tagName...),要求某些标记至少出现一个

接下来,我们就可以利用上述的内容,完成mark命令的标记定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const (
IN_PROGRESS = "inprogress"
DONE = "done"
)

var (
in bool
done bool
)

var MarkCmd = &cobra.Command{
Use: "mark {id}",
Short: "mark the specified task in some state",
Args: cobra.MatchAll(cobra.ExactArgs(1), utils.CheckNum(1)),
Run: MarkFunc,
}

func MarkFunc(cmd *cobra.Command, args []string) {
if in {
fmt.Println("in progress")
} else if done {
fmt.Println("done")
}
}

func init() {
MarkCmd.Flags().BoolVar(&in, IN_PROGRESS, false, "mark task with in-progress status")
MarkCmd.Flags().BoolVar(&done, DONE, false, "mark task with done status")
MarkCmd.MarkFlagsMutuallyExclusive(IN_PROGRESS, DONE)
MarkCmd.MarkFlagsOneRequired(IN_PROGRESS, DONE)
}

测试结果:

1
2
3
4
5
6
7
8
$ go run . mark 1
Error: at least one of the flags in the group [inprogress done] is required

$ go run . mark 1 --inprogress --done
Error: if any flags in the group [inprogress done] are set none of the others can be; [done inprogress] were all set

$ go run . mark 1 --inprogress
in progress

以上就是Cobra的基础内容了,想利用Cobra的高级特性参考文档,todo_json的开源仓库