cobra of Go daily

brief introduction

cobra Is a command itinerary library, can be used to write command-line programs. It also provides a scaffold, Used to generate cobra based application framework. Many well-known open source projects use cobra library to build command line, such as Kubernetes,Hugo,etcd Wait, wait, wait. This paper introduces the basic use of cobra library and some interesting features.

About author spf13 , here are two more sentences. SPF 13 has many open source projects, and its open source projects are of high quality. I believe anyone who has used vim knows spf13-vim , known as the ultimate vim configuration. It can be configured with one click, which is definitely a good news for lazy people like me. His viper Is a complete configuration solution. It perfectly supports JSON/TOML/YAML/HCL/envfile/Java properties configuration file and other formats, as well as some more practical features, such as configuration hot update, multi directory search, configuration saving, etc. There are also very popular static website generators hugo It's also his work.

Rapid use

Third party libraries need to be installed before use. The following command installs cobra generator program and cobra Library:

$ go get github.com/spf13/cobra/cobra

If the golang.org/x/text library cannot be found, you need to manually download the library from GitHub, and then execute the above installation command. I wrote a blog before Build Go development environment This method is mentioned.

We implement a simple command-line program GIT. Of course, this is not true git, just to simulate its command line. Finally, the external program is called through the os/exec library to execute the real git command and return the result. So we need to install git on our system, and Git is in the executable path. At present, we only add a subcommand version. The directory structure is as follows:

▾ get-started/
    ▾ cmd/
        helper.go
        root.go
        version.go
    main.go

root.go:

package cmd

import (
  "errors"

  "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command {
  Use: "git",
  Short: "Git is a distributed version control system.",
  Long: `Git is a free and open source distributed version control system
designed to handle everything from small to very large projects 
with speed and efficiency.`,
  Run: func(cmd *cobra.Command, args []string) {
    Error(cmd, args, errors.New("unrecognized command"))
  },
}

func Execute() {
  rootCmd.Execute()
}

version.go:

package cmd

import (
  "fmt"
  "os"

  "github.com/spf13/cobra"
)

var versionCmd = &cobra.Command {
  Use: "version",
  Short: "version subcommand show git version info.",
  
  Run: func(cmd *cobra.Command, args []string) {
    output, err := ExecuteCommand("git", "version", args...)
    if err != nil {
      Error(cmd, args, err)
    }

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

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

The main.go file only calls the command entry:

package main

import (
  "github.com/darjun/go-daily-lib/cobra/get-started/cmd"
)

func main() {
  cmd.Execute()
}

For coding convenience, external programs and error handling functions are encapsulated in helpers.go:

package cmd

import (
  "fmt"
  "os"
  "os/exec"

  "github.com/spf13/cobra"
)

func ExecuteCommand(name string, subname string, args ...string) (string, error) {
  args = append([]string{subname}, args...)

  cmd := exec.Command(name, args...)
  bytes, err := cmd.CombinedOutput()

  return string(bytes), err
}

func Error(cmd *cobra.Command, args []string, err error) {
  fmt.Fprintf(os.Stderr, "execute %s args:%v error:%v\n", cmd.Name(), args, err)
  os.Exit(1)
}

Each cobra program has a root command, which can be added to any number of subcommands. We add the subcommand to the root command in the init function of version.go.

Compiler. Note that you can't go to run main.go directly. This is not a single file program. If you force it, use go run.:

$ go build -o main.exe

Help information automatically generated by cobra, very cool:

$ ./main.exe -h
Git is a free and open source distributed version control system
designed to handle everything from small to very large projects
with speed and efficiency.

Usage:
  git [flags]
  git [command]

Available Commands:
  help        Help about any command
  version     version subcommand show git version info.

Flags:
  -h, --help   help for git

Use "git [command] --help" for more information about a command.

Help information for a single subcommand:

$ ./main.exe version -h
version subcommand show git version info.

Usage:
  git version [flags]

Flags:
  -h, --help   help for version

Call subcommand:

$ ./main.exe version
git version 2.19.1.windows.1

Unrecognized subcommand:

$ ./main.exe clone
Error: unknown command "clone" for "git"
Run 'git --help' for usage.

When compiling, you can change main.exe to git, which will make you feel more comfortable.

$ go build -o git
$ ./git version
git version 2.19.1.windows.1

When using cobra to build the command line, the directory structure of the program is generally simple. The following structure is recommended:

▾ appName/
    ▾ cmd/
        cmd1.go
        cmd2.go
        cmd3.go
        root.go
    main.go

Each command implements a file, and all command files are stored in the cmd directory. Outer main.go only initializes cobra.

Characteristic

cobra provides very rich functions:

  • Easily support subcommands, such as app server, app fetch, etc;
  • Fully compatible with POSIX options (including short and long options);
  • Nested subcommand;
  • Global, local level options. You can set options in multiple places and use them in a certain order;
  • Use scaffolding to easily generate program frames and commands.

First, three basic concepts need to be defined:

  • Command: the operation to be performed;
  • Parameter (Arg): the parameter of the command, that is, the object to operate;
  • Option (Flag): command options adjust the behavior of the command.

In the following example, server is a (child) command and - port is an option:

hugo server --port=1313

In the following example, clone is a (child) command, URL is a parameter, and - bare is an option:

git clone URL --bare

command

In cobra, commands and subcommands are represented by command structures. Command has a lot of fields to customize the behavior of commands. In practice, the most commonly used ones are those. We saw Use/Short/Long/Run in the previous example.

Use specifies the usage information, that is, how the command is called, in the format name arg1 [arg2]. Name is the command name, arg1 is a required parameter, arg3 is an optional parameter, and there can be multiple parameters.

Short/Long is the help information of the specified command, but the former is short and the latter is detailed.

Run is the function that actually performs the operation.

Defining a new subcommand is simple: create a cobra.Command variable, set some fields, and add them to the root command. For example, we want to add a clone subcommand:

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

Where the Use field clone url [destination] indicates that the subcommand name is clone, the parameter url is required, and the destination path destination is optional.

We compile the program as a mygit executable and put it in $GOPATH/bin. I like to put $GOPATH/bin in $PATH, so I can call mygit command directly:

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

You can continue to add commands. But I just stole a lazy one here and forwarded all the operations to the actual git for execution. It's really of little practical use. With this idea, imagine that we can combine multiple commands to implement many useful tools, such as packaging tool.

option

There are two kinds of options in cobra, one is permanent option, which can be used by the command and its subcommand. Define global options by adding an option to the root command. The other is a local option, which can only be used in the command that defines it.

cobra use pflag Resolve command line options. pflag is basically the same as flag. In this series of articles, there is an introduction to the flag library, Go daily one library flag.

As with flag, variables for storing options need to be defined in advance:

var Verbose bool
var Source string

Set permanent options:

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

Set local options:

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

Both parameters are the same, long option / short option name, default value and help information.

Next, we use a case to demonstrate the use of options.

Suppose we want to make a simple calculator that supports addition, subtraction, multiplication and division. In addition, you can set whether to ignore non numeric parameters and whether to report an error by dividing 0 through options. Obviously, the first option should be placed in the global option and the second in the division command. The program structure is as follows:

▾ math/
    ▾ cmd/
        add.go
        divide.go
        minus.go
        multiply.go
        root.go
    main.go

Here we show divide.go and root.go. Other command files are similar. I put the complete code in GitHub Yes.

divide.go:

var (
  dividedByZeroHanding int // How to divide by 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:

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

In divide.go, it defines the options of how to deal with the error except 0, and in root.go, it defines the options of how to deal with the resolution error. The options are listed as follows:

const (
  ContinueOnParseError  ErrorHandling = 1 // Parse error attempt to continue processing
  ExitOnParseError      ErrorHandling = 2 // Parse error program stop
  PanicOnParseError     ErrorHandling = 3 // Parse error panic
  ReturnOnDividedByZero ErrorHandling = 4 // Except 0 return
  PanicOnDividedByZero  ErrorHandling = 5 // Except 0 painc
)

In fact, the execution logic of the command is not complicated, that is, to change the parameter to float64. Then perform the corresponding operation and output the result.

Test procedure:

$ go build -o math
$ ./math add 1 2 3 4
1+2+3+4 = 10.00

$ ./math minus 1 2 3 4
1-2-3-4 = -8.00

$ ./math multiply 1 2 3 4
1*2*3*4 = 24.00

$ ./math divide 1 2 3 4
1/2/3/4 = 0.04

By default, parsing errors are ignored, and only the results of parameters with correct format are calculated:

$ ./math add 1 2a 3b 4
1+2a+3b+4 = 5.00

$ ./math divide 1 2a 3b 4
1/2a/3b/4 = 0.25

Set the processing of resolution failure, 2 for exit program, 3 for panic (see the above enumeration):

$ ./math add 1 2a 3b 4 -p 2
invalid number: 2a

$ ./math add 1 2a 3b 4 -p 3
panic: strconv.ParseFloat: parsing "2a": invalid syntax

goroutine 1 [running]:
github.com/darjun/go-daily-lib/cobra/math/cmd.ConvertArgsToFloat64Slice(0xc00004e300, 0x4, 0x6, 0x3, 0xc00008bd70, 0x504f6b, 0xc000098600)
    D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/helper.go:58 +0x2c3
github.com/darjun/go-daily-lib/cobra/math/cmd.glob..func1(0x74c620, 0xc00004e300, 0x4, 0x6)
    D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/add.go:14 +0x6d
github.com/spf13/cobra.(*Command).execute(0x74c620, 0xc00004e1e0, 0x6, 0x6, 0x74c620, 0xc00004e1e0)
    D:/code/golang/src/github.com/spf13/cobra/command.go:835 +0x2b1
github.com/spf13/cobra.(*Command).ExecuteC(0x74d020, 0x0, 0x599ee0, 0xc000056058)
    D:/code/golang/src/github.com/spf13/cobra/command.go:919 +0x302
github.com/spf13/cobra.(*Command).Execute(...)
    D:/code/golang/src/github.com/spf13/cobra/command.go:869
github.com/darjun/go-daily-lib/cobra/math/cmd.Execute(...)
    D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/cmd/root.go:45
main.main()
    D:/code/golang/src/github.com/darjun/go-daily-lib/cobra/math/main.go:8 +0x35

As for the option except 0, please try it yourself.

Careful friends should have noticed that there are still some imperfections in the program. For example, if you enter a non numeric parameter here, it will also be displayed in the result:

$ ./math add 1 2 3d cc
1+2+3d+cc = 3.00

You can improve yourself if you are interested~

Scaffolding

Through the previous introduction, we also see that the framework of cobra command is relatively fixed. This gives us a place to use tools, which can greatly improve our development efficiency.

When cobra library is installed in front, the scaffold program is also installed. Let's show you how to use this generator.

Use the cobra init command to create a cobra application:

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

Where scaffold is the application name, followed by the PKG name option to specify the package path. The generated program directory structure is as follows:

▾ scaffold/
    ▾ cmd/
        root.go
    LICENSE
    main.go

The structure of this project is exactly the same as that introduced before, and it is also the structure recommended by cobra. Likewise, main.go is just the entrance.

In root.go, the tool generated some extra code for us.

The profile option is added to the root command, which is required for most applications:

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

In the callback after initialization, if this option is found to be empty, the. scaffold.yaml file in the home directory is used by default:

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

Here's what I introduced the other day go-homedir Library. The reading of configuration file uses SPF 13's own open source project viper (poison dragon? What a genius.

In addition to the code file, cobra also generates a LICENSE file.

Now this program can't do anything. We need to add subcommands to it. Use cobra add command:

$ cobra add date

This command adds a date.go file in the cmd directory. The basic structure has been set up. The rest is to modify some descriptions and add some options.

We now realize the function of printing the calendar of this month according to the incoming year and month. If there is no incoming option, use the current year and month.

Option definition:

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]")
}

To modify the Run function of dateCmd:

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

The showCalendar function is implemented by using the method provided by time, which is not covered here. If you are interested, go to my GitHub to see the implementation.

See the running effect of the program:

$ go build -o main.exe
$ ./main.exe date
  Sun  Mon  Tue  Wed  Thu  Fri  Sat
                   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

$ ./main.exe date --year 2019 --month 12
  Sun  Mon  Tue  Wed  Thu  Fri  Sat
    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

You can add other functions to this program. Try it~

Other

cobra provides very rich features and customized interfaces, such as:

  • Set hook function to perform some operations before and after command execution;
  • Generate documents in markdown / restructured text / man page format;
  • Wait, wait, wait.

Due to the space limitation, we will not introduce them one by one. You can do your own research if you are interested. cobra library is widely used, and many well-known projects are useful, as mentioned above. Learn how these projects use cobra, from which you can learn about cobra's features and best practices. This is also a good way to learn about open source projects.

All the sample code in this article has been uploaded to my GitHub, Go dailyhttps://github.com/darjun/go-daily-lib/tree/master/cobra.

Reference resources

  1. cobra GitHub warehouse

I

My blog

Welcome to my WeChat GoUpUp, learn together and make progress together.

>This article is based on the platform of blog one article multiple sending OpenWrite Release!

Keywords: Programming git github vim Windows

Added by Bullet on Sat, 18 Jan 2020 11:57:23 +0200