Golang:命令行框架cobra简介


项目地址:spf13/cobra: A Commander for modern Go CLI interactions (github.com)

文档地址:cobra/user_guide.md at master · spf13/cobra (github.com)

Overview

cobra是一个用于创建命令行工具的库(框架),可以创建出类似git或者go一样的工具,进行我们平时熟悉的git clone/pull、go get/install等操作。kubernetes工具中就使用了cobra。

上述这些命令行工具的--help/-h既好看又好理解,开始时我也会好奇这些工具是如何实现多级子命令支持的?cobra的目标即是提供快速构建出此类工具的能力(cobra是框架,具体的业务逻辑当然还是要自己写)。例如可以为我们app或系统工具的bin文件添加--config来启动服务、添加--version来查看版本号等等。相比起标准库里的flag包,cobra强大了很多。

官网列出了cobra的一大堆优点:

  1. 易用的subcommand模式(即嵌套命令或子命令)

  2. 强大的flags支持(参考标准库flagSet)

  3. 支持global、local、cascading级别的flag设置

  4. 错误时的智能提示

  5. 自动生成的、美观的help信息,并默认支持--help和-h打印help信息

  6. 提供命令自动补全功能(bash, zsh, fish, powershell环境)

  7. 提供命令man page自动生成功能

  8. 有兄弟项目viper可以快捷的实现配置文件解析和flag绑定(暂不建议直接使用,viper包装了一些配置文件解析的细节,应当在熟悉cobra之后再去单独了解)

本文的核心目的为:通过手写一个示例来迅速入门cobra。

先来简单看下cobra框架中主要概念:

kubectl get pod|service [podName|serviceName] -n <namespace>

以上述kubectl get为例,cobra将kubectl称作做rootcmd(即根命令),get称做rootcmd的subcmd,pod|service则是get的subcmd,podName、serviceName是pod/service的args,-n/--namespace称作flag。同时我们还观察到-n这个flag其实写在任意一个cmd之后都会正常生效,这说明这是一个global flag,global flag对于rootcmd及所有子命令生效。

此外,当为cmd指定了一个与subcmd同名的args时,subcmd优先生效。

一、使用cobra的建议的项目目录结构

cobra文档告诉我们,使用cobra时最好遵循如下的项目目录设置(即:把命令行定义相关的部分单独写在一个名为cmd的包内):

  ▾ appName/
    ▾ cmd/
        add.go
        your.go
        commands.go
        here.go
      main.go

使用cobra的项目,其程序入口main.go一般极为简洁(因为相应的业务逻辑都在cmd里调用或实现):

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute() 
}

二、写一个简单地mytool工具

本文示例不会去写一个常驻的后台服务,只写一个命令行工具mytool进行演示。

我们为mytool工具预设如下功能,朝着这个目标前进:

// 打印mytool的版本号,预设为1.0-beta
mytool version    		

// names目录中创建name同名文件,将desc内容写入文件中,成功后打印"${name} is added"
mytool add "Donald.Trump" --desc "He knows everything"  
// names目录中删除name同名文件,成功后打印"${name} is deleted"
mytool del "Donald.Trump" 		
// names目录中读取name同名文件中的信息,如未找到对应name那么显示"${name} not found"
mytool get "Donald.Trump" 
// names目录中读取所有文件并打印相关信息
mytool get all

1. 首先,创建如下的项目结构:

然后使用go mod初始化包管理文件go.mod:go mod init mytool

2. 编写cmd/root.go文件,正如其名这是所有命令行的root:

package cmd

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

var rootCmd = &cobra.Command{
	Use:   "mytool",
	Short: "mytool is a tool to record names", // 子命令的简单说明,务必简短,因为会出现在其上级命令的help结果中,过长会导致换行、不美观,此外还会出现在自动补全时,过长容易影响视线
	Long: `The best tool to record names in the world! // 子命令的功用详细说明,可以写的比较详细些
Just try it!!!
Complete documentation is available at http://mytool.com`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Use mytool -h or --help for help.")
	},
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

3. 然后创建main/main.go文件,并编译后试试效果:

可以看到基础的-h功能展示了出来。接下来我们为框架补充实体逻辑。

4. 先增加一个version功能试试

// cmd目录下新增version.go文件,内容如下:
package cmd

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

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "show version of mytool",
	Long: `All tools have a version, this is mytool's"`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("mytool-1.0-beta")
	},
}

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

我们定义了一个全新的cobra.Command:versionCmd,我们希望他成为mytool的一个subcmd,只要使用rootCmd.AddCommand(versionCmd)就可以实现了。这个操作可以在cmd包中任意地方写,但统一起见我们在每个cmd文件中写一个init函数来执行对应cmd的add操作和未来可能存在的flag绑定等操作,然后在root.go中增加initAll()函数来调用组织子命令的init函数(需要注意的是init()已经被cobra占用,所以我们自己的init函数加上各种后缀即可)。

package cmd

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

var rootCmd = &cobra.Command{
	Use:   "mytool",
	Short: "mytool is a tool to record names",
	Long: `The best tool to record names in the world!
Just try it!!!
Complete documentation is available at http://mytool.com`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Use mytool -h or --help for help.")
	},
}

func initAll()  {
	initVersion()
}

func Execute() {
	initAll()
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

可以看到关于version的help已经呈现了出来,也能正常查看mytool的version了,但实测还没有子命令自动预测补全的功能,这个在下边尝试。

5. 增加add子命令

cmd/add.go,这个子命令使用指定的name args作为参数,使用--desc作为flag

package cmd

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

var (
	name string
	nameDesc string
)

var addNameCmd = &cobra.Command{
	Use:   "add",
	Short: "mytool name add operations",
	Long: `mytool add: add name info in names dir`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) != 1 {
			fmt.Println(cmd.Help())
			return
		}
		name = args[0]
		err := ioutil.WriteFile(fmt.Sprintf("./names/%s",name), []byte(nameDesc), 0644)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("%s is added\n", name)
	},
}

func initAdd() {
	addNameCmd.Flags().StringVarP(&nameDesc, "desc", "D", "", "description of this person")
	rootCmd.AddCommand(addNameCmd)
}

然后在root.go initAll()中增加initAdd()的调用:

func initAll()  {
	initVersion()
	initAdd()
}

可以看到add子命令已经出现在help中了,且多了一个completion子命令,尝试一下可以发现,这个子命令可以用于生成各个shell环境下的自动补全脚本,我这里就略过不做尝试了。

6. 接下来照猫画虎添加delete.go和get.go文件

package cmd

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

var delNameCmd = &cobra.Command{
	Use:   "del",
	Short: "mytool name del operations",
	Long: `mytool del: del name info in names dir`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) != 1 {
			fmt.Println("Only one name can be deleted in one time!")
			return
		}
		name = args[0]
		err := os.Remove(fmt.Sprintf("./names/%s",name))
		if err != nil {
			fmt.Printf("%s not found\n", name)
			return
		}
		fmt.Printf("%s is deleted\n", name)
	},
}

func initDelete()  {
	rootCmd.AddCommand(delNameCmd)
}
package cmd

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

var getNameCmd = &cobra.Command{
	Use:   "get",
	Short: "mytool name get operations",
	Long: `mytool get: get name info in names dir`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) != 1 {
			fmt.Println("Only one name can be get in one time! Or use get all to get all names!")
			return
		}
		name = args[0]
		nameInfo,err := ioutil.ReadFile(fmt.Sprintf("./names/%s",name))
		if err != nil {
			fmt.Printf("%s not found\n", name)
			return
		}
		fmt.Println(name,":",string(nameInfo))
	},
}

var getAllNamesCmd = &cobra.Command{
	Use:   "all",
	Short: "mytool name get all operations",
	Long: `mytool get all: get all names info in names dir`,
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) != 0 {
			fmt.Println(getNameCmd.Help())
			return
		}
		files, err := ioutil.ReadDir("./names")
		if err != nil {
			fmt.Println("Dir names not found!")
			return
		}
		for _, file := range files {
			nameInfo,_ := ioutil.ReadFile(fmt.Sprintf("./names/%s", file.Name()))
			fmt.Println(file.Name(),":",string(nameInfo))
		}
	},
}

func initGet() {
	getNameCmd.AddCommand(getAllNamesCmd)
	rootCmd.AddCommand(getNameCmd)
}

至此所有cmd添加完毕,我们只需要在root.go中集成相关init函数即可。

func initAll()  {
	initVersion()
	initAdd()
	initDelete()
	initGet()
}

经测试各项功能正常。

三、思考

1. 我做了什么?

在本次笔记中,根据官方文档编写了一个mytool的指令,其实现了基本的subcommand添加、flag添加、args判断,基于此初步了解了cobra的命令添加体系和组织逻辑。

2. 有哪些地方需要注意的 以及 我还能做些什么?

实际项目中的情况会比示例复杂的多,但只要清楚其脉络就可以融会贯通。

  • 如何设置无需指定参数的flag:通过指定包含默认值的bool类型的flag来实现无参flag设置,例如如何将本例中的get all改为无参flag get --all
  • args的自动校验:本例通过简单的len(args)进行参数校验,cobra提供了便捷的Command内置字段Args来支持args校验,其值可以是cobra内置args校验函数,如NoArgs,MinimumNArgs(int),MaximumNArgs(int),ExactArgs(int)等等,也可以自定义匿名函数进行args的提前校验
  • 如何强制指定flag:rootCmd.MarkFlagRequired(<flagName>)、rootCmd.MarkPersistentFlagRequired(<flagName>)
  • cobra.Command的Run函数返回error:默认的Run函数不会返回error,所以实际上本例中的Execute()没有遇到异常panic时都会返回nil。如果想要捕捉各个cmd的运行时的业务error,可以使用RunE参数来代替Run参数,RunE支持error返回
  • help和usage的自定义:一般不需要
  • preRun和PostRun钩子(hooks):cobra提供了preRun和PostRun的hooks,参考webhooks理解即可,他们提供了在运行Run函数之前/之后执行其他更多操作的能力,这部分的使用很简单,只需要为Command对象的几个字段赋值相应的匿名函数即可。以本文为例,我们需要在执行任意命令组合之前都检查names文件夹是否存在,如果不存在则创建之,这个逻辑就可以写在rootcmd的preRun函数中,这样我们就不必在每次用到names文件夹时都检查其是否存在了。
  • 自动补全和man page生成功能测试:completion功能很简单,而关于man page生成则可参考:cobra/README.md at master · spf13/cobra (github.com)

  • 多级子命令:通常我们最多写3层子命令(包含rootCmd在内)就可以了,建议按命令继承的层次来组织cmd内的目录层次,这样可以快速找到对应子命令的实现逻辑。

cobra作者spf13还有一个viper项目,此项目提供便捷的针对json,yaml,toml,ini,hcl等类型配置文件的解析,配合cobra使用更佳,当然仅使用cobra也很好,自己解析配置文件也很便捷。

自动补全相关:

app completion bash > /etc/bash_completion.d/app

上述指令可以快捷的生成针对app指令的bash补全配置,并直接放入/etc/bash_completion.d文件中,新会话登录后即可实现自动补全。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM