文章首发于个人公众号:「阿拉平平」

最近折腾了下命令行库 Cobra,和大家分享下。本文演示环境为 CentOS 7.5,Golang 1.11。

文章目录:

  1. Cobra 介绍
    1.1 概念
    1.2 安装
    1.3 初始化
    1.4 代码分析

  2. Cobra 实践
    2.1 子命令
    2.2 子命令嵌套
    2.3 参数
    2.4 标志
    2.5 读取配置
    2.6 编译运行

1. Cobra 介绍

Cobra 是一个用来创建命令行的 golang 库,同时也是一个用于生成应用和命令行文件的程序。

1.1 概念

APPNAME VERB NOUN --ADJECTIVEAPPNAME COMMAND ARG --FLAG

如果不是太理解的话,没关系,我们先看个例子:

hugo server --port=1313
  • hugo:根命令
  • server:子命令
  • --port:标志

再看个带有参数的例子:

git clone URL --bare
  • git:根命令
  • clone:子命令
  • URL:参数,即 clone 作用的对象
  • --bare:标志

总结下:

  • commands 代表行为,是应用的中心点
  • arguments 代表行为作用的对象
  • flags 是行为的修饰符

相信看了例子后,应该有个直观的认识了。接下来我们安装 Cobra。

1.2 安装

安装很简单:

go get -u github.com/spf13/cobra/cobra
i/o timeout
package golang.org/x/sys/unix: unrecognized import path "golang.org/x/sys/unix" (https fetch: Get https://golang.org/x/sys/unix?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
package golang.org/x/text/transform: unrecognized import path "golang.org/x/text/transform" (https fetch: Get https://golang.org/x/text/transform?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)
package golang.org/x/text/unicode/norm: unrecognized import path "golang.org/x/text/unicode/norm" (https fetch: Get https://golang.org/x/text/unicode/norm?go-get=1: dial tcp 216.239.37.1:443: i/o timeout)

网上解决方法很多,这里我推荐使用 gopm 来下载:

# 下载 gopm,之后会在 $GOPATH/bin 目录下生成 gopm
go get -u github.com/gpmgo/gopm

# 使用 gopm 来下载 cobra
gopm get -u -g github.com/spf13/cobra/cobra
$GOPATH/bin
go install github.com/spf13/cobra/cobra
$PATH
[root@localhost ~]# cp -a $GOPATH/bin/cobra /usr/local/bin
[root@localhost ~]# cobra
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:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  help        Help about any command
  init        Initialize a Cobra Application

Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)

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

接下来我们初始化一个项目。

1.3 初始化

cobra init
[root@localhost ~]# cd $GOPATH/src 
[root@localhost src]# cobra init demo --pkg-name=demo
Your Cobra applicaton is ready at
/root/go/src/demo

当前项目结构为:

demo
├── cmd
│   └── root.go
├── LICENSE
└── main.go
main.goroot.go

1.4 代码分析

main.goExecute()
package main

import "demo/cmd"

func main() {
  cmd.Execute()
}
root.go
...

var rootCmd = &cobra.Command{
  Use:   "demo",
  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.`,
  // Uncomment the following line if your bare application
  // has an action associated with it:
  //    Run: func(cmd *cobra.Command, args []string) { },
}

// 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() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

...

简单说明下:

  • Use:命令名
  • Short & Long:帮助信息的文字内容
  • Run:运行命令的逻辑

Command 结构体中的字段当然远不止这些,受限于篇幅,这里无法全部介绍。有兴趣的童鞋可以查阅下官方文档。

运行测试:

[root@localhost demo]# go run main.go
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.

subcommand is required
exit status 1

如果运行的结果和我的一致,那我们就可以进入到实践环节了。

2. Cobra 实践

铺垫了这么久,终于可以开始实践了。实践环节中,我会 提一些需求,然后我们一起实现一个简单的命令行工具。

2.1 子命令

subcommand is required
cobra addcreate
[root@localhost demo]# cobra add create
create created at /root/go/src/demo

当前项目结构为:

demo
├── cmd
│   ├── create.go
│   └── root.go
├── LICENSE
└── main.go
create.goinit()
...

func init() {
       rootCmd.AddCommand(createCmd)        
}

运行测试:

# 输入正确
[root@localhost demo]# go run main.go create
create called

# 未知命令
[root@localhost demo]# go run main.go crea
Error: unknown command "crea" for "demo"

Did you mean this?
    create

Run 'demo --help' for usage.
unknown command "crea" for "demo"

Did you mean this?
    create

2.2 子命令嵌套

对于功能相对复杂的 CLI,通常会通过多级子命令,即:子命令嵌套的方式进行描述,那么该如何实现呢?

demo create rule
rule
[root@localhost demo]# cobra add rule
rule created at /root/go/src/demo

当前目录结构如下:

demo
├── cmd
│   ├── create.go
│   ├── root.go
│   └── rule.go
├── LICENSE
└── main.go
createrulerule.goinit()
...

func init() {
        // 修改子命令的层级关系
        //rootCmd.AddCommand(ruleCmd)
        createCmd.AddCommand(ruleCmd)
}
demo createcreate calledcreate.go
...

var createCmd = &cobra.Command{
        Use:   "create",
        Short: "create",
        Long: "Create Command.",
        Run: func(cmd *cobra.Command, args []string) {
                // 如果 create 命令后没有参数,则提示帮助信息
                if len(args) == 0 {
                  cmd.Help()
                  return
                }
        },
}

...

运行测试:

create
[root@localhost demo]# go run main.go create
Create Command.

Usage:
  demo create [flags]
  demo create [command]

Available Commands:
  rule        A brief description of your command

Flags:
  -h, --help   help for create

Global Flags:
      --config string   config file (default is $HOME/.demo.yaml)

Use "demo create [command] --help" for more information about a command.
create rulerule called
[root@localhost demo]# go run main.go create rule
rule called

2.3 参数

先说说参数。现在有个需求:给 CLI 加个位置参数,要求参数有且仅有一个。这个需求我们要如何实现呢?

demo create rule foo 
type PositionalArgs func(cmd *Command, args []string) error

内置的验证方法如下:

  • NoArgs:如果有任何参数,命令行将会报错
  • ArbitraryArgs: 命令行将会接收任何参数
  • OnlyValidArgs: 如果有如何参数不属于 Command 的 ValidArgs 字段,命令行将会报错
  • MinimumNArgs(int): 如果参数个数少于 N 个,命令行将会报错
  • MaximumNArgs(int): 如果参数个数多于 N 个,命令行将会报错
  • ExactArgs(int): 如果参数个数不等于 N 个,命令行将会报错
  • RangeArgs(min, max): 如果参数个数不在 min 和 max 之间, 命令行将会报错

由于需求里要求参数有且仅有一个,想想应该用哪个内置验证方法呢?相信你已经找到了 ExactArgs(int)。

rule.go
...

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        
        Args: cobra.ExactArgs(1),
        Run: func(cmd *cobra.Command, args []string) {           
          fmt.Printf("Create rule %s success.\n", args[0])
        },
}

...

运行测试:

  • 不输入参数:
[root@localhost demo]# go run main.go create rule
Error: accepts 1 arg(s), received 0
  • 输入 1 个参数:
[root@localhost demo]# go run main.go create rule foo
Create rule foo success.
  • 输入 2 个参数:
[root@localhost demo]# go run main.go create rule
Error: accepts 1 arg(s), received 2

从测试的情况看,运行的结果符合我们的预期。如果需要对参数进行复杂的验证,还可以自定义 Args,这里就不多做赘述了。

2.4 标志

--namerule
demo create rule --name foo

Cobra 中有两种标志:持久标志 ( Persistent Flags ) 和 本地标志 ( Local Flags ) 。

持久标志:指所有的 commands 都可以使用该标志。比如:--verbose ,--namespace
本地标志:指特定的 commands 才可以使用该标志。

rulerule.go
package cmd

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

// 添加变量 name
var name string

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        Run: func(cmd *cobra.Command, args []string) {
          // 如果没有输入 name
          if len(name) == 0 {
            cmd.Help()
            return
          }     
          fmt.Printf("Create rule %s success.\n", name)
        },
}

func init() {
        createCmd.AddCommand(ruleCmd)
        // 添加本地标志
        ruleCmd.Flags().StringVarP(&name, "name", "n", "", "rule name")      
}
StringVarPStringVarStringVarP--name-n

运行测试:

# 这几种写法都可以执行
[root@localhost demo]# go run main.go create rule -n foo
Create rule foo success.
[root@localhost demo]# go run main.go create rule --name foo
Create rule foo success.
[root@localhost demo]# go run main.go create -n foo rule
Create rule foo success.

2.5 读取配置

--name
StringVarPvalue
~/.kube/config$HOME/.demo.yaml
Global Flags:
      --config string   config file (default is $HOME/.demo.yaml)
YAMLJSONTOMLHCL
$HOME/.demo.yaml
[root@localhost ~]# vim $HOME/.demo.yaml 
name: foo
rule.go
package cmd

import (
        "fmt"
         // 导入 viper 包
        "github.com/spf13/viper"
        "github.com/spf13/cobra"
)

var name string

var ruleCmd = &cobra.Command{
        Use:   "rule",
        Short: "rule",
        Long: "Rule Command.",
        Run: func(cmd *cobra.Command, args []string) {
          // 不输入 --name 从配置文件中读取 name
          if len(name) == 0 {
            name = viper.GetString("name")
            // 配置文件中未读取到 name,打印帮助提示
            if len(name) == 0 {
              cmd.Help()
              return
            }
          }
          fmt.Printf("Create rule %s success.\n", name)
        },
}

func init() {
        createCmd.AddCommand(ruleCmd)
        ruleCmd.Flags().StringVarP(&name, "name", "n", "", "rule name")
}

运行测试:

[root@localhost demo]# go run main.go create rule
Using config file: /root/.demo.yaml
Create rule foo success.

如果 CLI 没有用到配置文件,可以在初始化项目的时候关闭 Viper 的选项以减少编译后文件的体积,如下:

cobra init demo --pkg-name=demo --viper=false

2.6 编译运行

​编译生成命令行工具:

[root@localhost demo]# go build -o demo

运行测试:

[root@localhost demo]# ./demo create rule
Using config file: /root/.demo.yaml
Create rule foo success.
参考文档: