go cli脚手架开发利器——cobra库的初体验

关于

×错例:

  1. 不能完全代替shell,cobra能支持的脚本格式比较局限

  2. 无法像Java -jar -Dxx=str1 -Dxx2=str2 那样读取-D指示的值

  3. 读取参数只能为无格式纯文本,不要尝试读取json、特殊字符、转义符和富文本。

  4. 避免一大堆可变配置参数都通过flag传,参数太多建议改成读取配置文件

√正确:

  1. 读取5个以上的flag时使用,配置文件+viper,环境变量等,减少flag的个数到5个以内;

  2. 参数的值不要过长,仅纯文本string、int和bool类型,富文本、json等建议通过flag传文件路径,在run方法的逻辑读取文件,进行操作

  3. 短平快、小型脚手架推荐使用cobra封装命令集,同时学习了解pflag、flag、viper库

说明

cobra 简介

cobra既是一个用于创建强大现代CLI应用程序的库,也是一个生成应用程序和命令文件的程序。cobra被用在很多go语言的项目中,比如 Kubernetes、Docker、Istio、ETCD、Hugo、Github CLI等等,更多项目详见此列表。

cobra 概念

cobra由命令、参数、标志组成
commands代表动作,args是事物,flags是动作的修饰符(一般两个-连接符)
模式如下:
APPNAME VERB NOUN --ADJECTIVE. or APPNAME COMMAND ARG --FLAG(APPNAME 动词 名词 形容词 或者 APPNAME 命令 参数 标志)
如下的例子,server 是command,port是flag

hugo server --port=1313

Commands

命令是交互程序的中心,每一个命令都有子命令。
链接:cobra package - github.com/spf13/cobra - Go Packages
需要rootCmd和其他命令(按需)

Flags

flag是一种修改命令行为的方式,cobra支持完全兼容POSIX标志,也支持go flag package,cobra可以定义到子命令上的标志,也可以仅对该命令可用的标志

Args

args参数是命令command需要识别的一些值,cobra可以指定读取任意数量的参数,也可以不接受参数,此时该输入会被当作未定义的command报错。
command、args搭配使用

教程正文

demo1 快速了解

参考:Go语言命令行利器cobra使用教程 - 简书

demo

main.go负责调用rootCmd execute方法

import "cobra_cmd/cmd"

func main() {
    
    
    cmd.Execute()
}

在cmd目录下创建rootcmd.go

//每一个command命令,都是一个 &cobra.Command{
    
    
} 示例
var rootCmd = &cobra.Command{
    
    
    Use:   "root-cli",
    Short: "示例脚本 root cli",
    Long:  `这是 cobra 测试程序使用的示例脚本,您可参考cobra其他文档并随时更新本demo`,
         //参数验证器,设置某个脚本
能接受参数的个数。
    Args:  cobra.MinimumNArgs(1),
    RunE:  runRoot, //RunE Run字段都是执行具体的函数
,抽到别处定义
}
//rootCmd.Execute() 是命令执行入口
func Execute() {
    
    
    if err := rootCmd.Execute(); err != nil {
    
    
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
//具体的执行逻辑
func runRoot(cmd *cobra.Command, args []string) error {
    
    
    fmt.Printf("execute %s args:%v \n", cmd.Name(), args)
        //例如 这里处理无参数启动时程序处理
        if len(args) < 1 {
    
    
          return errors.New("requires at least one arg")
        }
     // do something.

    return nil

}

var cmdPrint = &cobra.Command{
    
    
    Use:   "print [string to print]",
    Short: "Print anything to the screen",
    Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("Print: " + strings.Join(args, " "))
    },
}

var cmdEcho = &cobra.Command{
    
    
    Use:   "echo [string to echo]",
    Short: "Echo anything to the screen",
    Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("Print: " + strings.Join(args, " "))
    },
}

var cmdTimes = &cobra.Command{
    
    
    Use:   "times [# 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, " "))
        }
    },
}

 //定义 标志
var echoTimes int

func init() {
    
    
    //通过AddCommand组成父子关系,多级命令
    rootCmd.AddCommand(helloCmd)

    // root有两个子命令,和一个cmdEcho命令下的子命令cmdTimes
    //一级root --> 二级print,echo --> 三级times
    rootCmd.AddCommand(cmdPrint, cmdEcho)
    cmdEcho.AddCommand(cmdTimes)

    //本地标志,只在mdTimes命令

起作用
    cmdTimes.Flags().IntVarP(&echoTimes, "echoTimes", "t", 1, "times to echo the input")
}

以上是一个入门demo,演示了命令command结构、标志的定义和读取、命令组成上下级、父子关系。

重点:
1.rootCmd和其他command放 cmd 包下;
2.在单独的commandXxx.go文件里,为每一个command定义 &cobra.Command{} 结构体并在init()方法里通过AddCommand() 做父子上下级关系关联、以及flag标志的读取操作;
3.main()入口方法调用到rootCmd.Execute()即完成工作。

知识点:Command创建命令

结构如下,列举大概常用的字段:

&cobra.Command{
    
    
        //
命令名称
    Use:   "echo [string to echo]",
        //
命令别名
    Aliases: []string{
    
    "log","output"},
        //
命令简介
    Short: "Echo anything to the screen",
        //
命令详细说明
    Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("Print: " + strings.Join(args, " "))
    },
}
  • use 是该命令的用法,单行用法消息,表示了某一级命令,可以理解为该命令的名字;
  • Short、Long是该命令的一行简介、一大段详细介绍文本;具体会在输出help帮助时才有不一样的提示,可以粗暴设置成同样内容;
  • Run、RunE是命令被执行时的具体逻辑,RunE会返回error给上层;
  • Alias是一组可代替Use那个名字的,同名,都会映射到本命令;
  • Args:参数验证,可以限制参数的个数等等,有如图几种验证方法:
    在这里插入图片描述

demo2 使用参数验证器、钩子函数

demo

package main

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

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.`,
    //运行命令的逻辑
    //Run: func(cmd *cobra.Command, args []string) {
    
    
    //debug
    //fmt.Println("== root命令运行成功 ==")

    //},

}

// 再定义二级子命令:
var createCmd = &cobra.Command{
    
    
    Use:   "create",
    Short: "创建书签",
}

var urlCmd = &cobra.Command{
    
    
    Use:   "url",
    Short: "Bookmark a url link ",
    Long:  "收藏一个url链接",

    //cobra 常用的参数配置校验器如下:
    //
    //MinimumNArgs(int) 当参数数目低于配置的最小参数个数时报错
    //MaximumNArgs(int) 当参数数目大于配置的最大参数个数时报错
    //ExactArgs(int)    如果参数数目不是配置的参数个数时报错
    //NoArgs            没有参数则报错

    //参数验证除了RangeArgs 还有 NoArg、ExactArgs、
    Args: cobra.RangeArgs(1, 3),

    Run: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("link is:", link)
        if len(args) == 2 {
    
    
            fmt.Println(args[0], args[1])
        } else if len(args) == 0 {
    
    
            fmt.Println("收藏链接参数:", args)
            return
        } else {
    
    
            fmt.Println("收藏链接参数:", args)
        }
        file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
        if err != nil {
    
    
            fmt.Println(err)
        }
        //及时关闭file句柄
        defer file.Close()
        //写入文件时,使用带缓存的 *Writer
        write := bufio.NewWriter(file)
        for i := 0; i < 5; i++ {
    
    
            write.WriteString("link \n")
        }
        //Flush将缓存的文件真正写入到文件中
        write.Flush()
    },

}
var link string
var filePath string

// init 方法完成父子关系绑定,标志的初始化设置以及其他初始化工作
func init() {
    
    
    rootCmd.AddCommand(createCmd)
    createCmd.AddCommand(urlCmd)
    //持久标志,传递给所有子命令
    rootCmd.PersistentFlags().StringVarP(&filePath, "path", "p", "./bookmarks.txt", "save to txt file")
    //局部标志,仅用于具体一个子命令
    urlCmd.Flags().StringVarP(&link, "link", "l", "", "收藏链接格式: 链接地址 名称")

    //钩子函数演示
    rootCmd.AddCommand(hookCmd)
    hookCmd.AddCommand(hookSub)
    hookSub.AddCommand(hookfinal)

}
func main() {
    
    

    // For environment variables.
    viper.SetEnvPrefix("core")
    viper.AutomaticEnv()
    if err := rootCmd.Execute(); err != nil {
    
    
        fmt.Println(err)
        os.Exit(1)
    }

}

// / 钩子函数测试
var hookCmd = &cobra.Command{
    
    
    Use:   "hookroot",
    Short: "钩子函数测试 一级",
    Long:  "测试 生命周期的不同钩子函数的继承关系",
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hookroot: PersistentPreRun")
    },
    PreRun: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hookroot: PreRun")
    },
    Run: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hookroot: Running...")
    },
    PostRun: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hookroot: PostRun")
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hookroot: PersistentPostRun")
    },
}

// // 会继承 父命令的PersistentPreRun、PersistentPostRun
var hookSub = &cobra.Command{
    
    
    Use:   "hooksub",
    Short: "钩子函数测试 二级",
    PreRun: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hooksub: PreRun")
    },
    Run: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hooksub: Running...")
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hooksub: PersistentPostRun") //覆盖了root的PersistentPostRun,并传给下级
    },
}

// // 会继承 父命令的PersistentPreRun、PersistentPostRun
var hookfinal = &cobra.Command{
    
    
    Use:     "hookfinal",
    Aliases: []string{
    
    "final", "finally"},
    Short:   "钩子函数测试 三级",
    PreRun: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hookfinal: PreRun")
    },
    Run: func(cmd *cobra.Command, args []string) {
    
    
        fmt.Println("hookfinal: Running...")
    },
}

output:
//go run .\helloworld.go hookroot hooksub hookfinal
//hookroot: PersistentPreRun
//hookfinal: PreRun
//hookfinal: Running...
//hooksub: PersistentPostRun/

以上是一个进阶demo,演示了PersistentFlags和Flags的区别,以及钩子函数在向下传递时可被覆盖

重点:
1.PersistentFlags 设置的是持久标志,由rootCmd根命令设置,可被其他命令使用,而flag设置的只能在当前命令使用;
2.在单独的command执行之前、之后都可以执行其他操作,也即预先处理、后置处理。执行顺序为PersistentPreRun、PreRun、Run、PostRun、PersistentPostRun
3.带Persistent的可以传递给子命令(继承),但也可以被子命令重新声明而被覆盖(重写)
4.不带Persistent开头的前后处理

知识点:参数验证方法

以下代码:Args: cobra.MinimumNArgs(1)就是一个验证方法

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

var rootCmd = &cobra.Command{
    
    
  Use:   "myapp",
  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.`,
  Args: cobra.MinimumNArgs(1),
  Run: func(cmd *cobra.Command, args []string) {
    
    
    // Do something here
  },
}

func init() {
    
    
  // Here you will define your flags and configuration settings.
}

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

可以看到在 rootCmd 中设置了 Args: cobra.MinimumNArgs(1),这将强制要求至少传入一个参数,否则会抛出一个错误。此外,还可以使用 cobra 提供的其他参数验证方法,例如:

cobra.MaximumNArgs(n int):强制要求不超过 n 个参数。
cobra.OnlyValidArgs([]string):强制要求传入的参数必须是所指定的 string 类型的 slice 中的值。
cobra.ArgRange(min, max int):强制要求传入参数的数量在 min 和 max 之间。
以上是一些常用的参数验证方法,Cobra 还有更多的验证方法和选项,可以根据实际需求进行选择和设置。

知识点:生命周期钩子函数

参考:
Go 每日一库之 cobra - 知乎
Cobra 中文文档 - 掘金

可以在执行命令之前和之后运行一个函数。PersistentPreRun 和 PreRun 函数将在 Run 之前执行。PersistentPostRun 和 PostRun 会在 Run 之后运行。如果子级未声明自己的 Persistent * Run 函数,则子级将继承父级的。这些函数的执行顺续如下:

PersistentPreRun
PreRun
Run
PostRun
PersistentPostRun
PersistentXxx是可以被子命令继承的前、后置处理

知识点:帮助命令

当你添加了子命令,Cobra 会自动添加一些帮助命令。当你执行 app help 命令时会显示帮助信息。另外,help 还支持其他命令作为输入参数。举例来说,你有一个没有额外配置的 create 命令,app help create 是有效的。每一个命令还会自动获取一个 --help 标志。

help 就像其他命令一样。并没有特殊的逻辑或行为。实际上,你可以根据需要提供自己的服务。
定义你自己的 help
你可以使用下面的方法提供你自己的 Help 命令或模板。

cmd.SetHelpCommand(cmd *Command)
cmd.setHelpCommand(f func(*Command, []string))
cmd.setHelpTemplate(s string)

后两者也适用于所有子命令。

知识点:使用信息

当用户提供无效的标志或无效的命令时,Cobra 会通过向用户显示 usage 进行响应。
这时可以自定义usage用法,提供自己的 usage函数和模板

cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)

可以参考 GitHub CLI 项目中usage使用信息的写法。

demo3 生成帮助文档文件(markdown文件)

参考cobra document#mrkdown-docs
cobra支持生成所有命令的帮助文档树doc.GenMarkdownTree(cmd,dir)

demo

main.go:

import (
	"github.com/spf13/cobra/doc"
	"log"
	"os"
)

func InitCommand() {
    
    
	rootCmd.AddCommand(helloCmd)
	// 两个顶层的命令,和一个cmdEcho命令下的子命令cmdTimes
	rootCmd.AddCommand(cmdPrint, cmdEcho)
	cmdEcho.AddCommand(cmdTimes)
	//utils.Mkdir("./docs")
	os.MkdirAll("H:\\docs", os.ModePerm) //显式创建目标文件夹,文件夹存在时再次调用不会做任何事情
	err := doc.GenMarkdownTree(rootCmd, "H:\\docs") //核心api  GenMarkdownTree
	if err != nil {
    
    
		log.Fatal(err)
	}
}

知识点 生成帮助文档文件(markdown文件)

给某个命令及其子命令生成帮助文档树(一系列文档),参考以下代码片段:

err := doc.GenMarkdownTree(rootCmd, "H:\\docs") //核心api  GenMarkdownTree
	if err != nil {
    
    
		log.Fatal(err)
	}

即可在目标文件夹下看到生成的md文件:
在这里插入图片描述

其他高级用法或生成其他格式帮助文档,请参考cobra document#mrkdown-docs ,以及cobra代码库的github.com/spf13/cobra/doc文件夹下的代码和文档。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_28421553/article/details/131479205