zoukankan      html  css  js  c++  java
  • 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",
    	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对象的几个字段赋值相应的匿名函数即可。

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

    想建一个数据库技术和编程技术的交流群,用于磨炼提升技术能力,目前主要专注于Golang和Python以及TiDB,MySQL数据库,群号:231338927,建群日期:2019.04.26,截止2021.02.01人数:300人 ... 如发现博客错误,可直接留言指正,感谢。
  • 相关阅读:
    POJ 3041 Asteroids 二分图匹配
    ZOJ 3705 Applications 模拟
    UNIX环境高级编程(第3版)
    明清美文四卷本(共四册)
    卑鄙的圣人:曹操(全10册)
    爱丽丝梦游仙境
    我在大清官场30年
    乌合之众:大众心理研究
    Java多线程编程实战指南
    Linux就该这么学
  • 原文地址:https://www.cnblogs.com/realcp1018/p/15763061.html
Copyright © 2011-2022 走看看