Go 每日一库之 cobra

栏目: IT技术 · 发布时间: 4年前

内容简介:用于生成基于 cobra 的应用程序框架。非常多知名的开源项目使用了 cobra 库构建命令行,如本文介绍 cobra 库的基本使用和一些有趣的特性。

简介

cobra 是一个命令行程序库,可以用来编写命令行程序。同时,它也提供了一个脚手架,

用于生成基于 cobra 的应用程序框架。非常多知名的开源项目使用了 cobra 库构建命令行,如 KubernetesHugoetcd 等等等等。

本文介绍 cobra 库的基本使用和一些有趣的特性。

关于作者 spf13 ,这里多说两句。spf13 开源不少项目,而且他的开源项目质量都比较高。

相信使用过 vim 的都知道 spf13-vim ,号称 vim 终极配置。

可以一键配置,对于我这样的懒人来说绝对是福音。他的 viper 是一个完整的配置解决方案。

完美支持 JSON/TOML/YAML/HCL/envfile/Java properties 配置文件等格式,还有一些比较实用的特性,如配置热更新、多查找目录、配置保存等。

还有非常火的静态网站生成器 hugo 也是他的作品。

快速使用

第三方库都需要先安装,后使用。下面命令安装了 cobra 生成器程序和 cobra 库:

$ go get github.com/spf13/cobra/cobra

如果出现了 golang.org/x/text 库找不到之类的错误,需要手动从 GitHub 上下载该库,再执行上面的安装命令。我以前写过一篇博客 搭建 Go 开发环境 提到了这个方法。

我们实现一个简单的命令行程序 git,当然这不是真的 git,只是模拟其命令行。最终还是通过 os/exec 库调用外部程序执行真实的 git 命令,返回结果。

所以我们的系统上要安装 git,且 git 在可执行路径中。目前我们只添加一个子命令 version 。目录结构如下:

▾ get-started/
    ▾ cmd/
        helper.go
        root.go
        version.go
    main.go

root.go

package cmd

import (
  "errors"

  "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command {
  Use: "git",
  Short: "Git is a distributed version control system.",
  Long: `Git is a free and open source distributed version control system
designed to handle everything from small to very large projects 
with speed and efficiency.`,
  Run: func(cmd *cobra.Command, args []string) {
    Error(cmd, args, errors.New("unrecognized command"))
  },
}

func Execute() {
  rootCmd.Execute()
}

version.go

package cmd

import (
  "fmt"
  "os"

  "github.com/spf13/cobra"
)

var versionCmd = &cobra.Command {
  Use: "version",
  Short: "version subcommand show git version info.",
  
  Run: func(cmd *cobra.Command, args []string) {
    output, err := ExecuteCommand("git", "version", args...)
    if err != nil {
      Error(cmd, args, err)
    }

    fmt.Fprint(os.Stdout, output)
  },
}

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

main.go 文件中只是调用命令入口:

package main

import (
  "github.com/darjun/go-daily-lib/cobra/get-started/cmd"
)

func main() {
  cmd.Execute()
}

为了编码方便,在 helpers.go 中封装了调用外部程序和错误处理函数:

package cmd

import (
  "fmt"
  "os"
  "os/exec"

  "github.com/spf13/cobra"
)

func ExecuteCommand(name string, subname string, args ...string) (string, error) {
  args = append([]string{subname}, args...)

  cmd := exec.Command(name, args...)
  bytes, err := cmd.CombinedOutput()

  return string(bytes), err
}

func Error(cmd *cobra.Command, args []string, err error) {
  fmt.Fprintf(os.Stderr, "execute %s args:%v error:%v\n", cmd.Name(), args, err)
  os.Exit(1)
}

每个 cobra 程序都有一个根命令,可以给它添加任意多个子命令。我们在 version.goinit 函数中将子命令添加到根命令中。

编译程序。注意,不能直接 go run main.go ,这已经不是单文件程序了。如果强行要用,请使用 go run .

$ go build -o main.exe

cobra 自动生成的帮助信息,very cool:

$ ./main.exe -h
Git is a free and open source distributed version control system
designed to handle everything from small to very large projects
with speed and efficiency.

Usage:
  git [flags]
  git [command]

Available Commands:
  help        Help about any command
  version     version subcommand show git version info.

Flags:
  -h, --help   help for git

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

单个子命令的帮助信息:

$ ./main.exe version -h
version subcommand show git version info.

Usage:
  git version [flags]

Flags:
  -h, --help   help for version

调用子命令:

$ ./main.exe version
git version 2.19.1.windows.1

未识别的子命令:

$ ./main.exe clone
Error: unknown command "clone" for "git"
Run 'git --help' for usage.

编译时可以将 main.exe 改成 git ,用起来会更有感觉 。

$ go build -o git
$ ./git version
git version 2.19.1.windows.1

使用 cobra 构建命令行时,程序的目录结构一般比较简单,推荐使用下面这种结构:

▾ appName/
    ▾ cmd/
        cmd1.go
        cmd2.go
        cmd3.go
        root.go
    main.go

每个命令实现一个文件,所有命令文件存放在 cmd 目录下。外层的 main.go 仅初始化 cobra。

特性

cobra 提供非常丰富的功能:

  • 轻松支持子命令,如 app serverapp fetch 等;
  • 完全兼容 POSIX 选项(包括短、长选项);
  • 嵌套子命令;
  • 全局、本地层级选项。可以在多处设置选项,按照一定的顺序取用;
  • 使用脚手架轻松生成程序框架和命令。

首先需要明确 3 个基本概念:

  • 命令(Command):就是需要执行的操作;
  • 参数(Arg):命令的参数,即要操作的对象;
  • 选项(Flag):命令选项可以调整命令的行为。

下面示例中, server 是一个(子)命令, --port 是选项:

hugo server --port=1313

下面示例中, clone 是一个(子)命令, URL 是参数, --bare 是选项:

git clone URL --bare

命令

在 cobra 中,命令和子命令都是用 Command 结构表示的。 Command 有非常多的字段,用来定制命令的行为。

在实际中,最常用的就那么几个。我们在前面示例中已经看到了 Use/Short/Long/Run

Use 指定使用信息,即命令怎么被调用,格式为 name arg1 [arg2]name 为命令名,后面的 arg1 为必填参数, arg3 为可选参数,参数可以多个。

Short/Long 都是指定命令的帮助信息,只是前者简短,后者详尽而已。

Run 是实际执行操作的函数。

定义新的子命令很简单,就是创建一个 cobra.Command 变量,设置一些字段,然后添加到根命令中。例如我们要添加一个 clone 子命令:

package cmd

import (
  "fmt"
  "os"

  "github.com/spf13/cobra"
)

var cloneCmd = &cobra.Command {
  Use: "clone url [destination]",
  Short: "Clone a repository into a new directory",
  Run: func(cmd *cobra.Command, args []string) {
    output, err := ExecuteCommand("git", "clone", args...)
    if err != nil {
      Error(cmd, args, err)
    }

    fmt.Fprintf(os.Stdout, output)
  },
}

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

其中 Use 字段 clone url [destination] 表示子命令名为 clone ,参数 url 是必须的,目标路径 destination 可选。

我们将程序编译为 mygit 可执行文件,然后将它放到 $GOPATH/bin 中。我喜欢将 $GOPATH/bin 放到 $PATH 中,所以可以直接调用 mygit 命令了:

$ go build -o mygit
$ mv mygit $GOPATH/bin
$ mygit clone https://github.com/darjun/leetcode
Cloning into 'leetcode'...

大家可以继续添加命令。但是我这边只是偷了个懒,将操作都转发到实际的 git 去执行了。这确实没什么实际的用处。

有这个思路,试想一下,我们可以结合多个命令实现很多有用的工具,例如打包工具:wink:。

选项

cobra 中选项分为两种,一种是 永久选项 ,定义它的命令和其子命令都可以使用。通过给根命令添加一个选项定义全局选项。

另一种是 本地选项 ,只能在定义它的命令中使用。

cobra 使用 pflag 解析命令行选项。 pflag 使用上基本与 flag 相同,该系列文章有一篇介绍 flag 库的, Go 每日一库之 flag

flag 一样,存储选项的变量也需要提前定义好:

var Verbose bool
var Source string

设置永久选项:

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

设置本地选项:

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

两种参数都是相同的,长选项/短选项名、默认值和帮助信息。

下面,我们通过一个案例来演示选项的使用。

假设我们要做一个简单的计算器,支持加、减、乘、除操作。并且可以通过选项设置是否忽略非数字参数,设置除 0 是否报错。

显然,前一个选项应该放在全局选项中,后一个应该放在除法命令中。程序结构如下:

▾ math/
    ▾ cmd/
        add.go
        divide.go
        minus.go
        multiply.go
        root.go
    main.go

这里展示 divide.goroot.go ,其它命令文件都类似。完整代码我放在 GitHub 上了。

divide.go

var (
  dividedByZeroHanding int // 除 0 如何处理
)
var divideCmd = &cobra.Command {
  Use: "divide",
  Short: "Divide subcommand divide all passed args.",
  Run: func(cmd *cobra.Command, args []string) {
    values := ConvertArgsToFloat64Slice(args, ErrorHandling(parseHandling))
    result := calc(values, DIVIDE)
    fmt.Printf("%s = %.2f\n", strings.Join(args, "/"), result)
  },
}

func init() {
  divideCmd.Flags().IntVarP(&dividedByZeroHanding, "divide_by_zero", "d", int(PanicOnDividedByZero), "do what when divided by zero")

  rootCmd.AddCommand(divideCmd)
}

root.go

var (
  parseHandling int
)

var rootCmd = &cobra.Command {
  Use: "math",
  Short: "Math calc the accumulative result.",
  Run: func(cmd *cobra.Command, args []string) {
    Error(cmd, args, errors.New("unrecognized subcommand"))
  },
}

func init() {
  rootCmd.PersistentFlags().IntVarP(&parseHandling, "parse_error", "p", int(ContinueOnParseError), "do what when parse arg error")
}

func Execute() {
  rootCmd.Execute()
}

divide.go 中定义了如何处理除 0 错误的选项,在 root.go 中定义了如何处理解析错误的选项。选项枚举如下:

const (
  ContinueOnParseError  ErrorHandling = 1 // 解析错误尝试继续处理
  ExitOnParseError      ErrorHandling = 2 // 解析错误程序停止
  PanicOnParseError     ErrorHandling = 3 // 解析错误 panic
  ReturnOnDividedByZero ErrorHandling = 4 // 除0返回
  PanicOnDividedByZero  ErrorHandling = 5 // 除0 painc
)

其实命令的执行逻辑并不复杂,就是将参数转为 float64 。然后执行相应的运算,输出结果。

测试程序:

$ go build -o math
$ ./math add 1 2 3 4
1+2+3+4 = 10.00

$ ./math minus 1 2 3 4
1-2-3-4 = -8.00

$ ./math multiply 1 2 3 4
1*2*3*4 = 24.00

$ ./math divide 1 2 3 4
1/2/3/4 = 0.04

默认情况,解析错误被忽略,只计算格式正确的参数的结果:

$ ./math add 1 2a 3b 4
1+2a+3b+4 = 5.00

$ ./math divide 1 2a 3b 4
1/2a/3b/4 = 0.25

设置解析失败的处理,2 表示退出程序,3 表示 panic(看上面的枚举):

$ ./math add 1 2a 3b 4 -p 2
invalid number: 2a

$ ./math add 1 2a 3b 4 -p 3
panic: strconv.ParseFloat: parsing "2a": invalid syntax

goroutine 1 [running]:
github.com/darjun/go-daily-lib/cobra/math/cmd.ConvertArgsToFloat64Slice(0xc00004e300, 0x4, 0x6, 0x3, 0xc00008bd70, 0x504f6b, 0xc000098600)
    D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/helper.go:58 +0x2c3
github.com/darjun/go-daily-lib/cobra/math/cmd.glob..func1(0x74c620, 0xc00004e300, 0x4, 0x6)
    D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/add.go:14 +0x6d
github.com/spf13/cobra.(*Command).execute(0x74c620, 0xc00004e1e0, 0x6, 0x6, 0x74c620, 0xc00004e1e0)
    D:/code/golang/src/github.com/spf13/cobra/command.go:835 +0x2b1
github.com/spf13/cobra.(*Command).ExecuteC(0x74d020, 0x0, 0x599ee0, 0xc000056058)
    D:/code/golang/src/github.com/spf13/cobra/command.go:919 +0x302
github.com/spf13/cobra.(*Command).Execute(...)
    D:/code/golang/src/github.com/spf13/cobra/command.go:869
github.com/darjun/go-daily-lib/cobra/math/cmd.Execute(...)
    D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/root.go:45
main.main()
    D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/main.go:8 +0x35

至于除 0 选项大家自己试试。

细心的朋友应该都注意到了,该程序还有一些不完善的地方。例如这里如果输入非数字参数,该参数也会显示在结果中:

$ ./math add 1 2 3d cc
1+2+3d+cc = 3.00

感兴趣可以自己完善一下~

脚手架

通过前面的介绍,我们也看到了其实 cobra 命令的框架还是比较固定的。这就有了 工具 的用武之地了,可极大地提高我们的开发效率。

前面安装 cobra 库的时候也将脚手架程序安装好了。下面我们介绍如何使用这个生成器。

使用 cobra init 命令创建一个 cobra 应用程序:

$ cobra init scaffold --pkg-name github.com/darjun/go-daily-lib/cobra/scaffold

其中 scaffold 为应用程序名,后面通过 pkg-name 选项指定包路径。生成的程序目录结构如下:

▾ scaffold/
    ▾ cmd/
        root.go
    LICENSE
    main.go

这个项目结构与之前介绍的完全相同,也是 cobra 推荐使用的结构。同样地, main.go 也仅仅是入口。

root.go 中,工具额外帮我们生成了一些代码。

在根命令中添加了配置文件选项,大部分应用程序都需要配置文件:

func init() {
  cobra.OnInitialize(initConfig)

  rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.scaffold.yaml)")
  rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

在初始化完成的回调中,如果发现该选项为空,则默认使用主目录下的 .scaffold.yaml 文件:

func initConfig() {
  if cfgFile != "" {
    viper.SetConfigFile(cfgFile)
  } else {
    home, err := homedir.Dir()
    if err != nil {
      fmt.Println(err)
      os.Exit(1)
    }

    viper.AddConfigPath(home)
    viper.SetConfigName(".scaffold")
  }

  viper.AutomaticEnv()

  if err := viper.ReadInConfig(); err == nil {
    fmt.Println("Using config file:", viper.ConfigFileUsed())
  }
}

这里用到了我前几天介绍的 go-homedir 库。配置文件的读取使用了 spf13 自己的开源项目 viper (毒龙?真是起名天才)。

除了代码文件,cobra 还生成了一个 LICENSE 文件。

现在这个程序还不能做任何事情,我们需要给它添加子命令,使用 cobra add 命令:

$ cobra add date

该命令在 cmd 目录下新增了 date.go 文件。基本结构已经搭好了,剩下的就是修改一些描述,添加一些选项了。

我们现在实现这样一个功能,根据传入的年、月,打印这个月的日历。如果没有传入选项,使用当前的年、月。

选项定义:

func init() {
  rootCmd.AddCommand(dateCmd)

  dateCmd.PersistentFlags().IntVarP(&year, "year", "y", 0, "year to show (should in [1000, 9999]")
  dateCmd.PersistentFlags().IntVarP(&month, "month", "m", 0, "month to show (should in [1, 12]")
}

修改 dateCmdRun 函数:

Run: func(cmd *cobra.Command, args []string) {
  if year < 1000 && year > 9999 {
    fmt.Fprintln(os.Stderr, "invalid year should in [1000, 9999], actual:%d", year)
    os.Exit(1)
  }

  if month < 1 && year > 12 {
    fmt.Fprintln(os.Stderr, "invalid month should in [1, 12], actual:%d", month)
    os.Exit(1)
  }

  showCalendar()
}

showCalendar 函数就是利用 time 提供的方法实现的,这里就不赘述了。感兴趣可以去我的 GitHub 上查看实现。

看看程序运行效果:

$ go build -o main.exe
$ ./main.exe date
  Sun  Mon  Tue  Wed  Thu  Fri  Sat
                   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

$ ./main.exe date --year 2019 --month 12
  Sun  Mon  Tue  Wed  Thu  Fri  Sat
    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

可以再为这个程序添加其他功能,试一试吧~

其他

cobra 提供了非常丰富的特性和定制化接口,例如:

  • 设置钩子函数,在命令执行前、后执行某些操作;
  • 生成 Markdown/ReStructed Text/Man Page 格式的文档;
  • 等等等等。

由于篇幅限制,就不一一介绍了。有兴趣可自行研究。cobra 库的使用非常广泛,很多知名项目都有用到,前面也提到过这些项目。

学习这些项目是如何使用 cobra 的,可以从中学习 cobra 的特性和最佳实践。这也是学习开源项目的一个很好的途径。

文中所有示例代码都已上传至我的 GitHub, Go 每日一库https://github.com/darjun/go-...

参考

  1. cobra GitHub 仓库

我的博客

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

Go 每日一库之 cobra

本文由博客一文多发平台 OpenWrite 发布!


以上所述就是小编给大家介绍的《Go 每日一库之 cobra》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

马云现象的经济学分析:互联网经济的八个关键命题

马云现象的经济学分析:互联网经济的八个关键命题

胡晓鹏 / 上海社会科学院出版社 / 2016-11-1 / CNY 68.00

互联网经济的产生、发展与扩张,在冲击传统经济理论观点的同时,也彰显了自身理论体系的独特内核,并与那种立足于工业经济时代的经典理论发生显著分野。今天看来,“马云”们的成功是中国经济长期“重制造、轻服务,重产能、轻消费,重国有、轻民营”发展逻辑的结果。但互联网经济的发展却不应仅仅止步于商业技巧的翻新,还需要在理论上进行一番审慎的思考。对此,我们不禁要问:互联网经济驱动交易发生的机理是什么?用户基数和诚......一起来看看 《马云现象的经济学分析:互联网经济的八个关键命题》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具