怎样上手cobra

2024-06-11 00:22:19 浏览数 (1)

cobra是go语言中一个非常强大的命令行构建工具,我们非常熟悉的docker、k8s、etcd都是基于cobra开发的。如果你想打造自己的命令行工具,那么cobra就是你的最佳选择。

cobra支持的功能非常完善,比如:help、子命令、标志等,它的使用还是非常简单的,下面我们一起看下。

一、命令组成结构

在正式开始介绍cobra来,我们先来了解下命令的组成结构。

在开发中我们经常使用git,常常会克隆代码仓库,比如: git clone git@github.com:spf13/cobra.git --depth=1

  • git 是根命令(root command)
  • clone 是命令(也可以认为是git的子命令),代表要执行的动作
  • git@github.com:spf13/cobra.git 是参数(argument),代表操作的对象
  • --depth=1 是标志(flag),它是对命令的补充、修饰

从上面我们可以看出一个命令由命令参数标志组成,cobra也不例外,它也围绕这三者展开。

二、一个最简单的命令

现在我们一起用cobra来构造一个最简单的命令,比如:我想构造一个叫hello的命令,执行后打印hello world

初始化项目

代码语言:javascript复制
shell复制代码mkdir cobra-practice
cd cobra-practice
go mod init example/cobra-practice
touch main.go

main.go内容

代码语言:javascript复制
go复制代码package main

import (
	"fmt"

	"github.com/spf13/cobra"
)

func main() {
	rootCmd := cobra.Command{
		// Use 定义命令的名字
		Use: "hello",
		// Short 简单描述
		// Long 详细描述
		Short: "Hello command",
		Long:  "This is hello command",
		// Run 命令执行的逻辑
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Println("Hello world")
		},
	}

	// Execute 启动命令
	rootCmd.Execute()
}

执行go mod tidy后,执行go run main.go 你将看到Hello world输出。 我们在项目路径下执行go build -o hello 可以编译出一个可执行文件hello,然后执行./hello 你将看到Hello world输出。

注意:go build -o . 如果不指定可执行文件名,生成的可执行文件并不是命令哦。

三、参数(arg)

前面我们构建了hello命令,但是它没有参数,我们来添加一个参数。我们希望不是每次都打印Hello world,而是打印Hello [name]

代码语言:javascript复制
go复制代码package main

import (
	"fmt"

	"github.com/spf13/cobra"
)

func main() {
	rootCmd := cobra.Command{
		// Use 定义命令的名字
		// []用于表明它需要一个参数
		Use: "hello [name]",
		// Short 简单描述
		// Long 详细描述
		Short: "Hello command",
		Long:  "This is hello command",
		// Run 命令执行的逻辑
		// args 是命令行参数
		// 如果命令行没有参数,args 是一个空数组
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Hello: %sn", args[0])
		},
	}

	// Execute 启动命令
	rootCmd.Execute()
}

我们可以看到添加参数主要是利用args来实现的,我们传入的参数存放在args数组切片中。我们执行 go run main.go dmy输出Hello: dmy

1 参数校验

上面我们如果运行go run main.go会报错,如下:

代码语言:javascript复制
go复制代码dongmingyan@pro ⮀ ~/go_playground/cobra-practice ⮀ go run main.go    
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
main.main.func1(0xc000004300?, {0x13401f8?, 0x0?, 0x0?})

原因是我们没有给hello命令添加参数,所以args是空数组,我们无法通过args[0]获取到参数。

因此有些时候我们有必要进行参数的校验,直接用Args就能实现,代码如下:

代码语言:javascript复制
go复制代码package main

import (
	"fmt"

	"github.com/spf13/cobra"
)

func main() {
	rootCmd := cobra.Command{
		// Use 定义命令的名字
		// []用于表明它需要一个参数
		Use: "hello [name]",
		// Short 简单描述
		// Long 详细描述
		Short: "Hello command",
		Long:  "This is hello command",
		// 限定必须准确的有一个参数
		Args: cobra.ExactArgs(1),
		// Run 命令执行的逻辑
		// args 是命令行参数
		// 如果命令行没有参数,args 是一个空数组
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Hello: %sn", args[0])
		},
	}

	// Execute 启动命令
	rootCmd.Execute()
}

此时如果继续执行go run main.go输出如下:

代码语言:javascript复制
css复制代码dongmingyan@pro ⮀ ~/go_playground/cobra-practice ⮀ go run main.go 
Error: accepts 1 arg(s), received 0
Usage:
  hello [name] [flags]

Flags:
  -h, --help   help for hello

看到了吧,这个提示友好了很多。

2.自定义参数校验

cobra.ExactArgs()是cobra自带的参数校验,非常方便,当然如果你想自定义参数校验也是可以的。

代码语言:javascript复制
go复制代码package main

import (
	"errors"
	"fmt"

	"github.com/spf13/cobra"
)

func main() {
	rootCmd := cobra.Command{
		// ...
    // 自定义校验 一个函数结构,返回error
		Args: func(cmd *cobra.Command, args []string) error {
			if args == nil || len(args) != 1 {
				return errors.New("must have one argument")
			}
			return nil
		},
		// ...
	}

	// Execute 启动命令
	rootCmd.Execute()
}

此时执行go run main.go会看到变成Error: must have one argument了。

3.内置的参数校验列表

除了上面的ExactArgs还有很多,这里列下。

  1. NoArgs 无任何参数
  2. ExactArgs(n) 必须恰好有n个参数
  3. MinimumNArgs(n) 至少有n个参数
  4. MaximumNArgs(n) 最多有n个参数
  5. RangeArgs(min, max) 参数个数在min和max之间
  6. OnlyValidArgs 验证传入参数是否在list中 PS:
  • 这里如果没有传入任何参数,那么不会做校验
  • 需要搭配:ValidArgs-指定参数的值列表一起使用。
代码语言:javascript复制
go复制代码validColors := []string{"red", "green", "blue"}

var cmdColor = &cobra.Command{
    Use:   "color",
    Short: "Color output",
    ValidArgs: validColors, // 指定参数的值列表
    Args: cobra.OnlyValidArgs, // 验证传入参数是否在list中,如果不在则报错
    Run: func(cmd *cobra.Command, args []string) {
        // 处理颜色参数
    },
}
  1. ArbitraryArgs 任意数量参数

四、标志(flag)

前面我们学习了参数,这里我们进一步学习标志如何使用。

假设我们需要实现,一个verson标志,如果为true的话,则为详细版本。

代码语言:javascript复制
go复制代码package main

import (
	"fmt"
	"time"

	"github.com/spf13/cobra"
)

// 是否是冗余版本
var verbose bool

func main() {
	rootCmd := cobra.Command{
		Use:   "hello [name]",
		Short: "Hello command",
		Long:  "This is hello command",
		Args:  cobra.ExactArgs(1),
		Run: func(cmd *cobra.Command, args []string) {
			// 使用标志绑定的变量
			if verbose { // 冗余版本
				fmt.Printf("%v hello: %sn", time.Now(), args[0])
			} else {
				fmt.Printf("Hello: %sn", args[0])
			}
		},
	}

	// 定义标志 并将verbose绑定到全局变量verbose上
	rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
	rootCmd.Execute()
}

上面我们执行go run main.go dmy -v=true,输出2024-06-09 15:39:41.591036 0800 CST m= 0.000355867 hello: dmy

五、子命令

前面的hello是一个命令,同时它也是一个根命令,我们可以在hello的基础上添加子命令。

我们添加一个version的子命令,用于打印版本信息。

代码语言:javascript复制
go复制代码package main

import (
	"fmt"
	"time"

	"github.com/spf13/cobra"
)

// 是否是冗余版本
var verbose bool

// 版本信息
var version = "v0.0.1"

func main() {
	rootCmd := cobra.Command{
		Use:   "hello [name]",
		Short: "Hello command",
		Long:  "This is hello command",
		Args:  cobra.ExactArgs(1),
		Run: func(cmd *cobra.Command, args []string) {
			if verbose {
				fmt.Printf("%v hello: %sn", time.Now(), args[0])
			} else {
				fmt.Printf("Hello: %sn", args[0])
			}
		},
	}

	// 版本命令
	versionCmd := &cobra.Command{
		Use:   "version",
		Short: "Print the version number",
		Long:  "Print the version number",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("hello version: %sn", version)
		},
	}

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

	// 在rootCmd中添加version子命令
	rootCmd.AddCommand(versionCmd)
	rootCmd.Execute()
}

我们执行go run main.go version,输出hello version: v0.0.1

六、持续标志

我们此时如果执行go run main.go -v=true会发现报错Error: unknown shorthand flag: 'v' in -v 原因是,我们的v只在rootCmd中定义,而versionCmd中并没有效。

如果我们想在versionCmd中也能拥有和rootCmd一样的v标志,我们可以使用PersistentFlags

代码语言:javascript复制
go复制代码// 改变添加标志的行为如下行即可
// PersistentFlags 持续标志 会顺延到它的子命令也有效
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")

七、钩子

经过前面的学习,对于常用的命令构造基本够用了;但是cobra还提供了一些更好的功能,比如钩子,什么是构子呢?

比如我们希望在执行某个命令前、后执行一些操作,比如读取配置文件,那么我们可以使用钩子。

看代码

代码语言:javascript复制
go复制代码package main

import (
	"fmt"
	"time"

	"github.com/spf13/cobra"
)

// 是否是冗余版本
var verbose bool

// 版本信息
var version = "v0.0.1"

func main() {
	rootCmd := cobra.Command{
		Use:   "hello [name]",
		Short: "Hello command",
		Long:  "This is hello command",
		Args:  cobra.ExactArgs(1),
		// 命令执行前执行
		PreRun: func(cmd *cobra.Command, args []string) {
			fmt.Println("hello执行前执行")
		},
		Run: func(cmd *cobra.Command, args []string) {
			if verbose {
				fmt.Printf("%v hello: %sn", time.Now(), args[0])
			} else {
				fmt.Printf("Hello: %sn", args[0])
			}
		},
		// 命令执行后执行
		PostRun: func(cmd *cobra.Command, args []string) {
			fmt.Println("hello执行后执行")
		},
	}

	versionCmd := &cobra.Command{
		Use:   "version",
		Short: "Print the version number",
		Long:  "Print the version number",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("hello version: %sn", version)
		},
	}

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

	rootCmd.AddCommand(versionCmd)
	rootCmd.Execute()
}

执行go run main.go dmy

代码语言:javascript复制
shell复制代码dongmingyan@pro ⮀ ~/go_playground/cobra-practice ⮀ go run main.go dmy
hello执行前执行
Hello: dmy
hello执行后执行

如果我们希望钩子在子命令中生效,我们可以使用PersistentPreRunPersistentPostRun

八、搭配viper

代码语言:javascript复制
go复制代码package main

import (
	"fmt"
	"time"

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

var verbose bool

// 版本信息
var version = "v0.0.1"

func main() {
	rootCmd := cobra.Command{
		Use:   "hello [name]",
		Short: "Hello command",
		Long:  "This is hello command",
		Args:  cobra.ExactArgs(1),
		PersistentPreRun: func(cmd *cobra.Command, args []string) {
			fmt.Println("hello执行前执行")
		},
		Run: func(cmd *cobra.Command, args []string) {
			if verbose {
				fmt.Printf("%v hello: %sn", time.Now(), args[0])
			} else {
				fmt.Printf("Hello: %sn", args[0])
			}
		},
		PostRun: func(cmd *cobra.Command, args []string) {
			fmt.Println("hello执行后执行")
		},
	}

	versionCmd := &cobra.Command{
		Use:   "version",
		Short: "Print the version number",
		Long:  "Print the version number",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("hello version: %sn", version)
		},
	}

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

	// 不带Var 此时没有绑定到变量上
	rootCmd.PersistentFlags().StringP("config", "c", "", "config file (default is $HOME/.hello.yaml)")
	// 绑定到viper的config中
	viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))

	rootCmd.AddCommand(versionCmd)
	rootCmd.Execute()

	// 一定要注意 viper变量的获取要在root.CmdExecute()之后执行
	// 因为要保证标志解析后使用
	fmt.Println("config:", viper.GetString("config"))
}

执行go run main.go dmy -c=hello.yaml 将会看到,

代码语言:javascript复制
makefile复制代码hello执行前执行
Hello: dmy
hello执行后执行
config: hello.yaml

我们能从命令行中获取到配置文件,并且配置文件是绑定到viper的config变量中的,就可以进一步对配置文件进行操作了。

九、Run与RunE

RunE是cobra提供的带错误处理的版本,建议使用RunE。它相比于Run多了一个error的返回值。如果返回了一个error,那么cobra会打印错误信息并退出。如果使用Run需要我们自己处理错误。

0 人点赞