Demystifying Cobra by making a simple Calculator CLI app

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

Cobra is one of the most popular projects in the Golang ecosystem. It is simple, elegant, and efficient.

What the heck is CLI?

A command-line interface processes commands to a computer program in the form of lines of text.

To put it simply, It’s an application that lets users type text commands into the computer to tell it what to do.

Those of us, who are familiar with using Git, often use commands like git clone URL or git add . etc. These are all commands.

Then, what is Cobra?

Package cobra is a commander providing a simple interface to create powerful modern CLI interfaces. In addition to providing an interface, Cobra simultaneously provides a controller to organize your application code.

Basically, Cobra works like a wrapper, and the reasons behind using cobra are it’s lightweight, efficient, and minimalistic.

Enough of theory, let’s get started.

In this post, I will build a simple Calculator using Cobra.

Requirements to get started:

  • Go installed in the machine.
  • Installing Cobra (go get -u github.com/spf13/cobra)
  • IDE of your choice. (I am using Goland by Jetbrains)

Concepts

Cobra is built on a structure of commands, arguments & flags. Commands represent actions, Args are things and Flags are modifiers for those actions.

The best applications read like sentences when used, and as a result, users intuitively know how to interact with them.

The pattern to follow is APPNAME VERB NOUN --ADJECTIVE. or APPNAME COMMAND ARG --FLAG (source)

Create a new directory outside of $GOPATH . Here, I have created a directory called calculator-cli. Open the directory in the terminal, and initialize modules in it by

go mod init calculator-cli

This will create a go.mod file inside the project directory.

A module is a collection of Go packages stored in a file tree with a go.mod file at its root. The go.mod file defines the module’s module path, which is also the import path used for the root directory, and its dependency requirements, which are the other modules needed for a successful build. Each dependency requirement is written as a module path and a specific semantic version.

To learn more about go modules visit here.

I believe you have already installed Cobra. So it’s time to initialize it.

cobra init --pkg-name calculator-cli

You will see, this has created a new directory called cmd inside the project.

|-calculator-cli
|-cmd
|-root.go
|-main.go

Design

A minimal command in Cobra is a structure with a name, description and a function to run the logic.

Lets’ understand the command flow in Cobra.

  • main.go is the starting point of the CLI.
  • rootCmd is the base command of any command-line interface. Say for example, git clone here, git is the base command, and clone is the child command. In the rootCmd variable, we are initializing the Cobra’s command struct variable. Inside the root.go, there is a function, find this line of code.
Run: func(cmd *cobra.Command, args []string) {},

change this to

Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Inside the rootCmd")
},
  • This is the first function that gets executed when we run the application. cobra.OnInitialize(initConfig) sets the passed functions to be run when each command’s Execute() method is called. When the command is run or called, all of the functions in the command’s initialization are executed first, followed by the execute method. The cobra.OnInitialize(initConfig) append the initConfigfunction declaration in the rootCmd’s initialization. Then, inside the init() function put this print statement,
fmt.Println("Inside init()")
  • At last, inside the initConfig()
fmt.Println("Inside initConfig()")

It’s time to see the flow of the CLI.

Run this command to see all the imports are there.

go mod vendor -v

Then running this will create the binary executable file of this app.

go install calculator-cli

Now in the terminal, type this command calculator-cli , this should show the following

Inside init()
Inside initConfig()
Inside the rootCmd

From here, we can get an overall idea about the execution flow of the CLI.

Adding integers

Our first task will be adding a series of integers. Since it is a CLI, we need to add a command to add numbers, right?

Run this command cobra add add . This will add a add.go file inside the cmd directory. This looks pretty similar like root.go . Let’s create a function that will add a series of numbers.

func addInt(s []string){
var sum int
for _, i := range s{
val, err := strconv.Atoi(i)
if err != nil{
fmt.Println(err)
}
sum = sum + val
}
fmt.Printf("Addition of %s is %d", s, sum)
}

Inside the addCmd, there’s a RUN function which takes *cobra.Command and args []string as parameters. Let’s call our defined addInt function from here

Run: func(cmd *cobra.Command, args []string) {
addInt(args)
},

Now it’s time to test if our function is working expectedly, rebuild the binary by go install calculator-cli .

Run, calculator-cli add 1200 500 this should show an output like this, Addition of [1200 500] is 1700 .

So, we now can add a series of numbers with this simple CLI app. What if we want to add floating-point numbers? For now, our application can’t handle it. For this, we will use the same sub-command add but pass a flag so that our app knows which function to execute.

There are two types of flags in Cobra.

Persistent flags will be available to the command it’s assigned to as well as every command under that command.

Local flags will only be available to the command to which it was assigned.

Okay, now in the add.go add a local flag that will have a shortend name f and default value set to false .

func init() {
rootCmd.AddCommand(addCmd)
addCmd.Flags().BoolP("float","f", false, "Addition of Floating Point Numbers")
}

Create another function that will be able to handle decimal values.

func addFloat(s []string){
var sum float64
for _, i := range s{
val, err := strconv.ParseFloat(i, 64)
if err != nil{
fmt.Println(err)
}
sum = sum + val
}
fmt.Printf("Addition of %s is %.3f", s, sum)
}

We have created the function and added a local flag, now it’s time to decide what to do when -f or --float is passed with the command.

Inside the addCmd, rewrite the function like this,

Run: func(cmd *cobra.Command, args []string) {
//fmt.Println("add called")
status, _ := cmd.Flags().GetBool("float")
if status{
addFloat(args)
}else{
addInt(args)
}
},

Rebuild the binary and test the addFloat function.

  • calculator-cli add 2.5 3.5 -f

We should get this Addition of [2.5 3.5] is 6.000

Finally, our application can handle adding floating-point numbers as well.

You can try adding more commands for multiplication, subtraction, etc by going into this GitHub repo.

I hope next time you’ll be working with Cobra, it will make more sense to you than before.

Thank you for reading!

Software Engineer | Stationary Addict | Traveler