golang 命令行解析库cobra的使用

golang 命令行解析库cobra的使用

​ 关于go语言的命令行解析,标准库flag提供的功能比较少,不能满足我的使用需求,所以就需要寻找第三方解决方案了.我选择cobra是因为它支持 sub command子命令,满足我的使用需求,而且被很多大公司使用(比如github cli),安全性应该没什么问题.

Overview

cobra提供简单的接口用于创建类似于git或者go官方工具的命令行工具

cobra可以快速创建基于cobra的应用的脚手架.

  • 简单的基于子命令的命令行接口: app server, app fetch等等
  • 完全兼容POSIX的flag(包括短和长版本)
  • 嵌套子命令
  • 全局,本地和级联flag
  • 快速生成应用脚手架cobra init appname & cobra add cmdname
  • 智能提示功能 (app srver... did you mean app server?)
  • 自动基于command和flag生成帮助
  • 自动添加帮助flag -h, --help
  • 自动添加shell自动补全功能(bash, zsh, fish, powershell)
  • 自动生成帮助文档
  • command 别名功能
  • 灵活定制help,usage等等
  • 可选 集成viper用于构建12-factor app

Concept

cobra基于结构化的command,arguments和flag进行构建

Commands代表行动

Args表示事物

Flags 是行动的修饰符

最好的应用使用起来像读语句一样,用户会自然而然地知道如何使用这个应用

遵循的原则是

APPNAME VERB NOUN --ADJECTIVE.或者APPNAME COMMAND ARG --FLAG

在接下来的例子里, server是一个command,port是一个flag

hugo server --port=1313

下一个命令我们在告诉git,从目标url 克隆一个裸仓库(只拷贝.git子目录)

git clone URL --bare

Commands

command是应用的核心,每次应用的互动都会包含一个command,一个command可以存在可选的子command,在这个例子hugo server --port=1313里,server就是一个command

More about cobra.Command

Flags

flag是修改command行为的一种方式,cobra提供完全POSIX兼容的flag就像gp官方命令行工具一样.

cobra的command可以定义影响所有子命令的flag或者只影响一个命令的command

.在这个例子里,hugo server --port=1313 port是一个flag

flag功能由pflag库提供.

Installing

使用go get下载最新版

go get -u github.com/spf13/cobra

导入项目:

import "github.com/spf13/cobra"

Getting Started

典型的项目结构

  ▾ appName/
    ▾ cmd/
        add.go
        your.go
        commands.go
        here.go
      main.go

通常main行数比较简单,起到初始化cobra的作用

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

Using the Cobra Generator

cobra自身提供的程序可以帮助你创建app和添加command,这是最简单的添加cobra到你的应用程序的方式

Here you can find more information about it.

Using the Cobra Library

手动实现cobra,你需要创建一个main.go和rootCmd文件,你需要视情况添加额外的command

Create rootCmd

cobra不需要构造函数,简单地创建你的command即可.

通常把rootCmd放到 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函数中定义flag或者修改配置

For example 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 er(msg interface{}) {
    fmt.Println("Error:", msg)
    os.Exit(1)
}

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

Create your main.go

定义好root command后,你需要在main函数里执行execute

在cobra app中,通常main.go文件中的内容比较少,只用于初始化cobra

In a Cobra app, typically the main.go file is very bare. It serves, one purpose, to initialize Cobra.

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

Create additional commands

可以添加新的command,通常我们把每个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")
  },
}

Returning and handling errors

If you wish to return an error to the caller of a command, RunE can be used.

如果你想针对command的调用返回一个error,可以使用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函数调用的时候,error就会被捕获

Working with Flags

通过flag传入参数可以控制comand的执行行为

Assign flags to a command

由于flag定义后,可以在很多不同的地方被使用,我们需要定义一个变量来接受flag

var Verbose bool
var Source string

Persistent Flags

persistent(持久性) flag,可以用于分配给他的命令以及所有子命令.

下面例子里的verbose作用是让命令行输出的信息更详细,所有的子命令都会支持 verbos flag

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

Local Flags

local flag ,本地分配,只会分配给那个特定的command

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

Local Flag on Parent Commands

默认情况下 cobra只会在目标command的基础上解析local flag,它的parent command会忽略解析.开启Command.TraverseChildren选项后,cobra就会执行目标command之前,在每个command上解析local command

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

Bind Flags with Config

你也可以使用viper绑定flag

var author string

func init() {
  rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}

在这个persistent flag的例子中persistent flag author ,用viper进行了绑定, 注意,当用户没有使用 --author flag时,就不会从配置文件中读取值

More in viper documentation.

Required flags

Flags are optional by default. If instead you wish your command to report an error when a flag has not been set, mark it as required:

默认情况下flag时可选的,如果你希望缺失flag的时候报错,可以标记为Required flags,如下

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

或者对于PersistentFlag来说

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

Positional and Custom Arguments

位置参数的验证,可以使用commnad的 args字段

下列验证器是内置的:

  • NoArgs - 如果没有任何参数,command会报错
  • ArbitraryArgs - command接受任何参数
  • OnlyValidArgs - 如果有任何位置参数不在 validArgs的范围里面, command会报错
  • MinimumNArgs(int) - 当位置参数的个数少于n时,command 会报错
  • MaximumNArgs(int) - 当位置参数的个数大于n时,,command会报错
  • ExactArgs(int) - 当位置参数的个数不正好是n个时,command会报错
  • ExactValidArgs(int) - 当不是正好有n个位置参数或者有任何位置参数不属于ValidArgs,command会报错.
  • RangeArgs(min, max) - 如果参数的个数不是在min和max之间,command会报错

关于自定义验证器的一个例子:

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

Example

在下面的例子里,定义了3个command,其中有两个时最高级别的command,另一个(cmdTimes)时其中一个的子command.在这个例子里,root不能执行意味着需要subcommand,这是通过不给rootCmd提供Run参数来实现的.

More documentation about flags is available at https://github.com/spf13/pflag

执行go run main.go echo dasda dasdaa dadsa, 执行go run main.go print dasada dasdafesf fasfdasf

times是echo的子命令所以执行命令是这样的go run main.go echo times -t 10 dsadas dasda dasda

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

For a more complete example of a larger application, please checkout Hugo.

Help Command

当你有一个新的subcommand,cobra会自动为你的应用添加help command.当用户执行app help时,会调用默认的help command.并且help支持所有其他输入的command.每个command都会自动添加--helpflag

Example

下面的输出是由cobra自动生成的. 除了command和flag你不需要其他东西就能生成help 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就和其他command一样,没有特殊的逻辑或行为.如果你想的话也可以自定义help.

Defining your own help

你可以提供自己的和help command或者你自己的模板,只需要使用下面的函数

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

The latter two will also apply to any children commands.

后两个,还可以提供给子command

Usage Message

当用户提供了一个无效的flag或者command,cobra会回复一份usage

Example

默认的help,也嵌入了一份usage作为输出

下面是遇到无效flag,输出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.

Defining your own usage

你可以提供自己的usage函数或者模板,这样就会覆盖公用模板了.

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

Version Flag

如果你的root command上有一个version字段,cobra会添加一个--versionflag. 使用--versionflag会使用version模板打印版本到输出.模板可以通过cmd.SetVersionTemplate(s string)函数来自定义

PreRun and PostRun Hooks

在你main函数里的run函数运行之前和之后,也是可以运行函数的.PersistentPreRunPreRun 函数会在Run之前执行,PersistentPostRunPostRun会在Run之后执行. 符合Persistent*Run格式的函数如果子命令没有定义自己的Run,就会继承.这些函数的执行顺序如下:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

下面是一个两个comand运用了以上所有feature的例子.当子command执行的时候,会执行root command的PersistentPreRun函数而不是root command的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()
}

Output:

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]

Suggestions when "unknown command" happens

当unknow command错误发生时,cobra会自动打印建议.这使得cobra拥有和git命令行类似的效果

$ hugo srever
Error: unknown command "srever" for "hugo"

Did you mean this?
        server

Run 'hugo --help' for usage.

建议会基于每个子command的注册自动生成,实现基于 Levenshtein distance.每个注册的command匹配到最小两个字母的时候,就会显示建议

如果你想要取消建议功能,或者调整字符的距离:

command.DisableSuggestions = true

or

command.SuggestionsMinimumDistance = 1

你也可以设置可能会被建议的command的名字,使用SuggestFor属性.这对那些字符串距离不太近的单词可以起到效果,但是注意不要使用那些你会用来作为别名的名字

$ kubectl remove
Error: unknown command "remove" for "kubectl"

Did you mean this?
        delete

Run 'kubectl help' for usage.

Generating documentation for your command

cobra会基于你的 subcommand和flag生成文档.Read more about it in the docs generation documentation.

Generating shell completions

cobra能生成shell自动补全,Bash, Zsh, Fish, Powershell.Read more about it in Shell Completions.

关于cobra generator的使用

cobra用于创建命令行应用的脚手架工具

执行下面的命令安装

go get github.com/spf13/cobra/cobra

cobra init

创建应用初始代码,提供正确的项目结构,并且自动应用你给定的license

可以在当前应用运行,或者你可以指定一个相对路径,如果目录不存在,他会自己创建一个.

执行需要提供--pkg-name,在非空的目录中也能成功执行

mkdir -p newApp && cd newApp
cobra init --pkg-name github.com/spf13/newApp

or

cobra init --pkg-name github.com/spf13/newApp path/to/newApp

cobra add

用于添加新的command,举个例子

  • app serve
  • app config
  • app config create

在项目目录上执行以下命令即可

cobra add serve
cobra add config
cobra add create -p 'configCmd'

command采用驼峰式命名,不然可能会遇到错误.For example, cobra add add-user is incorrect, but cobra add addUser is valid.

Once you have run these three commands you would have an app structure similar to the following:

执行以上命令后的项目结构

  ▾ app/
    ▾ cmd/
        serve.go
        config.go
        create.go
      main.go

配置文件

提供配置文件可以避免每次使用提供一堆信息.

举个例子 ~/.cobra.yaml :

author: Steve Francia 
license: MIT

你也可以使用其他内置license,比如GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD.

也可以不使用证书,把license设置为none即可,或者你也可以自定义license

author: Steve Francia 
year: 2020
license:
  header: This file is part of CLI application foo.
  text: |
    {{ .copyright }}

    This is my license. There are many like it, but this one is mine.
    My license is my best friend. It is my life. I must master it as I must
    master my life.

上面的copyright部分是用author和year两个属性生成的,

上面的例子生成的内容是:

Copyright © 2020 Steve Francia 

This is my license. There are many like it, but this one is mine.
My license is my best friend. It is my life. I must master it as I must
master my life.

header也会在license头部被使用.

/*
Copyright © 2020 Steve Francia 
This file is part of CLI application foo.
*/

你可能感兴趣的:(golang 命令行解析库cobra的使用)