CLI 应用程序框架 —— Go Cobra 入门

本文通过丰富的代码示例演示了 Cobra 的基本功能,读者可以跟随这些示例,学习如何使用 Cobra

Cobra

Cobra 由以下两部分组成:

  • 用于创建 CLI 应用程序的库(cobra 库)
  • 用于生成 Cobra 应用程序的工具(cobra-cli)

概念

Cobra 建立在命令(command)、参数(arguments)和标志(flags)的结构之上,这三个组件共同定义了命令行应用程序的行为。

  • 命令(Commands)代表着命令行应用程序可以执行的特定操作
  • 参数(Args)是这些操作所针对的实体或项
  • 标志(Flags)是可以修改命令或参数行为的修饰符。

设计良好的命令行应用程序在使用时读起来像句子,其遵循的模式如下:

  • APPNAME VERB NOUN --ADJECTIVE,如:git clone URL --bare
  • APPNAME COMMAND ARG --FLAG,如:hugo server --port=1313

cobra-cli

cobra-cli 是一个命令行工具,用于生成 Cobra 应用程序。我们可以使用命令 go install github.com/spf13/cobra-cli@latest 来安装它。

初始化应用程序

安装完 cobra-cli 后,我们可以在项目根目录下执行命令 cobra-cli init 以初始化我们的 Cobra 应用程序。初始化完成后,我们的项目目录结构如下:

│  go.mod
│  go.sum
│  LICENSE	# cobra-cli 生成的许可证文件
│  main.go	# cobra-cli 生成的程序入口文件
│
└─cmd
        root.go	# cobra-cli 生成的根命令文件

cobra-cli 生成的代码文件具有详细的注释,这里我们就不作讲解。

使用 cobro-cli init 时,我们还可以附加 flag :

cobra-cli init --author="Sue211213@163.com" --license=mit --viper
 
#D:\workspace\golang\src\cobra-demo
  • --author 指定作者,作者信息会显示在文件的版权头信息和 LICENSE 文件中
  • --license 指定许可证,许可证信息会显示在 LICENSE 文件中
  • --viper 使用 viper

添加命令

初始化 Cobra 应用程序后,我们还可以使用 cobra-cli add 命令为我们的应用程序添加其他命令,如:

cobra-cli add serve
cobra-cli add config
cobra-cli add create -p 'configCmd'
  • -p 指定该命令的父级,默认情况下父级命令为 rootCmd

指定 command ,运行程序:

go run main.go serve
#serve called

go run main.go config
#config called

go run main.go config create
#create called

配置文件

我们可以将 flag 写入到配置文件中,然后以读取配置文件的形式使用 flag。

创建配置文件 .cobra.yaml

author: Sue211213 <sue211213@163.com>	# 作者信息
license: MIT							# 许可证信息,可以为 none,也可以自定义
useViper: true							# 启用 viper

使用配置文件进行初始化:

cobra-cli init --config=.cobra.yaml
#Using config file: .cobra.yaml
#Your Cobra application is ready at
#D:\workspace\golang\src\cobra-demo

cobra

使用标志

持久标志

我们可以为某个命令分配持久标志,持久标志能够被该命令及其所有子命令使用。

在下面这个示例中,我们为根命令分配了一个名为 verbose 的持久标志,由于是根命令上的持久标志,所以该标志是一个全局标志,可用于所有命令:

var rootCmd = &cobra.Command{
	Use:   "app",
}

var cmdFoo = &cobra.Command{
	Use:   "foo",

	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("foo executed")
		if verbose {
			fmt.Println("verbose output enabled")
		}
	},
}

var verbose bool

func init() {
	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output")
}

func main() {
	rootCmd.AddCommand(cmdFoo)
	
	_ = rootCmd.Execute()
}
go run main.go foo
#foo executed

go run main.go foo -v
#foo executed
#verbose output enabled
本地标志

我们可以为某个命令分配本地标志,本地标志只有该命令能够使用。

在下面这个示例中,我们为子命令 foo 分配了一个本地标志 name,该标志只能由子命令 foo 使用:

var rootCmd = &cobra.Command{
	Use:   "app",
}

var cmdFoo = &cobra.Command{
	Use:   "foo",

	Run: func(cmd *cobra.Command, args []string) {
		name, _ := cmd.Flags().GetString("name")
		fmt.Printf("Hello %s\n", name)
	},
}

func init() {
	cmdFoo.Flags().StringP("name", "n", "world", "The name to greet")
}

func main() {
	rootCmd.AddCommand(cmdFoo)
    
	_ = rootCmd.Execute()
}
go run main.go -n=sue211213@163.com
#Error: unknown shorthand flag: 'n' in -n=sue211213@163.com
#...

go run main.go foo -n=sue211213@163.com
#Hello sue211213@163.com
将标志与配置绑定

我们可以使用 viper 将我们的标志和配置信息进行绑定。

在下面的这个示例中,我们将 name 标志与环境变量 APP_NAME 进行绑定:

var rootCmd = &cobra.Command{
	Use:   "app",

	Run: func(cmd *cobra.Command, args []string) {
		name := viper.GetString("name")
		fmt.Printf("Hello, %s\n", name)
	},
}

func init() {
	rootCmd.Flags().StringP("name", "n", "world", "the name to greet")
	_ = viper.BindPFlag("name", rootCmd.Flags().Lookup("name"))
	viper.SetEnvPrefix("app")
	viper.AutomaticEnv()
}

func main() {
	_ = rootCmd.Execute()
}
APP_NAME=Bob go run main.go
#Hello, Bob
必填标志

我们可以为某个命令设置必填标志。

在下面这个示例中,我们为 app 命令设置了必填标志 name:

var rootCmd = &cobra.Command{
	Use:   "app",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func init() {
	rootCmd.Flags().StringP("name", "n", "World", "The name to greet")
	_ = rootCmd.MarkFlagRequired("name")
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go
#Error: required flag(s) "name" not set
#...
标志组

我们可以使用 MarkFlagsRequiredTogether 函数限制某些标志必须一起执行,如:

var rootCmd = &cobra.Command{
	Use:   "myapp",
	Short: "A simple command line application",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func init() {
	rootCmd.Flags().StringP("username", "u", "", "Username (required)")
	rootCmd.Flags().StringP("password", "p", "", "Password (required)")
	rootCmd.MarkFlagsRequiredTogether("username", "password")
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go --username=alice
#Error: if any flags in the group [username password] are set they must all be set; missing [password]
#...

我们可以使用 MarkFlagsRequiredTogether 函数限制某些标志不能同时出现,如:

var rootCmd = &cobra.Command{
	Use:   "myapp",
	Short: "A simple command line application",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func init() {
	rootCmd.Flags().Bool("json", false, "Output in JSON format")
	rootCmd.Flags().Bool("yaml", false, "Output in YAML format")
	rootCmd.MarkFlagsMutuallyExclusive("json", "yaml")
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go --json --yaml
#Error: if any flags in the group [json yaml] are set none of the others can be; [json yaml] were all set
#...

参数验证

我们可以对传入的参数进行验证。

内置验证

在下面这个示例中,我们为 hello 命令设置了有且只有一个参数验证;为 bye 命令设置了最少有一个参数验证;为 color 命令设置了选项参数验证:

var rootCmd = &cobra.Command{
	Use: "app",
}

var cmdHello = &cobra.Command{
	Use:  "hello [name]",
	Args: cobra.ExactArgs(1),

	Run: func(cmd *cobra.Command, args []string) {
	},
}

var cmdBye = &cobra.Command{
	Use:  "bye [name]...",
	Args: cobra.MinimumNArgs(1),
	
	Run: func(cmd *cobra.Command, args []string) {
	},
}

var cmdColor = &cobra.Command{
	Use:       "color [color]",
	Args:      cobra.OnlyValidArgs,
	ValidArgs: []string{"red", "green", "blue"},
	
	Run: func(cmd *cobra.Command, args []string) {
	},
}

func init() {
	rootCmd.AddCommand(cmdHello)
	rootCmd.AddCommand(cmdBye)
	rootCmd.AddCommand(cmdColor)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go hello Alice Bob
#Error: accepts 1 arg(s), received 2
#...

go run main.go bye
#Error: requires at least 1 arg(s), only received 0
#...

go run main.go color yellow
#Error: invalid argument "yellow" for "app color"
#...
自定义验证

在下面这个示例中,我们为主目录自定义了参数验证函数,要求传入参数必须为整数,且为2个:

var rootCmd = &cobra.Command{
	Use:  "app",
	Args: validateNumbers,

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func validateNumbers(cmd *cobra.Command, args []string) error {
	if len(args) != 2 {
		return fmt.Errorf("requires two arguments")
	}

	for _, arg := range args {
		if _, err := strconv.Atoi(arg); err != nil {
			return fmt.Errorf("invalid number: %s", arg)
		}
	}

	return nil
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go 1 2 3
#Error: requires two arguments

go run main.go 1 a
#Error: invalid number: a

帮助命令

当我们有子命令时,Cobra 会自动向我们的应用程序添加帮助命令和帮助标志。如:

var rootCmd = &cobra.Command{
	Use: "app",
}

var cmdHello = &cobra.Command{
	Use: "hello",
}

func init() {
	rootCmd.AddCommand(cmdHello)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go help
#Usage:
#  app [command]
#
#Available Commands:
#  completion  Generate the autocompletion script for the specified shell
#  help        Help about any command
#
#Flags:
#  -h, --help   help for app
#
#Additional help topics:
#  app hello
#
#Use "app [command] --help" for more information about a command.

go run main.go --help
#Usage:
#  app [command]
#
#Available Commands:
#  completion  Generate the autocompletion script for the specified shell
#  help        Help about any command
#
#Flags:
#  -h, --help   help for app
#
#Additional help topics:
#  app hello
#
#Use "app [command] --help" for more information about a command.
自定义帮助信息

我们可以用 setHelpCommand 函数自定义帮助信息,如:

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "An example app with SetHelpCommand",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

var cmdFoo = &cobra.Command{
	Use:   "foo",
	Short: "A subcommand that prints foo",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

var helpCmd = &cobra.Command{
	Use:   "help [command]",
	Short: "Help about any command",

	Run: func(cmd *cobra.Command, args []string) {
		if len(args) == 0 {
			fmt.Println("This is an example app with SetHelpFunc.")
			fmt.Println("Usage:")
			fmt.Println("  app [command]")
			fmt.Println()
			fmt.Println("Available Commands:")
			fmt.Println("  foo    A subcommand that prints foo")
			fmt.Println()
		} else {
			sub, _, err := cmd.Root().Find(args)
			if err != nil {
				fmt.Printf("Unknown help topic %q\n", args[0])
				return
			}
			_ = sub.Help()
		}
	},
}

func init() {
	rootCmd.AddCommand(cmdFoo)
	rootCmd.SetHelpCommand(helpCmd)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go help
#This is an example app with SetHelpFunc.
#Usage:
#  app [command]
#
#Available Commands:
#  foo    A subcommand that prints foo

go run main.go help foo
#A subcommand that prints foo
#
#Usage:
#  app foo

我们还可以使用 SetHelpFunc 函数自定义帮助信息,如:

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "An example app with SetHelpFunc",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

var cmdFoo = &cobra.Command{
	Use:   "foo",
	Short: "A subcommand that prints foo",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func customHelp(cmd *cobra.Command, args []string) {
	if len(args) == 0 {
		fmt.Println("This is an example app with SetHelpFunc.")
		fmt.Println("Usage:")
		fmt.Println("  app [command]")
		fmt.Println()
		fmt.Println("Available Commands:")
		fmt.Println("  foo    A subcommand that prints foo")
		fmt.Println()
	} else {
		sub, _, err := cmd.Root().Find(args)
		if err != nil {
			fmt.Printf("Unknown help topic %q\n", args[0])
			return
		}
		_ = sub.Help()
	}
}

func init() {
	rootCmd.SetHelpFunc(customHelp)
	rootCmd.AddCommand(cmdFoo)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go help
#This is an example app with SetHelpFunc.
#Usage:
#  app [command]
#
#Available Commands:
#  foo    A subcommand that prints foo

go run main.go help foo
#This is an example app with SetHelpFunc.
#Usage:
#  app [command]

我们还可以使用 SetHelpTemplate 自定义帮助信息,如:

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "An example app with SetHelpTemplate",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

var cmdFoo = &cobra.Command{
	Use:   "foo",
	Short: "A subcommand that prints foo",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

const customTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}}{{end}}
Usage:
  {{.UseLine}}
{{if .HasAvailableSubCommands}}
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
  {{.Name}}    {{.Short}}{{end}}{{end}}{{end}}
`

func init() {
	rootCmd.SetHelpTemplate(customTemplate)
	rootCmd.AddCommand(cmdFoo)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go help 
#An example app with SetHelpTemplate
#Usage:
#  app [flags]
#
#Available Commands:
#  completion    Generate the autocompletion script for the specified shell
#  foo    A subcommand that prints foo
#  help    Help about any command

go run main.go help foo
#A subcommand that prints foo
#Usage:
#  app foo [flags]

用法信息

当我们提供无效标志时,Cobra 会向我们显示用法信息,如:

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "An example app with SetUsageFunc",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go --name
#Error: unknown flag: --name
#Usage:
#  app [flags]
#
#Flags:
#  -h, --help   help for app
自定义用法信息

我们可以使用 SetUsageFunc 函数自定义用法信息,如:

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "An example app with SetUsageFunc",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func customUsage(cmd *cobra.Command) error {
	_, _ = fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS]\n", cmd.Name())
	_, _ = fmt.Fprintln(os.Stderr, "My app does something useful.")
	_, _ = fmt.Fprintln(os.Stderr, "")
	_, _ = fmt.Fprintln(os.Stderr, "Options:")
	cmd.Flags().VisitAll(func(flag *pflag.Flag) {
		_, _ = fmt.Fprintf(os.Stderr, "  --%s=%s\n", flag.Name, flag.Usage)
	})
	return nil
}

func init() {
	rootCmd.SetUsageFunc(customUsage)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go --name
#Error: unknown flag: --name
#Usage: app [OPTIONS]
#My app does something useful.
#
#Options:
#  --help=help for app

我们还可以使用 SetUsageTemplate 函数自定义用法信息,如:

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "An example app with SetUsageTemplate",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

const customTemplate = `Usage: {{.UseLine}}
My app does something useful.

Options:
  {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}
`

func init() {
	rootCmd.SetUsageTemplate(customTemplate)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go --name
#Error: unknown flag: --name
#Usage: app [flags]
#My app does something useful.
#
#Options:
#    -h, --help   help for app

版本标志

我们可以在根命令上设置版本字段,Cobra 会自动添加 version 标志,此标志的作用是输出应用的版本信息,如:

var rootCmd = &cobra.Command{
	Use:     "app",
	Short:   "An example app with Version and SetVersionTemplate",
	Version: "1.0.0",
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go --version
#app version 1.0.0
自定义版本信息

我们可以使用 cmd.SetVersionTemplate(s string) 函数自定义版本信息模板,如:

var rootCmd = &cobra.Command{
	Use:     "app",
	Short:   "An example app with Version and SetVersionTemplate",
	Version: "1.0.0",
}

const customTemplate = `This is {{.Name}}, an example app with Version and SetVersionTemplate.
The current version is {{.Version}}.
`

func init() {
	rootCmd.SetVersionTemplate(customTemplate)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go --version
#This is app, an example app with Version and SetVersionTemplate.
#The current version is 1.0.0.

钩子函数

cobra 为我们提供了钩子函数,它们的执行顺序如下:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

注意,若子命令没有自己的钩子函数,将会继承父命令的钩子函数

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)
	},
}

func init() {
	rootCmd.AddCommand(subCmd)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go
#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: []

go run main.go sub
#Inside rootCmd PersistentPreRun with args: []
#Inside subCmd PreRun with args: []
#Inside subCmd Run with args: []
#Inside subCmd PostRun with args: []
#Inside subCmd PersistentPostRun with args: []

出现 “unknown command” 时的建议

当发生 “unknown command” 错误时,Cobra 将打印建议,建议是根据注册的子命令自动生成的,如:

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "An example app with DisableSuggestions",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

var cmdFoo = &cobra.Command{
	Use:   "foo",
	Short: "A subcommand that prints foo",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func init() {
	rootCmd.AddCommand(cmdFoo)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go f
#Error: unknown command "f" for "app"
#
#Did you mean this?
#        foo
#
#Run 'app --help' for usage.
禁用建议

我们可以使用 command.DisableSuggestions = true 以禁用子命令建议,如:

var rootCmd = &cobra.Command{
	Use:                "app",
	Short:              "An example app with DisableSuggestions",
	DisableSuggestions: true,

	Run: func(cmd *cobra.Command, args []string) {
	},
}

var cmdFoo = &cobra.Command{
	Use:   "foo",
	Short: "A subcommand that prints foo",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func init() {
	rootCmd.AddCommand(cmdFoo)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go f
#Error: unknown command "f" for "app"
#Run 'app --help' for usage.

我们还可以使用 SuggestFor 属性显式设置建议给定命令的名称,如:

var rootCmd = &cobra.Command{
	Use:   "app",
	Short: "An example app with SuggestFor",

	Run: func(cmd *cobra.Command, args []string) {
	},
}

var cmdBar = &cobra.Command{
	Use:        "bar",
	Short:      "A subcommand that prints bar",
	SuggestFor: []string{"qux"},

	Run: func(cmd *cobra.Command, args []string) {
	},
}

func init() {
	rootCmd.AddCommand(cmdBar)
}

func main() {
	_ = rootCmd.Execute()
}
go run main.go qux
#Error: unknown command "qux" for "app"
#
#Did you mean this?
#        bar
#
#Run 'app --help' for usage.

为命令生成文档

我们可以根据子命令、标志等生成以下格式的文档:

  • Markdown,如:

    var rootCmd = &cobra.Command{
    	Use:   "app",
    	Short: "An example app with documentation",
    
    	Run: func(cmd *cobra.Command, args []string) {
    	},
    }
    
    func main() {
    	err := doc.GenMarkdownTree(rootCmd, "./docs")
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    
  • ReStructured Text,如:

    var rootCmd = &cobra.Command{
    	Use:   "app",
    	Short: "An example app with documentation",
    }
    
    func main() {
    	err := doc.GenReSTTree(rootCmd, "./docs")
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    
  • Man Page,如:

    var rootCmd = &cobra.Command{
    	Use:   "app",
    	Short: "An example app with documentation",
    }
    
    func main() {
    	header := &doc.GenManHeader{
    		Title:   "APP",
    		Section: "1",
    		Date:    &time.Time{},
    		Source:  "Cobra Example App",
    		Manual:  "Cobra Example Manual",
    	}
    
    	err := doc.GenManTree(rootCmd, header, "./docs")
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    

本文由 Sue211213 原创,发表于 2023 年 8 月 13 日,转载请注明出处和作者,谢谢!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值