本文主要介绍 Steve Francia(spf13) 大神写的用于快速构建命令行程序的 golang 包 cobra, 基于 cobra 写命令行的著名项目一只手数不过来: Docker CLI、Helm、istio、etcd、Git、Github CLI …
cobra 包提供以下功能:
cobra 遵循 commands
, arguments
& flags
结构。
举例来说
#appname command arguments
docker pull alpine:latest
#appname command flag
docker ps -a
#appname command flag argument
git commit -m "msg"
开发者可根据情况进行自组织。
安装 cobra 包和二进制工具 cobra-cli, cobra-cli 可以帮助我们快速创建出一个 cobra 基础代码结构。
go get -u github.com/spf13/cobra@latest
go install github.com/spf13/cobra-cli@latest
启用 GO111MODULE=on
, 我们初始化一个 xpower
$ go mod init xpower
go: creating new go.mod: module xpower
使用 cobra-cli 初始化基础代码结构
$ cobra-cli init
Your Cobra application is ready at /root/demo/xpower
#查看目录结构
$ tree xpower
xpower
├── cmd
│ └── root.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go
1 directory, 5 files
运行 demo 可以看到 cobra 包本身的一些提示信息。
$ go run main.go
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.
查看 main.go
, cobra-cli 为我们创建了一个 cmd
的包并且调用了包里面的 Execute()
函数
/*
Copyright © 2022 NAME HERE
*/
package main
import "xpower/cmd"
func main() {
cmd.Execute()
}
从上面的目录结构中可以看到 cmd
包目前只有一个 root.go
, 我们可以在这里操作根命令相关的内容。
大多数时候 CLI 可能会包含多个子命令比如 git clone
、git add
, cobra-cli 可通过 add
添加子命令。
现在我们添加 wget
和 ping
子命令, 即接下来我们将通过 xpower 来重写 wget
和 ping
的部分功能。
cobra-cli add wget
cobra-cli add ping
现在的目录结构如下:
# tree xpower
xpower
├── cmd
│ ├── ping.go
│ ├── root.go
│ └── wget.go
├── go.mod
├── go.sum
├── LICENSE
└── main.go
ping
和 wget
已经被集成到 root.go
中
$ go run main.go
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.
Usage:
xpower [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
ping A brief description of your command
wget A brief description of your command
Flags:
-h, --help help for xpower
-t, --toggle Help message for toggle
Use "xpower [command] --help" for more information about a command.
cobra 建立在命令、参数、标志的结构之上
commands 代表动作, args 是事物, flags 是动作的修饰符
最好的应用程序在使用时读起来就像句子, 因此, 用户直观地知道如何与它们交互
模式如下: APPNAME VERB NOUN --ADJECTIVE. or APPNAME COMMAND ARG --FLAG
(APPNAME 动词 名词 形容词 或者 APPNAME 命令 参数 标志
)
一些真实世界的好例子可以更好地说明这一点
kubectl
命令更能体现 APPNAME 动词 名词 形容词
kubectl get pods -owide
如下的例子, server
是 command, port
是 flag
hugo server --port=1313
这个命令中, 我们告诉 git 克隆 url
git clone URL --bare
命令是应用程序的中心点, 应用程序支持的每一个交互都包含在一个命令中, 命令可以有子命令, 也可以运行操作
在上面的例子中, server
是命令
更多关于 cobra.Command
flag 是一种修改命令行为的方式, cobra 支持完全兼容 POSIX 标志, 也支持 go flag package, cobra 可以定义到子命令上的标志, 也可以仅对该命令可用的标志
在上面的命令中, port
是标志
标志的功能由 pflag library 提供, pflag library 是 flag 标准库的一个分支, 在添加 POSIX 兼容性的同时维护相同的接口。
使用 cobra 很简单, 首先, 使用 go get 按照最新版本的库, 这个命令会安装 cobra 可执行程序以及库和依赖项
go get -u github.com/spf13/cobra
下一步, 引入 cobra 到应用程序中
import "github.com/spf13/cobra"
虽然欢迎您提供自己的组织, 但通常基于 Cobra 的应用程序将遵循以下组织结构:
▾ appName/
▾ cmd/
add.go
your.go
commands.go
here.go
main.go
在 Cobra 应用程序中, main.go
文件通常非常简单。它有一个目的: 初始化 Cobra。
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
cobra 提供了程序用来创建你的应用程序然后添加你想添加的命令, 这是将 cobra 引入应用程序最简单的方式
这儿你可以发现关于 cobra 的更多信息
要手动实现 cobra, 需要创建一个 main.go
和 rootCmd
文件, 可以根据需要提供其他命令
Cobra 不需要任何特殊的构造器。只需创建命令。
理想情况下, 您可以将其放在 app/cmd/root.go
中:
var rootCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
love by spf13 and friends in Go.
Complete documentation is available at http://hugo.spf13.com`,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
在 init()
函数中定义标志和处理配置
例子如下, cmd/root.go
:
package cmd
import (
"fmt"
"os"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
// Used for flags.
cfgFile string
userLicense string
rootCmd = &cobra.Command{
Use: "cobra",
Short: "A generator for Cobra based Applications",
Long: `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.`,
}
)
// Execute executes the root command.
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution")
rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project")
rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
viper.SetDefault("author", "NAME HERE " )
viper.SetDefault("license", "apache")
rootCmd.AddCommand(addCmd)
rootCmd.AddCommand(initCmd)
}
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
cobra.CheckErr(err)
// Search config in home directory with name ".cobra" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra")
}
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
使用 root 命令, 您需要让主函数执行它。为清楚起见, Execute 应该在根目录下运行, 尽管它可以在任何命令上调用。
在 Cobra 应用程序中, main.go
文件通常非常简单。它有一个目的: 初始化 Cobra。
package main
import (
"{pathToYourApp}/cmd"
)
func main() {
cmd.Execute()
}
可以定义其他命令, 通常每个命令在 cmd/
目录中都有自己的文件。
如果要创建版本命令, 可以创建 cmd/version.go
并用以下内容填充它:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
},
}
如果希望将错误返回给命令的调用者, 可以使用 RunE
。
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(tryCmd)
}
var tryCmd = &cobra.Command{
Use: "try",
Short: "Try and possibly fail at something",
RunE: func(cmd *cobra.Command, args []string) error {
if err := someFunc(); err != nil {
return err
}
return nil
},
}
然后可以在 execute
函数调用中捕获错误。
标志提供修饰符来控制操作命令的操作方式。
由于标志是在不同的位置定义和使用的, 因此我们需要在外部定义一个具有正确作用域的变量来分配要使用的标志。
var Verbose bool
var Source string
有两种不同的方法来分配标志。
标志可以是 “持久” 的, 这意味着该标志将可用于分配给它的命令以及该命令下的每个命令。对于全局标志, 在根上指定一个标志作为持久标志。
// v flag 将在 rootCmd 及以下的子命令上都生效
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
也可以在本地分配一个标志, 该标志只应用于该特定命令。
// 该命令只在 localCmd 上起作用
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
默认情况下, Cobra 只解析目标命令上的本地标志, 而忽略父命令上的任何本地标志。通过启用 Command.TraverseChildren
, Cobra 将在执行目标命令之前解析每个命令上的本地标志。
command := cobra.Command{
Use: "print [OPTIONS] [COMMANDS]",
TraverseChildren: true,
}
使用 viper 绑定标志
var author string
func init() {
rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}
在本例中, 持久标志 author 与 viper 绑定。注意: 当用户未提供 --author
标志时, 变量 author 将不会设置为 config 中的值。
更多关于 viper 的文档
Flags 默认是可选的, 如果希望命令在未设置标志时报告错误, 请根据需要进行标记:
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkPersistentFlagRequired("region")
可以使用命令的 Args 字段指定位置参数的验证。
内置了以下验证器:
NoArgs
: 如果存在任何位置参数, 该命令将报告错误。ArbitraryArgs
: 该命令将接受任何 args。OnlyValidArgs
: 如果有任何位置参数不在命令的 ValidArgs 字段中, 则该命令将报告错误。MinimumNArgs(int)
: 如果没有至少 N 个位置参数, 则该命令将报告错误。MaximumNArgs(int)
: 如果位置参数超过 N 个, 则该命令将报告错误。ExactArgs(int)
: 如果没有正好 N 个位置参数, 则命令将报告错误。ExactValidArgs(int)
: 如果没有正好 N 个位置参数, 或者如果有任何位置参数不在命令的 ValidArgs 字段中, 则该命令将报告错误RangeArgs(min, max)
: 如果 args 的数目不在预期的最小和最大 args 数目之间, 则命令将报告错误。var cmd = &cobra.Command{
Short: "hello",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires a color argument")
}
if myapp.IsValidColor(args[0]) {
return nil
}
return fmt.Errorf("invalid color specified: %s", args[0])
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, World!")
},
}
在下面的示例中, 我们定义了三个命令。两个是顶级命令, 一个(cmdTimes)是顶级命令之一的子命令。在这种情况下, 根是不可执行的, 这意味着需要一个子命令。这是通过不为 “rootCmd” 提供 “Run” 来实现的。
我们只为一个命令定义了一个标志。
有关标志的更多文档, 请访问 https://github.com/spf13/pflag
package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
func main() {
var echoTimes int
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("Echo: " + strings.Join(args, " "))
},
}
var cmdTimes = &cobra.Command{
Use: "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, " "))
}
},
}
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
var rootCmd = &cobra.Command{Use: "app"}
rootCmd.AddCommand(cmdPrint, cmdEcho)
cmdEcho.AddCommand(cmdTimes)
rootCmd.Execute()
}
对于一个更完整的例子更大的应用程序, 请检查 Hugo。
当您有子命令时, Cobra 会自动将 help 命令添加到应用程序中。当用户运行"应用程序帮助"时, 将调用此函数。此外, help 还支持所有其他命令作为输入。例如, 您有一个名为"create"的命令, 没有任何附加配置; 调用"app help create"时, Cobra 将起作用。每个命令都会自动添加"-help"标志。
以下输出由 Cobra 自动生成。除了命令和标志定义之外, 不需要任何东西。
$ cobra help
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.
Usage:
cobra [command]
Available Commands:
add Add a command to a Cobra Application
help Help about any command
init Initialize a Cobra Application
Flags:
-a, --author string author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for cobra
-l, --license string name of license for the project
--viper use Viper for configuration (default true)
Use "cobra [command] --help" for more information about a command.
帮助就像其他命令一样。它周围没有特殊的逻辑或行为。事实上, 你可以提供你想提供的。
您可以为默认命令提供自己的帮助命令或模板, 以用于以下功能:
cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)
当用户提供无效的标志或无效的命令时, Cobra 通过向用户显示"用法"来响应。
你可以从上面的帮助中认识到这一点。这是因为默认帮助将用法作为其输出的一部分嵌入。
$ cobra --invalid
Error: unknown flag: --invalid
Usage:
cobra [command]
Available Commands:
add Add a command to a Cobra Application
help Help about any command
init Initialize a Cobra Application
Flags:
-a, --author string author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for cobra
-l, --license string name of license for the project
--viper use Viper for configuration (default true)
Use "cobra [command] --help" for more information about a command.
您可以提供自己的使用函数或模板供 Cobra 使用。与帮助一样, 函数和模板也可以通过公共方法重写:
cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)
如果在 root 命令上设置了 version
字段, Cobra 会添加一个顶级的 “–version” 标志。运行带有 “-version” 标志的应用程序将使用版本模板将版本打印到标准输出。可以使用 cmd.SetVersionTemplate(s string)
函数自定义模板。
可以在命令的主运行函数之前或之后运行函数。PersistentPreRun
和 PreRun
函数将在运行之前执行。PersistentPostRun
和 PostRun
将在运行后执行。如果子函数不声明自己的函数, 则它们将继承 Persistent*Run
函数。这些函数按以下顺序运行:
下面是使用所有这些特性的两个命令的示例。执行子命令时, 它将运行根命令的 PersistentPreRun
, 而不是根命令的 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]
当发生"未知命令"错误时, Cobra 将打印自动建议。这使得 Cobra 在发生拼写错误时的行为类似于 git 命令。例如:
$ hugo srever
Error: unknown command "srever" for "hugo"
Did you mean this?
server
Run 'hugo --help' for usage.
基于注册的每个子命令和 Levenshtein 距离的实现, 建议是自动的。匹配最小距离 2(忽略大小写)的每个已注册命令都将显示为建议。
如果需要在命令中禁用建议或调整字符串距离, 请使用:
command.DisableSuggestions = true
or
command.SuggestionsMinimumDistance = 1
您还可以使用 SuggestFor 属性显式设置将为其建议给定命令的名称。这允许对在字符串距离方面不接近的字符串提供建议, 但在您的一组命令中是有意义的, 并且对于某些您不需要别名的字符串。例子:
$ kubectl remove
Error: unknown command "remove" for "kubectl"
Did you mean this?
delete
Run 'kubectl help' for usage.
Cobra 可以基于子命令、标志等生成文档。请在 docs generation 文档 中阅读更多关于它的信息。
Cobra 可以为以下 shell 生成 shell 完成文件: bash、zsh、fish、PowerShell。如果您在命令中添加更多信息, 这些补全功能将非常强大和灵活。在 Shell Completions 中阅读更多关于它的信息。