https://cloud.tencent.com/developer/article/2403412

1、Cobra简介

Cobra 是一个 Go 语言开发的命令行(CLI)框架,它提供了简洁、灵活且强大的方式来创建命令行程序。它包含一个用于创建命令行程序的库(Cobra 库),以及一个用于快速生成基于 Cobra 库的命令行程序工具(Cobra 命令)。

go get -u github.com/spf13/cobra@latest

cobra的三个元素:command命令、arg参数、flag标志

AppName command arg --flag

🔽 🔽 🔽 🔽

AppName verb noun --adjective

Cobra 的工作原理和内部机制主要包括以下几个关键步骤:

  • 初始化命令:首先,创建一个根命令并初始化 Cobra 库。根命令是整个命令行应用程序的入口,可以包含多个子命令和相关的参数和标志。

  • 定义命令和子命令:使用 Cobra 提供的 API 定义命令和子命令,并为每个命令定义相应的执行函数。

  • 解析命令行参数:当用户在命令行中输入命令时,Cobra 会解析用户输入的命令、参数和标志,并调用相应的执行函数来处理。

  • 执行命令:根据用户输入的命令,Cobra 会调用相应的执行函数来执行命令所需的操作,包括打印输出、调用其他函数等。

  • 处理错误和异常:在执行过程中,Cobra 会检测并处理可能出现的错误和异常情况,以保证命令行应用程序的稳定性和可靠性。

通过这些步骤,Cobra 能够实现一个完整的命令行应用程序,并提供丰富的功能和选项,包括命令、子命令、参数和标志的定义、解析、执行,以及自动生成帮助文档和自动补全等功能。

(1)如何处理命令行参数和标志

Cobra 提供了一种简单而强大的方式来处理命令行参数和标志。可以通过 Cobra 提供的 API 来定义参数和标志,并在执行函数中获取和使用它们。例如:

var cmd = &cobra.Command{
    Use:   "say",
    Short: "Print a message",
    Run: func(cmd *cobra.Command, args []string) {
        message, _ := cmd.Flags().GetString("message")
        fmt.Println(message)
    },
}

func init() {
    cmd.Flags().StringP("message", "m", "Hello World", "Message to print")
}

在这个例子中,我们定义了一个名为say的命令,其中包含一个名为message的标志。在执行函数中,我们通过cmd.Flags().GetString("message")来获取标志的值,并打印输出。

(2)自动生成帮助文档和自动补全的功能

Cobra 提供了自动生成帮助文档和自动补全的功能,使得用户能够更方便地了解命令行应用程序的使用方式,并提高命令行操作的效率。要启用这些功能,您只需在初始化命令时调用相应的方法即可。例如:

var rootCmd = &cobra.Command{
    Use:   "app",
    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:\n\nCobra is a CLI library for Go that empowers applications.\nThis application is a tool to generate the needed files\nto quickly create a Cobra application.",
    Run: func(cmd *cobra.Command, args []string) { },
}

func init() {
    cobra.OnInitialize(initConfig)

    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.app.yaml)")

    rootCmd.AddCommand(addCmd)
    rootCmd.AddCommand(deleteCmd)

    // Enable auto-completion
    rootCmd.GenBashCompletion(os.Stdout)

    // Enable automatic generation of help documentation
    rootCmd.SetHelpCommand(&cobra.Command{Use: "no-help", Hidden: true})
    rootCmd.AddCommand(helpCmd)
}

在这个例子中,我们通过调用 rootCmd.GenBashCompletion(os.Stdout) 方法来启用自动补全功能,并通过 rootCmd.SetHelpCommand() 方法来禁用默认的帮助命令,并使用自定义的帮助命令。这样,用户就可以通过输入命令时按下 Tab 键来自动补全命令和参数,并使用 app help 命令来获取帮助信息了。

2、Cobra使用

我们将创建一个简单的文件管理器命令行应用程序,该应用程序具有以下功能:

  • 列出指定目录下的所有文件和子目录

  • 创建新文件或目录

  • 删除指定的文件或目录

我们首先定义根命令以及三个子命令:list、create 和 delete。根命令用于启动应用程序,而子命令用于执行具体的操作。我们将在各个子命令中添加各种参数和标志,以支持不同的功能。

var rootCmd = &cobra.Command{
    Use:   "filemgr",
    Short: "A simple file manager CLI tool",
    Long:  "filemgr is a simple file manager CLI tool that allows you to list, create, and delete files and directories.",
}

var listCmd = &cobra.Command{
    Use:   "list [directory]",
    Short: "List all files and directories in the specified directory",
    Args:  cobra.MaximumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        directory := "."
        if len(args) > 0 {
            directory = args[0]
        }
        // 实现列出文件和目录的逻辑
    },
}

var createCmd = &cobra.Command{
    Use:   "create <name>",
    Short: "Create a new file or directory with the specified name",
    Args:  cobra.ExactArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        name := args[0]
        // 实现创建文件或目录的逻辑
    },
}

var deleteCmd = &cobra.Command{
    Use:   "delete <name>",
    Short: "Delete the file or directory with the specified name",
    Args:  cobra.ExactArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        name := args[0]
        // 实现删除文件或目录的逻辑
    },
}

func init() {
    rootCmd.AddCommand(listCmd)
    rootCmd.AddCommand(createCmd)
    rootCmd.AddCommand(deleteCmd)
}

在上述子命令中,我们使用了Args字段来定义子命令所需的参数。此外,我们还可以使用Flags来定义各种参数和标志,以支持更多的功能。例如,我们可以为list命令添加一个标志来指定是否显示隐藏文件,为create命令添加一个标志来指定创建文件还是目录,为delete命令添加一个标志来指定是否递归删除目录。

var listCmd = &cobra.Command{
    Use:   "list [directory]",
    Short: "List all files and directories in the specified directory",
    Args:  cobra.MaximumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        directory := "."
        if len(args) > 0 {
            directory = args[0]
        }
        showHidden, _ := cmd.Flags().GetBool("hidden")
        // 实现列出文件和目录的逻辑
    },
}

func init() {
    listCmd.Flags().BoolP("hidden", "H", false, "Show hidden files and directories")
    rootCmd.AddCommand(listCmd)
}

3、特性和技巧

3.1、处理错误和异常

在命令行应用程序开发中,处理错误和异常情况是至关重要的。Cobra 提供了一种简单而有效的方式来处理错误和异常情况,使得我们能够及时捕获和处理可能出现的错误。以下是一些处理错误和异常情况的常用技巧:

错误处理:在执行命令时,可以通过返回错误对象来指示执行过程中是否发生了错误。在执行函数中,可以使用fmt.Errorf()函数或errors.New()函数来创建错误对象,并返回给 Cobra。例如:

Run: func(cmd *cobra.Command, args []string) error {
    if err := doSomething(); err != nil {
        return fmt.Errorf("error occurred: %v", err)
    }
    return nil
},

异常处理:除了错误处理外,您还可以使用 Go 语言的panicrecover机制来处理更严重的异常情况。在执行函数中,您可以使用panic()函数来触发一个异常,然后在调用该函数的上层函数中使用recover()函数来捕获并处理该异常。例如:

Run: func(cmd *cobra.Command, args []string) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    // 执行可能触发 panic 的代码
    panic("something went wrong")
},

3.2、自定义函数和选项

除了内置的命令和选项外,Cobra 还允许我们定义自定义函数和选项,以满足特定需求。例如,您可以定义一个自定义函数来实现某个特定功能,并在需要时在命令中调用该函数。您还可以定义自定义选项来扩展 Cobra 的功能,以支持更多的配置选项。以下是一些示例:

// 自定义函数
func customFunction() {
    // 实现自定义功能
}

var customCmd = &cobra.Command{
    Use:   "custom",
    Short: "Execute custom function",
    Run: func(cmd *cobra.Command, args []string) {
        customFunction()
    },
}

// 自定义选项
var customOption string

func init() {
    rootCmd.PersistentFlags().StringVar(&customOption, "custom-option", "", "A custom option")
}

3.3、PreRun and PostRun Hooks

可以在命令的 main 函数之前或之后运行函数。

  • PersistentPreRun

  • PreRun

  • Run

  • PostRun

  • PersistentPostRun

package main
import (
  "fmt"
  "github.com/spf13/cobra"
)
func main() {
  var rootCmd = &cobra.Command{
    Use:   "root [sub]",
    Short: "My root command",
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args)
    },
    PreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd Run with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args)
    },
  }
  var subCmd = &cobra.Command{
    Use:   "sub [no options!]",
    Short: "My subcommand",
    PreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd Run with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PersistentPostRun with args: %v\n", args)
    },
  }
  rootCmd.AddCommand(subCmd)
  rootCmd.SetArgs([]string{""})
  rootCmd.Execute()
  fmt.Println()
  rootCmd.SetArgs([]string{"sub", "arg1", "arg2"})
  rootCmd.Execute()
}
Inside rootCmd PersistentPreRun with args: []
Inside rootCmd PreRun with args: []
Inside rootCmd Run with args: []
Inside rootCmd PostRun with args: []
Inside rootCmd PersistentPostRun with args: []
Inside rootCmd PersistentPreRun with args: [arg1 arg2]
Inside subCmd PreRun with args: [arg1 arg2]
Inside subCmd Run with args: [arg1 arg2]
Inside subCmd PostRun with args: [arg1 arg2]
Inside subCmd PersistentPostRun with args: [arg1 arg2]

4、简单实例

4.1、文件管理器

我们将创建一个简单的文件管理器命令行应用程序,该应用程序具有以下功能:

  • 列出指定目录下的所有文件和子目录

  • 创建新文件或目录

  • 删除指定的文件或目录

package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "file-manager",
	Short: "A simple file manager CLI",
	Long:  `This is a simple command-line tool to manage files and directories.`,
}

var listCmd = &cobra.Command{
	Use:   "list [dir]",
	Short: "List files and directories",
	Long:  `List all files and directories in the specified directory. Optionally include hidden files.`,
	Args:  cobra.ExactArgs(1), // Ensure that exactly one argument (directory) is provided
	Run: func(cmd *cobra.Command, args []string) {
		dir := args[0]
		includeHidden, _ := cmd.Flags().GetBool("hidden")
		listFiles(dir, includeHidden)
	},
}

var createCmd = &cobra.Command{
	Use:   "create [name]",
	Short: "Create a new file or directory",
	Long:  `Create a new file or directory. Use the --dir flag to create a directory, or create a file by default.`,
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		name := args[0]
		isDir, _ := cmd.Flags().GetBool("dir")
		createFileOrDir(name, isDir)
	},
}

var deleteCmd = &cobra.Command{
	Use:   "delete [name]",
	Short: "Delete a file or directory",
	Long:  `Delete the specified file or directory. Use the --recursive flag to delete a directory recursively.`,
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		name := args[0]
		recursive, _ := cmd.Flags().GetBool("recursive")
		deleteFileOrDir(name, recursive)
	},
}

func init() {
 // 也不是所有的flag都可以解析,需要输入提前定义好的flag才可以进行下一步
	listCmd.Flags().BoolP("hidden", "h", false, "Include hidden files")
	createCmd.Flags().BoolP("dir", "d", false, "Create a directory instead of a file")
	deleteCmd.Flags().BoolP("recursive", "r", false, "Recursively delete directories")

	// Add subcommands to the root command
	rootCmd.AddCommand(listCmd)
	rootCmd.AddCommand(createCmd)
	rootCmd.AddCommand(deleteCmd)
}

func listFiles(dir string, includeHidden bool) {
	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		// Skip hidden files unless the flag is set
		if !includeHidden && info.Name()[0] == '.' {
			return nil
		}
		fmt.Println(path)
		return nil
	})

	if err != nil {
		fmt.Println("Error listing files:", err)
	}
}

func createFileOrDir(name string, isDir bool) {
	var err error
	if isDir {
		err = os.Mkdir(name, 0755)
	} else {
		_, err = os.Create(name)
	}

	if err != nil {
		fmt.Println("Error creating file or directory:", err)
	} else {
		fmt.Println("Created:", name)
	}
}

func deleteFileOrDir(name string, recursive bool) {
	var err error
	if recursive {
		err = os.RemoveAll(name) // Removes directories and their contents recursively
	} else {
		err = os.Remove(name) // Removes files or empty directories
	}

	if err != nil {
		fmt.Println("Error deleting file or directory:", err)
	} else {
		fmt.Println("Deleted:", name)
	}
}

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

4.2、docker save

func NewImageCommand(dockerCli command.Cli) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "image",
		Short: "Manage images",
		Args:  cli.NoArgs,
		RunE:  command.ShowHelp(dockerCli.Err()),
	}
	cmd.AddCommand(
		NewBuildCommand(dockerCli),
		NewHistoryCommand(dockerCli),
		NewImportCommand(dockerCli),
		NewLoadCommand(dockerCli),
		NewPullCommand(dockerCli),
		NewPushCommand(dockerCli),
		NewSaveCommand(dockerCli),
		NewTagCommand(dockerCli),
		newListCommand(dockerCli),
		newRemoveCommand(dockerCli),
		newInspectCommand(dockerCli),
		NewPruneCommand(dockerCli),
	)
	return cmd
}

// NewSaveCommand creates a new `docker save` command
func NewSaveCommand(dockerCli command.Cli) *cobra.Command {
	var opts saveOptions

	cmd := &cobra.Command{
		Use:   "save [OPTIONS] IMAGE [IMAGE...]",
		Short: "Save one or more images to a tar archive (streamed to STDOUT by default)",
		Args:  cli.RequiresMinArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			opts.images = args
			return RunSave(cmd.Context(), dockerCli, opts)
		},
		Annotations: map[string]string{
			"aliases": "docker image save, docker save",
		},
		ValidArgsFunction: completion.ImageNames(dockerCli),
	}

	flags := cmd.Flags()

	flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
	flags.StringVar(&opts.platform, "platform", "", `Save only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
	_ = flags.SetAnnotation("platform", "version", []string{"1.48"})

	_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
	return cmd
}

// RunSave performs a save against the engine based on the specified options
func RunSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error {
	if opts.output == "" && dockerCli.Out().IsTerminal() {
		return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
	}

	if err := command.ValidateOutputPath(opts.output); err != nil {
		return errors.Wrap(err, "failed to save image")
	}

	var options image.SaveOptions
	if opts.platform != "" {
		p, err := platforms.Parse(opts.platform)
		if err != nil {
			return errors.Wrap(err, "invalid platform")
		}
		// TODO(thaJeztah): change flag-type to support multiple platforms.
		options.Platforms = append(options.Platforms, p)
	}

	responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options)
	if err != nil {
		return err
	}
	defer responseBody.Close()

	if opts.output == "" {
		_, err := io.Copy(dockerCli.Out(), responseBody)
		return err
	}

	return command.CopyToFile(opts.output, responseBody)
}