Golang | 使用 Cobra 构建命令行工具

Xpitz · · 717 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

最近折腾了下命令行库 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 概念

Cobra 结构由三部分组成:命令 (commands)、参数 (arguments)、标志 (flags)。基本模型如下:
APPNAME VERB NOUN --ADJECTIVE 或者 APPNAME 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

下载完成后安装 cobra 工具,在 $GOPATH/bin 会生成可执行文件:

go install github.com/spf13/cobra/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 初始化 demo 项目:

[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.go。代码逻辑很简单,就是调用 cmd 包里 Execute()函数:

package main

import "demo/cmd"

func main() {
  cmd.Execute()
}

再看下 root.go 中 rootCmd 的字段:

...

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

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

Cobra 实践

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

子命令

之前运行会提示 subcommand is required,是因为根命令无法直接运行。那我们就添加个子命令试试。

通过 cobra add 添加子命令 create:

[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

子命令嵌套

对于功能相对复杂的 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

目前createrule 是同级的,所以需要修改 rule.goinit() 来改变子命令间的层级关系:

...

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

虽然调整了命令的层级关系,但是目前运行 demo create 会打印 create called,我希望运行时可以打印帮助提示。所以我们继续完善下代码,修改 create.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 rule,输出 rule called
[root@localhost demo]# go run main.go create rule
rule called

参数

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

demo create rule foo 

实现前先说下,Command 结构体中有个 Args 的字段,接受类型为 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,这里就不多做赘述了。

标志

再说说标志。现在要求 CLI 不接受参数,而是通过标志 --namerule 进行描述。这个又该如何实现?

demo create rule --name foo

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

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

这个标志的作用是修饰和描述 rule的名字,所以选用本地标志。修改 rule.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")      
}

说明:StringVarP 用来接收类型为字符串变量的标志。相较StringVarStringVarP 支持标志短写。以我们的 CLI 为例:在指定标志时可以用 --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.

读取配置

最后说说配置。需求:要求 --name 标志存在默认值,且该值是可配置的。

如果只需要标志提供默认值,我们只需要修改 StringVarPvalue 参数就可以实现。但是这个需求关键在于标志是可配置的,所以需要借助配置文件。

很多情况下,CLI 是需要读取配置信息的,比如 kubectl 的~/.kube/config。在帮助提示里可以看到默认的配置文件为 $HOME/.demo.yaml

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

​配置库我们可以使用 Viper。Viper 是 Cobra 集成的配置文件读取库,支持 YAMLJSONTOMLHCL 等格式的配置。

添加配置文件 $HOME/.demo.yaml,增加 name 字段:

[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

编译运行

​编译生成命令行工具:

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

运行测试:

[root@localhost demo]# ./demo create rule
Using config file: /root/.demo.yaml
Create rule foo success.
参考文档:
  1. Github - https://github.com/spf13/cobra
  2. Cobra 的一些笔记 - https://zhangguanzhang.github.io/2019/06/02/cobra/
  3. Golang之使用Cobra - https://o-my-chenjian.com/2017/09/20/Using-Cobra-With-Golang/
  4. golang命令行库Cobra的使用 - https://www.jianshu.com/p/7abe7cff5384

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:Xpitz

查看原文:Golang | 使用 Cobra 构建命令行工具

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

717 次点击  
加入收藏 微博
下一篇:
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传