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

功能

  • 简易的子命令行模式,如 app server, app fetch等等
  • 完全兼容posix命令行模式
  • 嵌套子命令subcommand
  • 支持全局,局部,串联flags
  • 使用Cobra很容易的生成应用程序和命令,使用cobra create appname和cobra add cmdname
  • 如果命令输入错误,将提供智能建议,如 app srver,将提示srver没有,是否是app server
  • 自动生成commands和flags的帮助信息
  • 自动生成详细的help信息,如app help
  • 自动识别-h,–help帮助flag
  • 自动生成应用程序在bash下命令自动完成功能
  • 自动生成应用程序的man手册
  • 命令行别名
  • 零活定义help和usage信息
  • 可选的紧密集成的viper apps

概念

Cobra是建立在结构的命令、参数和选项之上。

cobra 中有个重要的概念,分别是 commands、arguments 和 flags。其中 commands 代表行为,arguments 就是命令行参数(或者称为位置参数),flags 代表对行为的改变(也就是我们常说的命令行选项)。执行命令行程序时的一般格式为:

1
APPNAME COMMAND ARG --FLAG

比如下面的例子:

1
2
3
4
5
# server是 commands,port 是 flag
hugo server --port=1313

# clone 是 commands,URL 是 arguments,brae 是 flag
git clone URL --bare

如果是一个简单的程序(功能单一的程序),使用 commands 的方式可能会很啰嗦,但是像 git、docker 等应用,把这些本就很复杂的功能划分为子命令的形式,会方便使用(对程序的设计者来说又何尝不是如此)。

安装

使用Cobra很简单。首先,使用go get安装最新版本

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

然后在你项目里引用Cobra

1
import "github.com/spf13/cobra"

开始

通常基于Cobra的应用程序将遵循下面的组织结构,当然你也可以遵循自己的接口:

1
2
3
4
5
6
7
  ▾ appName/
    ▾ cmd/
        add.go
        your.go
        commands.go
        here.go
      main.go

在Cobra应用程序中,通常main.go文件非常空洞。它主要只干一件事:初始化Cobra。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import (
  "fmt"
  "os"

  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

使用Cobra库

使用Cobra,需要创建一个空的main.go文件和一个rootCmd文件。你可以选择在合适的地方添加额外的命令。

命令

在 cobra 中,命令和子命令都是用Command结构表示的。Command有非常多的字段,用来定制命令的行为。在实际中,最常用的就那么几个。

Use指定使用信息,即命令怎么被调用,格式为name arg1 [arg2]。name为命令名,后面的arg1为必填参数,arg3为可选参数,参数可以多个。

Short/Long都是指定命令的帮助信息,只是前者简短,后者详尽而已。

Run是实际执行操作的函数。

定义新的子命令很简单,就是创建一个cobra.Command变量,设置一些字段,然后添加到根命令中。例如我们要添加一个clone子命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package cmd

import (
  "fmt"
  "os"

  "github.com/spf13/cobra"
)

var cloneCmd = &cobra.Command {
  Use: "clone url [destination]",
  Short: "Clone a repository into a new directory",
  Run: func(cmd *cobra.Command, args []string) {
    output, err := ExecuteCommand("git", "clone", args...)
    if err != nil {
      Error(cmd, args, err)
    }

    fmt.Fprintf(os.Stdout, output)
  },
}

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

其中Use字段clone url [destination]表示子命令名为clone,参数url是必须的,目标路径destination可选。

我们将程序编译为mygit可执行文件,可以直接调用mygit命令了:

1
2
3
4
$ go build -o mygit
$ mv mygit $GOPATH/bin
$ mygit clone https://github.com/darjun/leetcode
Cloning into 'leetcode'...

创建rootCmd

Cobra不需要特殊的构造函数。简单的就可以创建你的命令。

理想情况下你把这个放在在 app/cmd/root.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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()函数。

比如 cmd/root.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import (
  "fmt"
  "os"

  homedir "github.com/mitchellh/go-homedir"
  "github.com/spf13/cobra"
  "github.com/spf13/viper"
)

func init() {
  cobra.OnInitialize(initConfig)
  rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
  rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
  rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
  rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
  rootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
  viper.BindPFlag("projectbase", rootCmd.PersistentFlags().Lookup("projectbase"))
  viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
  viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
  viper.SetDefault("license", "apache")
}

func initConfig() {
  // Don't forget to read config either from cfgFile or from home directory!
  if cfgFile != "" {
    // Use config file from the flag.
    viper.SetConfigFile(cfgFile)
  } else {
    // Find home directory.
    home, err := homedir.Dir()
    if err != nil {
      fmt.Println(err)
      os.Exit(1)
    }

    // Search config in home directory with name ".cobra" (without extension).
    viper.AddConfigPath(home)
    viper.SetConfigName(".cobra")
  }

  if err := viper.ReadInConfig(); err != nil {
    fmt.Println("Can't read config:", err)
    os.Exit(1)
  }
}

创建 main.go

你需要在main函数里执行root命令。

通常main.go文件非常空洞。它主要只干一件事:初始化Cobra。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main


import (
  "fmt"
  "os"

  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

创建其它的命令

其它的命令通常定义在cmd/目录下的自己文件内

如果你想创建一个version命令,你可以创建cmd/version.go文件,并在文件里这么写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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")
  },
}

使用选项

选项提供修饰符以控制操作命令的操作方式。

选项

cobra 中选项分为两种,一种是永久选项,定义它的命令和其子命令都可以使用。通过给根命令添加一个选项定义全局选项。另一种是本地选项,只能在定义它的命令中使用。

cobra 使用pflag解析命令行选项。pflag使用上基本与flag相同.

与flag一样,存储选项的变量也需要提前定义好:

1
2
var Verbose bool
var Source string

设置永久选项:

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

设置本地选项:

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

两种参数都是相同的,长选项/短选项名、默认值和帮助信息。

给命令分配一个选项

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

1
2
var Verbose bool
var Source string

持久选项

‘持久’表示每个在那个命令下的命令都将能分配到这个选项。对于全局选项,‘持久’的选项绑定在root上。

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

局部选项

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

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

举例

下面,我们通过一个案例来演示选项的使用。

假设我们要做一个简单的计算器,支持加、减、乘、除操作。并且可以通过选项设置是否忽略非数字参数,设置除 0 是否报错。显然,前一个选项应该放在全局选项中,后一个应该放在除法命令中。程序结构如下:

1
2
3
4
5
6
7
8
▾ math/
    ▾ cmd/
        add.go
        divide.go
        minus.go
        multiply.go
        root.go
    main.go

这里展示divide.go和root.go,其它命令文件都类似。

divide.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var (
  dividedByZeroHanding int // 除 0 如何处理
)
var divideCmd = &cobra.Command {
  Use: "divide",
  Short: "Divide subcommand divide all passed args.",
  Run: func(cmd *cobra.Command, args []string) {
    values := ConvertArgsToFloat64Slice(args, ErrorHandling(parseHandling))
    result := calc(values, DIVIDE)
    fmt.Printf("%s = %.2f\n", strings.Join(args, "/"), result)
  },
}

func init() {
  divideCmd.Flags().IntVarP(&dividedByZeroHanding, "divide_by_zero", "d", int(PanicOnDividedByZero), "do what when divided by zero")

  rootCmd.AddCommand(divideCmd)
}

root.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
var (
  parseHandling int
)

var rootCmd = &cobra.Command {
  Use: "math",
  Short: "Math calc the accumulative result.",
  Run: func(cmd *cobra.Command, args []string) {
    Error(cmd, args, errors.New("unrecognized subcommand"))
  },
}

func init() {
  rootCmd.PersistentFlags().IntVarP(&parseHandling, "parse_error", "p", int(ContinueOnParseError), "do what when parse arg error")
}

func Execute() {
  rootCmd.Execute()
}

在divide.go中定义了如何处理除 0 错误的选项,在root.go中定义了如何处理解析错误的选项。选项枚举如下:

1
2
3
4
5
6
7
const (
  ContinueOnParseError  ErrorHandling = 1 // 解析错误尝试继续处理
  ExitOnParseError      ErrorHandling = 2 // 解析错误程序停止
  PanicOnParseError     ErrorHandling = 3 // 解析错误 panic
  ReturnOnDividedByZero ErrorHandling = 4 // 除0返回
  PanicOnDividedByZero  ErrorHandling = 5 // 除0 painc
)

其实命令的执行逻辑并不复杂,就是将参数转为float64。然后执行相应的运算,输出结果。

使用配置绑定选项

你同样可以通过viper绑定选项:

1
2
3
4
5
6
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不会被赋值。

必须的标记

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

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

位置和自定义参数

验证位置参数可以通过* Command的Args字段。

内置下列验证方法

  • NoArgs - 如果有任何参数,命令行将会报错。
  • ArbitraryArgs - 命令行将会接收任何参数.
  • OnlyValidArgs - 如果有如何参数不属于Command的ValidArgs字段,命令行将会报错。
  • MinimumNArgs(int) - 如果参数个数少于N个,命令行将会报错。
  • MaximumNArgs(int) - 如果参数个数多余N个,命令行将会报错。
  • ExactArgs(int) - 如果参数个数不能等于N个,命令行将会报错。
  • RangeArgs(min, max) - 如果参数个数不在min和max之间, 命令行将会报错.

一个设置自定义验证的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var cmd = &cobra.Command{
  Short: "hello",
  Args: func(cmd *cobra.Command, args []string) error {
    if len(args) < 1 {
      return errors.New("requires at least one arg")
    }
    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!")
  },
}

例子

在下面的例子,我们定义了3个命令。2个在顶级,一个(cmdTimes)是其中一个顶级命令的子命令。在这个例子里,由于没有给rootCmd提供Run,单独的root是不能运行的,必须要有子命令。

我们仅为一个命令定义了标记。

更多关于flags的文档可以在https://github.com/spf13/pflag 找到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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("Print: " + strings.Join(args, " "))
    },
  }

  var cmdTimes = &cobra.Command{
    Use:   "times [# 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命令

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

例子

下面的输入是 Cobra 自动生成的。除了命令和选项的定义,其它不再需要。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ 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命令或模板。使用下面的方法:

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

后2个也将适用于任何子命令

使用信息

当用户提供无效的标记或命令,Cobra 将会返回用法。

例子

你可能从上面的帮助意识到,默认的帮助将被嵌入到用法里然后作为输出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ 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 使用。 比如帮助,方法和模板都可以重写。

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

版本标记

如果Version字段设置到了根命令,Cobra 会提供了一个顶层 ‘–version’标记。运行带上‘–version’标记的程序,将会按照模板版本信息。模板可以通过cmd.SetVersionTemplate(s string)方法修改

运行前和运行后钩子

在命令运行前或运行后,再运行方法非常容易。PersistentPreRun和PreRun方法将会在Run之前执行。PersistentPostRun和PostRun方法将会在Run之后执行。Persistent*Run方法会被子命令继承,如果它们自己没有定义的话。这些方法将按照下面的属性执行:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

下面的例子,2个命令都使用了上面的特性。当子命令执行的时候,它将执行根命令的PersistentPreRun,但不会执行根命令的PersistentPostRun:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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()
}

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 会自动输出建议,当遇到“unknown command”错误时。这使得当输入错误时, Cobra 的行为类似git命令。例如:

1
2
3
4
5
6
7
$ hugo srever
Error: unknown command "srever" for "hugo"

Did you mean this?
        server

Run 'hugo --help' for usage.

建议会基于注册的子命令自动生成。使用了Levenshtein distance的实现。每一个注册的命令会匹配2个距离(忽略大小写)来提供建议。

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

1
command.DisableSuggestions = true

1
command.SuggestionsMinimumDistance = 1

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

1
2
3
4
5
6
7
$ kubectl remove
Error: unknown command "remove" for "kubectl"

Did you mean this?
        delete

Run 'kubectl help' for usage.

生成命令的文档

Cobra 可以基于子命令,标记,等生成文档。以以下格式:

1
2
3
Markdown
ReStructured Text
Man Page

生成bash-completion

Cobra 可以生成一个bash-completion文件。如果你给命令添加更多信息,这些completions可以非常强大和灵活。更多介绍在Bash Completions。

Cobra生成器

Cobra提供自己的程序来创建你的程序并且添加你想要的命令。这是最简单的方式把Cobra添加到你的程序里。

这里你能找到相关信息

通过前面的介绍,我们也看到了其实 cobra 命令的框架还是比较固定的。这就有了工具的用武之地了,可极大地提高我们的开发效率。

前面安装 cobra 库的时候也将脚手架程序安装好了。下面我们介绍如何使用这个生成器。

使用cobra init命令创建一个 cobra 应用程序:

1
cobra init scaffold --pkg-name github.com/darjun/go-daily-lib/cobra/scaffold

其中scaffold为应用程序名,后面通过pkg-name选项指定包路径。生成的程序目录结构如下:

1
2
3
4
5
 scaffold/
     cmd/
        root.go
    LICENSE
    main.go

这个项目结构与之前介绍的完全相同,也是 cobra 推荐使用的结构。同样地,main.go也仅仅是入口。

在root.go中,工具额外帮我们生成了一些代码。

在根命令中添加了配置文件选项,大部分应用程序都需要配置文件:

1
2
3
4
5
6
func init() {
  cobra.OnInitialize(initConfig)

  rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.scaffold.yaml)")
  rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

在初始化完成的回调中,如果发现该选项为空,则默认使用主目录下的.scaffold.yaml文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func initConfig() {
  if cfgFile != "" {
    viper.SetConfigFile(cfgFile)
  } else {
    home, err := homedir.Dir()
    if err != nil {
      fmt.Println(err)
      os.Exit(1)
    }

    viper.AddConfigPath(home)
    viper.SetConfigName(".scaffold")
  }

  viper.AutomaticEnv()

  if err := viper.ReadInConfig(); err == nil {
    fmt.Println("Using config file:", viper.ConfigFileUsed())
  }
}

除了代码文件,cobra 还生成了一个 LICENSE 文件。

现在这个程序还不能做任何事情,我们需要给它添加子命令,使用cobra add命令:

1
cobra add date

该命令在cmd目录下新增了date.go文件。基本结构已经搭好了,剩下的就是修改一些描述,添加一些选项了。

我们现在实现这样一个功能,根据传入的年、月,打印这个月的日历。如果没有传入选项,使用当前的年、月。

选项定义:

1
2
3
4
5
6
func init() {
  rootCmd.AddCommand(dateCmd)

  dateCmd.PersistentFlags().IntVarP(&year, "year", "y", 0, "year to show (should in [1000, 9999]")
  dateCmd.PersistentFlags().IntVarP(&month, "month", "m", 0, "month to show (should in [1, 12]")
}

修改dateCmd的Run函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Run: func(cmd *cobra.Command, args []string) {
  if year < 1000 && year > 9999 {
    fmt.Fprintln(os.Stderr, "invalid year should in [1000, 9999], actual:%d", year)
    os.Exit(1)
  }

  if month < 1 && year > 12 {
    fmt.Fprintln(os.Stderr, "invalid month should in [1, 12], actual:%d", month)
    os.Exit(1)
  }

  showCalendar()
}

showCalendar函数就是利用time提供的方法实现的,这里就不赘述了。