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 daily,https://github.com/darjun/go-daily-lib/tree/master/cobra.
Reference resources
- cobra GitHub warehouse
I
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!