跳至主要內容

Cobra 系统化你的命令行程序

离心原创大约 7 分钟tutorialgolangcobra

你自己写代码经常会go run xxx.go,但是如果一个仓库下有一个cmd目录并且之下会有很多main函数,那么你每次启动都会go run cmd/xxx.go xxxx,这样做很ugly,有没有办法让启动程序更优雅美观一些呢?当然是有的,社区有很成熟的命令行库程序,例如今天我们来介绍的Cobra.

Cobra 系统化你的命令行程序

Cobra是Go的一个Cli框架,为Go语言程序提供现代强大的命令行程序。

提供一些特别强大的功能(官网的功能描述):

  • 基于子命令的简单 CLI:app server、app fetch 等
  • 完全符合 POSIX 标准的标志(包括短版本和长版本)
  • 嵌套子命令
  • 全局、局部和级联标志
  • 轻松生成应用程序和应用程序带有 cobra init appname & 的命令cobra add cmdname
  • 智能建议(app srver...您的意思是app server吗?)
  • 自动生成命令和标志的帮助
  • 自动帮助标记识别-h、--help等
  • 为您的应用程序自动生成 bash 自动完成功能
  • 为您的应用程序自动生成手册页
  • 命令别名,以便您可以在不破坏它们的情况下进行更改
  • 灵活定义您自己的帮助、用法等。

实际上有很多成熟的项目在使用cobra,例如kubectlhugo等等。

下载也非常简单,在自己的项目中集成就可以了.

go get -u github.com/spf13/cobra/cobra

&

import "github.com/spf13/cobra"

其中我们只需要理解三个核心的概念就可以完全掌握cobra了:

  1. Commands 描述行为
  2. Args 命令行参数
  3. Flags 命令行选项

一般的格式为:

APPNAME COMMAND ARG --FLAG

举一个大家都会用的命令:

# 创建xxx分支
# git 是APPNAME 
# checkout 表示 COMMAND 表示行为
# ARG 是 xxx 表示checkout的参数是xxx
# -b 是 flags 是命令行的选项
git checkout -b xxx

其实这个概念理解了,就只需要按照文档跑一遍就会使用cobra了。

入门过程

最近cobra的api发生了变化,之前文章的用法都失败了,但没关系我们只需要跟着文档跑就可以了。

要使用cobra创建自定义命令行的程序,你首先需要在本地创建一个go.mod,接着就可以使用cobra命令行来创建程序了:

提示

对了,记得事先下载cobra-cli命令

go install github.com/spf13/cobra-cli@latest
go mod init cobra-test
# 使用cobra
cobra-cli init  cobra_test

下面你的程序应该长这个样子:

├── cobra_test
│   ├── LICENSE
│   ├── cmd
│   │   └── root.go
│   └── main.go
├── go.mod
└── go.sum

理论上讲你可以在当前目录下创建很多可运行的main程序,但一般情况下只需要一个main程序,接着往主要的main程序添加命令启动各个服务就好。

所以我们把cobra_test目录下的所有程序移动到当前目录下:

mv ./cobra_test/* ./

下面你就可以开始go run main.go

➜  demo_cobra 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.

如果你想添加不同的命令还可以继续做,比如要一个echo服务,cmd目录下就可以加一个echo文件,只需要在文件里编写逻辑就好:

➜  demo_cobra cobra-cli add echo        
echo created at /Users/lixin/code/demo_code/demo_cobra
➜  demo_cobra tree .
.
├── LICENSE
├── cmd
│   ├── echo.go
│   └── root.go
├── go.mod
├── go.sum
└── main.go

为命令添加具体的功能

到目前为止,我们一共为 cobra_demo 程序添加了两个 Command,分别是 rootCmd(cobra init 命令默认生成)echoCmd。

打开文件 root.go ,找到变量 rootCmd 的初始化过程并修改 Run 方法:

Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("cobra demo program")
},

接着运行go run main.go就变化了

go run main.go
cobra demo program

为 Command 添加选项(flags)

选项(flags)用来控制 Command 的具体行为。根据选项的作用范围,可以把选项分为两类:

  • persistent
  • local

对于 persistent 类型的选项,既可以设置给该 Command,又可以设置给该 Command 的子 Command。对于一些全局性的选项,比较适合设置为 persistent 类型,比如控制输出的 verbose 选项:

var Verbose bool
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")

local 类型的选项只能设置给指定的 Command,比如下面定义的 source 选项:

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

var Name string
rootCmd.Flags().StringVarP(&Name, "name", "n", "", "user name (required)")
rootCmd.MarkFlagRequired("name")

命令行参数(arguments)

首先我们来搞清楚命令行参数(arguments)与命令行选项的区别(flags/options)。以常见的 ls 命令来说,其命令行的格式为:

ls [OPTION]... [FILE]

其中的 OPTION 对应本文中介绍的 flags,以 - 或 -- 开头;而 FILE 则被称为参数(arguments)或位置参数。一般的规则是参数在所有选项的后面,上面的 … 表示可以指定多个选项和多个参数。

cobra 默认提供了一些验证方法:

NoArgs - 如果存在任何位置参数,该命令将报错
ArbitraryArgs - 该命令会接受任何位置参数
OnlyValidArgs - 如果有任何位置参数不在命令的 ValidArgs 字段中,该命令将报错
MinimumNArgs(int) - 至少要有 N 个位置参数,否则报错
MaximumNArgs(int) - 如果位置参数超过 N 个将报错
ExactArgs(int) - 必须有 N 个位置参数,否则报错
ExactValidArgs(int) 必须有 N 个位置参数,且都在命令的 ValidArgs 字段中,否则报错
RangeArgs(min, max) - 如果位置参数的个数不在区间 min 和 max 之中,报错

比如要让 Command echo 至少有一个位置参数,可以这样初始化它:

var cmdTimes = &cobra.Command{
    Use: …
    Short: …
    Long: …
    Args: cobra.MinimumNArgs(1),
    Run:}

一个完整的 demo

我们在前面创建的代码的基础上,为 image 命令添加行为(打印信息到控制台),并为它添加一个子命令 cmdTimes,下面是更新后的 image.go 文件的内容(本文 demo 的完整代码请参考这里):

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
    "strings"
)

// echoCmd represents the echo command
var echoCmd = &cobra.Command{
    Use:   "echo",
    Short: "Print echo information",
    Long: "Print all echo information",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("echo one is ubuntu 16.04")
        fmt.Println("echo two is ubuntu 18.04")
        fmt.Println("echo args are : " + strings.Join(args, " "))
    },
}

var echoTimes int
var cmdTimes = &cobra.Command{
    Use:   "times [string to echo]",
    Short: "Echo anything to the screen more times",
    Long: `echo things multiple times back to the user by providing
a count and a string.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        for i := 0; i < echoTimes; i++ {
            fmt.Println("Echo: " + strings.Join(args, " "))
        }
    },
}

func init() {
    rootCmd.AddCommand(echoCmd)
    cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
    echoCmd.AddCommand(cmdTimes)
}
go run main.go
$ ./cobrademo echo hello
$ ./cobrademo echo times -t=3 world

因为我们为 cmdTimes 命令设置了 Args: cobra.MinimumNArgs(1),所以必须为 times 子命令传入一个参数,不然 times 子命令会报错:

在 Commnad 执行前后执行额外的操作

Command 执行的操作是通过 Command.Run 方法实现的,为了支持我们在 Run 方法执行的前后执行一些其它的操作,Command 还提供了额外的几个方法,它们的执行顺序如下:

  1. PersistentPreRun
  2. PreRun
  3. Run
  4. PostRun
  5. PersistentPostRun

修改 rootCmd 的初始化代码如下:

var rootCmd = &cobra.Command{
    Use:   "cobrademo",
    Short: "sparkdev's cobra demo",
    Long: "the demo show how to use cobra package",
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args)
    },
    PreRun: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Inside rootCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Printf("cobra demo program, with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Inside rootCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
        fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args)
    },
}

总结

cobra 是一个非常实用(流行)的包,很多优秀的开源应用都在使用它,包括 Docker 和 Kubernetes 等等。当我们熟悉了 cobra 包的基本用法后,再去看 Docker 等应用的命令行工具的格式,是不是就很容易理解了!