美文网首页应用系统
基于Cobra的golang命令行工具开发

基于Cobra的golang命令行工具开发

作者: 霹雳楊 | 来源:发表于2019-11-29 11:54 被阅读0次

    基于golang语言开发命令行工具(command line interfaces,CLI)最常用的框架是Cobra。通过Cobra可以使用简单的接口实现一个强大现代的CLI工具,许多知名的项目比如Docker、Kubernetes等都用Cobra来开发自己的命令行工具。下面我们将对Cobra的基本用法做简要介绍。

    • 概览
    • 相关概念
      • Commands(命令)
      • Flags(标志)
    • 安装
    • 开始使用
      • 使用Cobra生成器
      • 使用Cobra库
      • 使用Flags
      • 位置和自定义参数
      • 示例
      • Help Command
      • 展示Usage
      • 预处理和后处理等Hooks函数
      • 处理 unknown command 的建议
      • 生成命令行文档
      • 实现命令自动补全

    概览


    Cobra提供简单的接口来创建强大的现代化CLI接口,比如git与go。Cobra同时也提供一个二进制工具,用于创建命令行程序。

    Cobra提供:

    • 简单的子命令模式: 如app serverapp fetch
    • flags兼容posix模式(包括长、短版本)
    • 支持子命令嵌套
    • 支持全局、局部以及继承falgs
    • 智能提示,如app srver,将提示srver子命令不存在,是否为app server
    • 自动生成子命令及其flags
    • 自动生成-h--help等flags提醒
    • 自动生成命令行docs和man文件
    • 支持命令行别名
    • 灵活定义help和usage信息
    • 可与viper库结合使用,方便参数、配置和环境变量的管理

    相关概念


    Cobra的构建基于结构化的commands,arguments和flags,即命令、参数和标志。

    Commands代表命令行程序执行什么命令,Args(即arguments)和Flags即这些命令的修饰符。

    最好的命令行程序应该像读句子那样去使用,用户通过命令行本身就可以自然地知道这段命令执行的是什么操作。

    一个典型的命令行设计模式类似APPNAME COMMAND ARG --FLAGAPPNAME VERB NOUN --ADJECTIVE

    例如下面的例子,‘server’是一个操作, ‘port’是一个标志:

    hugo server --port=1313

    下面的另一个例子告诉我们,它要通过Git来执行clone操作拷贝url对应的项目到本地的裸仓库:

    git clone URL --bare

    Commands(命令)


    Command是一个命令行程序最重要的概念。命令行程序支持的每一个交互操作都应该被包含在一个command中。一条command可以有多条子commands并且能够可选地执行它们。

    在上面的例子中, ‘server’就是一条command。

    点击查看更多有关Cobra Command的信息

    Flags(标志)


    一个Flag用来控制command的行为。Cobra完全兼容POSIX的flags模式,如同Go自带的标准flag库那样。一条Cobra生成的command可以将它的flags继承给它的子commadn,也可以限定这些flags只能被该command使用。

    在上面的例子中,‘port’即一个flag。

    更加强大的flag功能可以使用pflag库,它是一个标准flag库的扩展。

    安装


    使用Cobra十分简单。首先通过go get安装最新版本的代码库及相关依赖,这条命令同时也会安装cobra可执行程序:

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

    接下来,在golang代码中引用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生成器的使用,关于该工具的详细信息点击此处

    使用Cobra库


    使用Cobra,需要创建一个main.go文件和一个rootCmd文件。当然,你也可以选择别的地方去添加额外的commands。

    创建rootCmd

    Cobra没有构造函数,所以直接简单地创建一个command对象就行。

    这里假设下面的代码位于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.Println(err)
        os.Exit(1)
      }
    }
    

    你也可以在init()函数中定义和处理flags和配置。

    例如 cmd/root.go:

    package cmd
    
    import (
        "fmt"
    
        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 <EMAIL ADDRESS>")
        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()
            if err != nil {
                er(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())
        }
    }
    

    创建main.go

    在cmd/root.go文件中我们已经定义了一个名为rootCmd的command作为根,为了执行该command,还需要将其放入main.go文件中执行。

    在一个Cobra程序中,main.go文件通常只干一件事,即初始化Cobra并执行command。

    package main
    
    import (
      "{pathToYourApp}/cmd"
    )
    
    func main() {
      cmd.Execute()
    }
    

    添加其他command

    不同的command通常分别在不同的go文件中定义,这里假设所有command的实现都处于cmd/目录下不同的文件中。
    例如,如果你想创建名为version的command,可以创建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")
      },
    }
    

    使用Flags


    Flags提供控制command行为的能力。

    给command绑定flags

    一般情况下,我们需要预先定义变量来存储flags的值,这样便于我们各处使用它们(Cobra也支持不显式地定义和使用flags,但这种情况我们后面再说)。

    如下,我们定义了Verbose和Source两个不同类型的变量来表示和存储flags值。

    var Verbose bool
    var Source string
    

    Cobra有两种不同的方式给command绑定flags,分别为Persistent Flags和Local Flags。

    Persistent Flags( 持久型flags)

    Persistent Flags表示flag不仅绑定在一个command上,同时也绑定在了这个command的子command上。Persistent Flags可以被一个command下的所有子command使用。

    例如,我们给根command(即rootCmd)绑定了一个名为‘verbose’的Persistent Flag,那么这个flag就成了一个全局flag,可以被所有command使用(因为rootCommand为根command,显然后续所有增加的command都为子command)。

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

    Local Flags(本地flags)

    Local Flags表示flag仅能被其绑定的command使用。

    如下我们将名为'source'的flag绑定给一个名为localCmd的command,除了localCmd外,其他command无法接收到这个flag的值,即只有loalCmd能给‘Source’变量通过指定flag来赋值。

    localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
    

    command定义的Local Flags只能由绑定了这些flags的command使用,但这里还有一种方便的方式,可以使得一个command所有的子command定的flags都绑定给他们的父command,只要在创建command时,指定TraverseChildren为true即可,这样父command在真正执行前会遍历其所有的子command来绑定flags。

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

    给flags绑定配置

    在使用命令行程序时不总会显式地提供flags的值,有时希望程序自己去环境变量、配置文件等地方自动寻找配置参数。这时我们可以通过viper库来实现这一功能,将flags的值绑定给viper。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这个flag绑定给了viper,viper一般会按照flag、环境变量、配置文件和默认值的优先级去寻找环境变量,当用户没有通过--author给出flag值时,viper会降低优先级去别处寻找匹配的参数值。

    Required flags

    默认情况下,是否指定flags是可选的,如果你希望当一个flag没有设置时,命令行报错,你可以标记它为必须的

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

    位置和自定义参数


    验证和限制命令行程序的位置参数可以通过CommandArgs字段来实现。

    Cobra内置有下列验证函数给Args字段使用:

    • NoArgs - 不允许有位置参数
    • ArbitraryArgs - 可以接受任意多个位置参数
    • OnlyValidArgs - 只允许指定CommandValidArgs字段里指定的位置参数
    • MinumumNArgs(int) - 限定最少提供多少个位置参数
    • MaximumNArgs(int) - 限定最多能提供多少个位置参数
    • ExactArgs(int) - 限定必须提供多少个对应的位置参数
    • ExcatValidArgs(int) - 限定必须提供多少个对应的位置参数,并且位置参数必须位于ValidArgs字段
    • RangeArgs(min, max) - 限定位置参数的个数必须处于某一个区间内

    以下为一个限制位置参数的例子:

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

    示例


    目前为止我们已经介绍了很多Cobra的基本用法,本节会给出一个完整的例子来回顾前面讲过的知识。

    在下面的例子中,我们定义了3个commands。两个commands为顶级命令,一个command为顶级命令的子命令。在这个例子中,由于rootCmd没有为Run字段提供方法,所以单独的root是不能运行的,必须要有子commands。

    注意这里我们只为一个名为echoTimes的command 设置了flag。更多flags的用法参考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

    Help Command


    当你的程序有子命令时,Cobra 会自动给你程序添加help命令。当你运行app help,会调用help命令。另外,help同样支持其它输入命令。例如,你有一个没有任何其它配置的命令叫create,当你调用app help create Corbra 将会起作用。

    例子

    下面的输出是Cobra自动生成的,除了command和flag的定义,我们没有对command做任何其他定制。

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

    help 就跟其它命令一样,并没有特殊的逻辑或行为。事实上,你也可以提供你自己定义的help。

    自定义Help

    你可以使用下面的函数来定义自己的help:

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

    后两个函数定义的help会被command的子命令继承。

    展示Usage


    当用户错误的使用命令行程序时(如指定非法的flag和command),Cobra将会自动显示命令行程序的用法说明usage

    例子

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

    自定义用法说明

    你能提供你自己的usage函数或模板给 Cobra 使用。
    类似于自定义help,usage的方法和模板都会覆盖默认的公共说明。

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

    版本Flag


    当顶级Command的Version字段被定义后,Cobra会自动为顶级command添加一个--versionflag。当命令行程序指定该flag时,Cobra会调用内置的版本函数打印命令行程序的相关版本信息。当然,你也可以使用cmd.SetVersionTemplate(s string)函数来自定义版本信息的展示内容。

    预处理和后处理等Hooks函数


    Cobra提供了多个钩子(hooks)函数的接口,你可以很容易地去决定在command执行Run中的实际函数之前或之后,需要执行哪些方法。

    PersistentPreRunPreRun函数会在Run之前执行。

    PersistentPostRunPostRun函数将会在Run之后执行。

    Persistent*Run这种模式的函数会被子command继承。

    钩子函数的执行顺序如下:

    • PersistentPreRun

    • PreRun

    • Run

    • PostRun

    • PersistentPostRun

    如下的例子使用了上面提到的所有钩子函数。需要注意的是,当子command执行时,它会执行根command的PersistentPreRun函数而不会执行根command的PersistentPostRun函数(因为子command自己定义了该函数)。

    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]
    

    处理 unknown command 的建议


    Cobra在unknown command错误发生时,会自动打印建议。这就让Cobra的处理错误行为的方式类似git命令那样。例如:

    $ hugo srever
    Error: unknown command "srever" for "hugo"
    
    Did you mean this?
            server
    
    Run 'hugo --help' for usage.
    

    建议会基于注册的子命令自动生成。使用了Levenshtein distance的实现。每一个模糊匹配的命令间隔为2个字符。

    如果你希望在你的命令里,禁用建议或减小字符串的距离,使用:

    command.DisableSuggestions = true
    

    command.SuggestionsMinimumDistance = 1
    

    你也可以通过SuggestFor来给命令提供明确的名词建议。这个特性允许当字符串不相近,但是意思与你的命令相近时,提供指定的命令建议,比如:

    $ kubectl remove
    Error: unknown command "remove" for "kubectl"
    
    Did you mean this?
            delete
    
    Run 'kubectl help' for usage.
    

    生成命令行文档


    Cobra可以基于command、flags等来自动生成文档,支持下面几种格式:

    实现命令自动补全


    Cobra还提供了自动生成bash或zsh自动补全脚本的功能,这部分参见

    相关文章

      网友评论

        本文标题:基于Cobra的golang命令行工具开发

        本文链接:https://www.haomeiwen.com/subject/bqnwwctx.html