Contents

浅析现代化命令行框架 Cobra

导语
Cobra 是一个可以创建强大的现代化 CLI 应用程序库,它还提供了一个可以生成应用和命令文件的程序的命令行工具:cobra-cli.

I. Cobra 简介

Cobra 是一个可以创建强大的现代化 CLI 应用程序库,它还提供了一个可以生成应用和命令文件的程序的命令行工具:cobra-cli。 许多大型项目(e.g. kubernetes, Docker, Etcd, Rkt, Hugo etc.)都采用了 cobra 来构建他们的应用程序。

Cobra 具有很多特性,一些核心特性如下:

  • 可以构建基于子命令的 CLI,并支持嵌套子命令:app server, app fetch
  • 可以通过 cobra-cli init appname & cobra-cli add cmdname 轻松生成应用和子命令
  • 智能化命令建议:app srver...did you mean app server
  • 自动生成命令和标志的 helpe 文本,并能自动识别 -h, --help 等标志
  • 自动为应用程序生成 bash、zsh、fish、powershell 自动补全脚本
  • 支持命令别名、自定义帮助、自定义用法等
  • 可以与 viper、pflag 紧密集成,用于构建 12-factor 应用程序

Cobra 建立在 commands、arguments 和 flags 结构之上。Commands 代表命令,arguments 代表非选项参数,flags 代表选项参数(标志)。

CLI 模式

一个好的应用程序应该是易懂的,用户可以清晰知道如何去使用这个应用程序,因此通常遵循如下模式: APPNAME VERB NOUN --ADJECTIVE 或者 APPNAME COMMAND ARG --FLAG,例如:

1
2
3
4
# clone 是一个 Commands
# URL 是一个非选项参数
# bare 一个选项参数
git clone URL --bare

NOTE:VERB 代表动词,NOUN 代表名词,ADJECTIVE 代表形容词

II. cobra-cli 命令安装

Cobra 提供了 cobra-cli 命令,用来初始化一个应用程序并为其添加命令,方便开发基于 Cobra 的应用,可用以下方法进行安装:

1
2
$ go install github.com/spf13/cobra-cli@latest
# ...

cobra-cli 提供了 4 个子命令:

  • init: 初始化一个 cobra 应用程序
  • add: 给通过 cobra init 创建的应用程序添加子命令
  • completion: 为指定的 shell 生成命令自动补全脚本
  • help: 打印任意命令的帮助信息

cobra-cli 还提供了一些全局参数:

  • -a, --author: 指定 Copyright 版权声明中的作者
  • --config: 指定 cobra 配置文件的路径
  • -l, --license: 指定生成的应用程序所使用的开源协议,内置的有:GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD;
  • --viper: 使用 viper 作为命令行参数解析工具,默认为 true。

III. Cobra 使用

在构建 cobra 应用时,可以自行组织代码目录结构,但 cobra 建议如下目录结构:

1
2
3
4
5
6
7
$ tree app_name
app_name
├── cmd
│   ├── add.go
│   ├── create.go
│   └── list.go
└── main.go

main.go 文件的目的只有一个:初始化 cobra 应用程序并注册子命令

1
2
3
4
5
6
7
8
9
package main

import (
  "{pathtToApp}/cmd"
)

func main() {
  cmd.Execute()
}

使用 cobra-cli 命令生成应用程序并添加子命令

可以选择使用 cobra-cli 命令行工具快速生成一个应用程序,并添加子命令,然后基于生成的代码进行二次开发,提高开发效率,具体方法如下:

1. 初始化应用程序

使用 cobra-cli init 命令初始化一个应用程序,然后就可以基于这个 Demo 进行二次开发,提高开发效率:

1
2
3
4
$ mkdir -p kyden-demo && cd kydne-demo && go mod init kyden-demo
$ cobra-cli init --license=MIT --viper
$ ls
cmd  go.mod  go.sum  LICENSE  main.go

2. 添加子命令

当一个应用程序初始化完成之后,就可以使用 cobra-cli add 命令添加一些命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ cobra-cli add serve
$ cobra-cli add config
$ cobra-cli add create -p 'configCmd' # 此命令的父命令的变量名(默认为 'rootCmd')

$ tree kyden-demo 
kyden-demo
├── LICENSE
├── cmd
│   ├── config.go
│   ├── create.go
│   ├── root.go
│   └── serve.go
├── go.mod
├── go.sum
└── main.go

执行 cobra-cli add 命令之后,会在 cmd 目录下生成命令源码文件。 cobra-cli 不仅可以添加命令,也可以添加子命令,例如 cobra-cli add create -p 'configCmd'config 命令添加了 create 子命令,-p 指定子命令的父命令:<父命令>Cmd.

3. 编译运行

在生成命令后,可以直接执行 go build 命令编译应用程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ go build -v .
$ ./kyden-demo -h
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
  kyden-demo [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  config      A brief description of your command
  help        Help about any command
  serve       A brief description of your command

Flags:
      --config string   config file (default is $HOME/.kyden-demo.yaml)
  -h, --help            help for kyden-demo
  -t, --toggle          Help message for toggle

Use "kyden-demo [command] --help" for more information about a command.

4. 配置 cobra

cobra 在生成应用程序时,也会在当前目录下生成 LINCENSE 文件,并且会在生成的 Go 源码文件中中,添加 LINCENSE Header。

LINCENSE 和 LINCENSE Header 的内容可以通过 cobra 配置文进行配置,默认配置文件 ~/.cobra.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ cat ~/.cobra.yaml
author: Kyden <kytedance@gmail.com>
year: 2024
license:
  header: This file is part of CLI application foo.
  text: |
    {{ .copyright }}

    This is my license. There are many like it, but this one is mine.
    My license is my best friend. It is my life. I must master it as I must
    master my life.

$ cobra-cli init
Copyright © 2024 Kyden <kytedance@gmail.com>

This is my license. There are many like it, but this one is mine.
My license is my best friend. It is my life. I must master it as I must
master my life.

{{ .copyright }} 的具体内容会根据 authoryear 生成,根据此配置生成的 LICENSE 文件内容.

也可以使用内建的 licenses,内建的 licenses 有:GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD。

使用 cobra 库创建命令

当使用 cobra 库编码实现一个应用程序,需要首选创建一个空的 main.go 文件和一个 rootCmd 文件,然后根据需要添加其他命令。

具体步骤如下:

  1. 创建 rootCmd
1
2
3
4
5
6
7
8
9
$ mkdir -p cobrademo && cobrademo
$ go mod init cobrademo
go: creating new go.mod: module cobrademo
go: to add module requirements and sums:
        go mod tidy
$ cobra-cli init
Using config file: /Users/kyden/.cobra.yml
Your Cobra application is ready at
/tmp/cobrademo

通常情况下,会将 rootCmd 放在 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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
Copyright © 2024 Kyden <kytedance@gmail.com>
This file is part of CLI application foo.
*/
package cmd

import (
	"os"

	"github.com/spf13/cobra"
)



// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "cobrademo",
	Short: "A brief description of your application",
	Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
    Run: func(cmd *cobra.Command, args []string) {
        // Do stuff here
    },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

func init() {
	// Here you will define your flags and configuration settings.
	// Cobra supports persistent flags, which, if defined here,
	// will be global for your application.

	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobrademo.yaml)")

	// Cobra also supports local flags, which will only run
	// when this action is called directly.
	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

还可以在 init() 函数中定义标志和处理配置,例如:cmd/helper.go:

  1. 创建 main.go

还需要一个 main 函数来调用 rootCmd,通常会创建一个 main.go 文件,在 main.go 中调用 rootCmd.Execute() 来执行命令:

1
2
3
4
5
6
7
8
9
package main

import (
  "{pathToApp}/cmd"
)

func main() {
  cmd.Execute()
}

main.go 中不建议放太多代码,通常只需要调用 cmd.Execute() 即可

  1. 添加命令

除了 rootCmd,还可以调用 AddCommand() 来添加其他命令,通常情况下,会把其他命令的源码文件放在 cmd 目录下,例如添加一个 version 命令(cmd/version.go):

 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
33
34
35
36
37
38
39
40
/*
Copyright © 2024 Kyden <kytedance@gmail.com>
This file is part of CLI application foo.
*/
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

// versionCmd represents the version command
var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "A brief description of your command",
	Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("version called")
	},
}

func init() {
	rootCmd.AddCommand(versionCmd)

	// Here you will define your flags and configuration settings.

	// Cobra supports Persistent Flags which will work for this command
	// and all subcommands, e.g.:
	// versionCmd.PersistentFlags().String("foo", "", "A help for foo")

	// Cobra supports local flags which will only run when this command
	// is called directly, e.g.:
	// versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
  1. 编译运行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ go build -v .
$ ./cobrademo -h
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
  cobrademo [flags]
  cobrademo [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  helper      A brief description of your command
  version     A brief description of your command

Flags:
  -h, --help     help for cobrademo
  -t, --toggle   Help message for toggle

Use "cobrademo [command] --help" for more information about a command.

使用标志

cobra 可以跟 pflag 结合使用,实现强大的标志功能。 具体步骤如下:

  1. 使用持久化的标志

标志是可以"持久化"的,即该标志可用于它所分配的命令以及该命令下的每个子命令。 例如,在 rootCmd 中定义持久化标志:

1
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
  1. 使用本地标志

本地标志,只能在其所绑定的命令上使用:

1
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

上面的 --source 标志智能在 rootCmd 命令上引用,而不能在 rootCmd 的子命令上引用。

  1. 将标志绑定到 viper

可以讲标志绑定到 viper,这样就可以使用 viper.Get() 获取标志的值。

1
2
3
4
5
6
7
var auther string

func init() {
	rootCmd.PersistentFlags().StringVar(
    &auther, "author", "Your Name", "Author name for copyright attribution")
	viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("auther"))
}
  1. 设置标志为必选

默认情况下,标志是可选的,也可以设置标志为必选。 当设置标志为必选时,若不提供标志时,cobra 会报错:

1
2
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")

IV. Reference