Exploring Go Packages: Cobra

In this tutorial, we discuss how to code a help message using the Cobra package.

Jane Kozhevnikova
Level Up Coding

--

A picture of mine

› Briefly about Cobra

Cobra is a powerful command line package for Golang. The full list of what projects use Cobra is here.

› Before You Begin

Install Cobra in your machine:

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

Yet, you do not need to create files of our app. There is a generator, which helps to create a working directory very easily.

To install the generator use this command:

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

Before we start coding, let’s review a help message we are going to create with the Cobra package.

› Writing Help Message

A help message (or usage message, or doc) is what users see when they

  • invoke a program,
  • type a flag like --hor -h,
  • type invalid arguments.

This message includes the name of the program, its usage, and options. If you use the command line, you are already familiar with help messages or usage messages. In Linux, you can usually type program --helpto get a list of acceptable arguments of the program.

In this tutorial, we write a help message for a program that outputs:

  • a list of random numbers of a specified count and range, or
  • a list of random letters of a specific count and language.

This is our usage message:

Let’s analyze a command, numbers, from the preceding message:

randx numbers --count <count> [--range <range>...]

  • randx — a name of the program,
  • numbers — a command,
  • --count— an option of the command,
  • <count> — a value of the option — count,
  • [--range <range>…] — an optional parameter which requires a value or several values,
  • [--verbose]— an optional flag.

We have two commands, numbers and letters (bool), to enable the numbers or letters mode, respectively. If one of them is true, another is false.

The — count(int) option is required for both the numbers andletters commands. The program outputs a list of numbers or letters of the specified count.

$ randx numbers --count 1

$ randx letters --count 1

To declare an optional argument --range([]string), we use square brackets. Three dots in the square brackets mean that you can pass several arguments to the command prompt. For example,

--range 1,10 --range 1,11 or
--range 1-10 --range 1-11 or
--range 1:10 --range 1:11

This option sets a range of random numbers. If there are two arguments, the program outputs two lists with the specified ranges.

The optional argument, --lang(string), is also in square brackets. The option accepts a language. Therefore the program outputs letters of the specified language.

The program must return a help message when the required option is not set and/or the arguments are invalid. Both --range and --lang options can be omitted.

Also, we have three options (or just flags) without operands in the usage message:

  • -h / --help — output the usage message,
  • --verboseoutput the details of what a program does; the flag is used to inform a user what the program actually does, including errors while performing a required action. This can be helpful for troubleshooting problems.
  • --version — output the version of the program which you use.

› Generating Project Structure

  1. Create and go to a directory:
mkdir -p ~/go/src/github.com/your_username/app && 
cd ~/go/src/github.com/your_username/app

2. Generate a project structure with cobra init:

cobra init --pkg-name ~/go/src/github.com/your_username/app

After that, we have the following project structure:

— — cmd
— — — root.go
— — LICENSE
— — main.go

The root.go file contains the rootCmdcommand, which initializes all the commands and flags (or options).

› Creating Commands

To add a new command simply use cobra add commandName in the project directory. For our help message, we need to create two commands numbers and letters:

$ cobra add numbers
$ cobra add letters

After that, the cobra package generates two files namely numbers.go and letters.go in the cmd directory. Now we have the following project structure:

 — — cmd
— — — root.go
— — — numbers.go
— — — letters.go
— — LICENSE
— — main.go

› What main.go looks like

In the main.go file we execute our commands with cmd.Execute(). The full code looks like this:

package mainimport “github.com/your_user_name/app/cmd”func main() {
cmd.Execute()
}

There is no need to add additional code here. Let’s create subcommands and options in the numbers.go and letters.go files.

› Numbers Command

Cobra generated some of the following code. We declared countFlagNumbersand rangeFlagNumbers variables to store values from
--countand --rangeflags respectively. Also, we initialized required flags (--countand --range) in the init() function.

// beginning of numbers.gopackage cmdimport (
"fmt"
"github.com/spf13/cobra"
)
var (
countFlagNumbers int
rangeFlagNumbers []string
)
var numbersCmd = &cobra.Command{
Use: "numbers",
Short: "Returns random numbers",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("numbers mode")
fmt.Println("--count:", countFlagNumbers)
fmt.Println("--range:", rangeFlagNumbers)
fmt.Println("--verbose:", verbose)
},
}
// here is init()

We call arootCmd.AddCommand() method to initialize the numbers command.

//beginning of init()func init() {
rootCmd.AddCommand(numbersCmd)
...

The declaration of every flag has the following pattern:

commandName.Flags().TypeOfValuesP(
where to store, long flag name,
short flag name, default value,
explanation,
)

Declare the --count flag:

...    numbersCmd.Flags().IntVarP(
&countFlag, "count", "c", 0,
"A count of random numbers",
)
...

And make this flag required:

...     numbersCmd.MarkFlagRequired("count")...

When we make a flag required, it means that a user cannot execute the program without this flag set. There will be an error.

Declare the --range flag:

...
numbersCmd.Flags().StringSliceVarP(
&rangeFlag, "range", "r", []string{"1:100"},
"Range of numbers. Optional",
)
}
// end of init()
// end of numbers.go

› Letters Command

The same as in numbers.go, some of the following code was generated. Here we declared countFlagLetters and langFlagLetters variables to store the received values from the --count and --langflags respectively.

// beginning of letters.gopackage cmdimport (
“fmt”
“github.com/spf13/cobra”
)
var (
countFlagLetters int
langFlagLetters string
)

The lettersCmd command has the same structure as numbersCmd. All the flags are initialized the same way as in the numbers.go file.

var lettersCmd = &cobra.Command{
Use: "letters",
Short: "Returns random letters",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("letters mode")
fmt.Println("--count:", countFlagLetters)
fmt.Println("--lang:", langFlagLetters)
fmt.Println("--verbose:", verbose)
},
}
func init() {
rootCmd.AddCommand(lettersCmd)
lettersCmd.Flags().IntVarP(
&Count, "count", "c", 0,
"A count of random letters",
)
lettersCmd.MarkFlagRequired("count")

lettersCmd.Flags().StringVarP(
&Lang, "lang", "l", "en",
"A language. Optional",
)
}
// end of letters.go

› Adding Version and Verbose flags

In root.go, we update the usage of our program, add the --version and
--verboseflags.

package cmdimport (
"fmt"
"os"
"github.com/spf13/cobra"
)

Declare a verbose variable. It has a bool type.

var (
verbose bool
)

In order to show the version of our program, add a Versionparameter and specify a current version in quotes.

var rootCmd = &cobra.Command{
Use: "randx",
Version: "1.0.1",
Short: "Returns random numbers or letters.",
}
func Execute() {
if err := rootCmd.Execute();
err != nil {
fmt.Println(err)
os.Exit(1)
}
}

The--verboseflag is persistent and global. Which means you can tell the program to show you details of the program execution with any command and flag set.

func init() {
rootCmd.PersistentFlags().BoolVarP(
&verbose, "verbose", "v", false,
"Verbose output",
)
}

› Usage

In this section, you can find several cases of how to use Cobra in the command line.

›› Letters Mode

$ go run main.go letters --count 9 
$ go run main.go letters -c 9
letters mode
--count: 9
--lang: en // this is a default value
--verbose: false
$ go run main.go letters --count 9 --lang en
$ go run main.go letters -c 9 -l en
letters mode
--count: 9
--lang: en
--verbose: false

›› Numbers Mode

$ go run main.go numbers --count 9 
$ go run main.go numbers -c 9
numbers mode
--count: 9
--range: [1:100] // this is a default value
--verbose: false
$ go run main.go numbers --count 10 --range 9,10
$ go run main.go numbers -c 10 -r 9,10
numbers mode
--count: 10
--range: [9 10]
--verbose: false
$ go run main.go numbers --count 10 --range 9,10 --range 0,15
numbers: true
--count: 10
--range: [9 10 0 15]
--verbose: false

Some comments about delimiters. If you you use the comma as a delimiter in
--range flag, you get only a list of numbers. As discussed earlier, you can use the colon: to separate these numbers. For example:

$ go run main.go numbers --count 10 --range 9:10 --range 10:11
numbers mode
--count: 10
--range: [9:10 10:11]
--verbose: false

›› Verbose

$ go run main.go numbers --count 18 --range 9,19 --verbose 
$ go run main.go numbers -c 18 -r 9,19 --verbose
numbers mode
--count: 18
--range: [9 19]
--verbose: true

› About Author

Jane is a Go programmer and technical writer in software engineering. She has been writing technical material for 5 years in both English and Russian. She completed her specialist degree in Information Security from Novosibirsk State Technical University, and specializes in the information security of automated systems. You can follow her on Twitter and see her other written work at publications.enthusiastic.io.

--

--