Go 通过 cobra 快速构建命令行应用

这里填写标题

  • 1. Go 通过 cobra 快速构建命令行应用
    • 1.1. cobra 能帮我们做啥?
    • 1.2. cobra 基础使用
    • 1.3. 概念
    • 1.4. Commands
    • 1.5. Flags
    • 1.6. Installing
    • 1.7. 入门
    • 1.8. 使用 cobra 生成器
    • 1.9. 使用 cobra 库
    • 1.10. 创建 rootCmd
    • 1.11. 创建 main.go
    • 1.12. 创建其他命令
    • 1.13. 返回和处理错误
    • 1.14. 使用 Flags
      • 1.14.1. 为命令指定标志
      • 1.14.2. 持久标志
      • 1.14.3. 本地标志
      • 1.14.4. 父命令上的本地标志
      • 1.14.5. 在配置上绑定标志
      • 1.14.6. 必须的 flags
      • 1.14.7. 持久性 Flags
    • 1.15. 位置参数和自定义参数
    • 1.16. 设置自定义验证程序的示例:
    • 1.17. 例子
    • 1.18. 帮助命令
      • 1.18.1. 例子
      • 1.18.2. 自定义帮助
    • 1.19. 使用消息
      • 1.19.1. 例子
      • 1.19.2. 自定义用法
    • 1.20. 标志版本
    • 1.21. 运行前运行后钩子
    • 1.22. 发生"未知命令"时的建议
    • 1.23. 为您的命令生成文档
    • 1.24. shell 补全

1. Go 通过 cobra 快速构建命令行应用

本文主要介绍 Steve Francia(spf13) 大神写的用于快速构建命令行程序的 golang 包 cobra, 基于 cobra 写命令行的著名项目一只手数不过来: Docker CLI、Helm、istio、etcd、Git、Github CLI …

1.1. cobra 能帮我们做啥?

cobra 包提供以下功能:

  • 轻松创建基于子命令的 CLI: 如 app server、app fetch 等。
  • 自动添加-h,–help 等帮助性 Flag
  • 自动生成命令和 Flag 的帮助信息
  • 创建完全符合 POSIX 的 Flag(标志)(包括长、短版本)
  • 支持嵌套子命令
  • 支持全局、本地和级联 Flag
  • 智能建议 (app srver… did you mean app server?)
  • 为应用程序自动生成 shell 自动完成功能 (bash、zsh、fish、powershell)
  • 为应用程序自动生成 man page
  • 命令别名, 可以在不破坏原有名称的情况下进行更改
  • 支持灵活自定义 help、usege 等。
  • 无缝集成 viper 构建 12-factor 应用

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"

开发者可根据情况进行自组织。

1.2. cobra 基础使用

安装 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 clonegit add, cobra-cli 可通过 add 添加子命令。

现在我们添加 wgetping 子命令, 即接下来我们将通过 xpower 来重写 wgetping 的部分功能。

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

pingwget 已经被集成到 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.

1.3. 概念

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

1.4. Commands

命令是应用程序的中心点, 应用程序支持的每一个交互都包含在一个命令中, 命令可以有子命令, 也可以运行操作
在上面的例子中, server 是命令
更多关于 cobra.Command

1.5. Flags

flag 是一种修改命令行为的方式, cobra 支持完全兼容 POSIX 标志, 也支持 go flag package, cobra 可以定义到子命令上的标志, 也可以仅对该命令可用的标志
在上面的命令中, port 是标志
标志的功能由 pflag library 提供, pflag library 是 flag 标准库的一个分支, 在添加 POSIX 兼容性的同时维护相同的接口。

1.6. Installing

使用 cobra 很简单, 首先, 使用 go get 按照最新版本的库, 这个命令会安装 cobra 可执行程序以及库和依赖项

go get -u github.com/spf13/cobra

下一步, 引入 cobra 到应用程序中

import "github.com/spf13/cobra"

1.7. 入门

虽然欢迎您提供自己的组织, 但通常基于 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()
}

1.8. 使用 cobra 生成器

cobra 提供了程序用来创建你的应用程序然后添加你想添加的命令, 这是将 cobra 引入应用程序最简单的方式
这儿你可以发现关于 cobra 的更多信息

1.9. 使用 cobra 库

要手动实现 cobra, 需要创建一个 main.gorootCmd 文件, 可以根据需要提供其他命令

1.10. 创建 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())
    }
}

1.11. 创建 main.go

使用 root 命令, 您需要让主函数执行它。为清楚起见, Execute 应该在根目录下运行, 尽管它可以在任何命令上调用。
在 Cobra 应用程序中, main.go 文件通常非常简单。它有一个目的: 初始化 Cobra。

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

1.12. 创建其他命令

可以定义其他命令, 通常每个命令在 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")
  },
}

1.13. 返回和处理错误

如果希望将错误返回给命令的调用者, 可以使用 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 函数调用中捕获错误。

1.14. 使用 Flags

标志提供修饰符来控制操作命令的操作方式。

1.14.1. 为命令指定标志

由于标志是在不同的位置定义和使用的, 因此我们需要在外部定义一个具有正确作用域的变量来分配要使用的标志。

var Verbose bool
var Source string

有两种不同的方法来分配标志。

1.14.2. 持久标志

标志可以是 “持久” 的, 这意味着该标志将可用于分配给它的命令以及该命令下的每个命令。对于全局标志, 在根上指定一个标志作为持久标志。

// v flag 将在 rootCmd 及以下的子命令上都生效
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")

1.14.3. 本地标志

也可以在本地分配一个标志, 该标志只应用于该特定命令。

// 该命令只在 localCmd 上起作用
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

1.14.4. 父命令上的本地标志

默认情况下, Cobra 只解析目标命令上的本地标志, 而忽略父命令上的任何本地标志。通过启用 Command.TraverseChildren, Cobra 将在执行目标命令之前解析每个命令上的本地标志。

command := cobra.Command{
  Use: "print [OPTIONS] [COMMANDS]",
  TraverseChildren: true,
}

1.14.5. 在配置上绑定标志

使用 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 的文档

1.14.6. 必须的 flags

Flags 默认是可选的, 如果希望命令在未设置标志时报告错误, 请根据需要进行标记:

rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")

1.14.7. 持久性 Flags

rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkPersistentFlagRequired("region")

1.15. 位置参数和自定义参数

可以使用命令的 Args 字段指定位置参数的验证。
内置了以下验证器:

  • NoArgs: 如果存在任何位置参数, 该命令将报告错误。
  • ArbitraryArgs: 该命令将接受任何 args。
  • OnlyValidArgs: 如果有任何位置参数不在命令的 ValidArgs 字段中, 则该命令将报告错误。
  • MinimumNArgs(int): 如果没有至少 N 个位置参数, 则该命令将报告错误。
  • MaximumNArgs(int): 如果位置参数超过 N 个, 则该命令将报告错误。
  • ExactArgs(int): 如果没有正好 N 个位置参数, 则命令将报告错误。
  • ExactValidArgs(int): 如果没有正好 N 个位置参数, 或者如果有任何位置参数不在命令的 ValidArgs 字段中, 则该命令将报告错误
  • RangeArgs(min, max): 如果 args 的数目不在预期的最小和最大 args 数目之间, 则命令将报告错误。

1.16. 设置自定义验证程序的示例:

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

1.17. 例子

在下面的示例中, 我们定义了三个命令。两个是顶级命令, 一个(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。

1.18. 帮助命令

当您有子命令时, Cobra 会自动将 help 命令添加到应用程序中。当用户运行"应用程序帮助"时, 将调用此函数。此外, help 还支持所有其他命令作为输入。例如, 您有一个名为"create"的命令, 没有任何附加配置; 调用"app help create"时, Cobra 将起作用。每个命令都会自动添加"-help"标志。

1.18.1. 例子

以下输出由 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.

帮助就像其他命令一样。它周围没有特殊的逻辑或行为。事实上, 你可以提供你想提供的。

1.18.2. 自定义帮助

您可以为默认命令提供自己的帮助命令或模板, 以用于以下功能:

cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)

1.19. 使用消息

当用户提供无效的标志或无效的命令时, Cobra 通过向用户显示"用法"来响应。

1.19.1. 例子

你可以从上面的帮助中认识到这一点。这是因为默认帮助将用法作为其输出的一部分嵌入。

$ 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.

1.19.2. 自定义用法

您可以提供自己的使用函数或模板供 Cobra 使用。与帮助一样, 函数和模板也可以通过公共方法重写:

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

1.20. 标志版本

如果在 root 命令上设置了 version 字段, Cobra 会添加一个顶级的 “–version” 标志。运行带有 “-version” 标志的应用程序将使用版本模板将版本打印到标准输出。可以使用 cmd.SetVersionTemplate(s string) 函数自定义模板。

1.21. 运行前运行后钩子

可以在命令的主运行函数之前或之后运行函数。PersistentPreRunPreRun 函数将在运行之前执行。PersistentPostRunPostRun 将在运行后执行。如果子函数不声明自己的函数, 则它们将继承 Persistent*Run 函数。这些函数按以下顺序运行:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

下面是使用所有这些特性的两个命令的示例。执行子命令时, 它将运行根命令的 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]

1.22. 发生"未知命令"时的建议

当发生"未知命令"错误时, 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.

1.23. 为您的命令生成文档

Cobra 可以基于子命令、标志等生成文档。请在 docs generation 文档 中阅读更多关于它的信息。

1.24. shell 补全

Cobra 可以为以下 shell 生成 shell 完成文件: bash、zsh、fish、PowerShell。如果您在命令中添加更多信息, 这些补全功能将非常强大和灵活。在 Shell Completions 中阅读更多关于它的信息。

你可能感兴趣的:(#,package,golang,cobra)