Demystifying Cobra by making a simple Calculator CLI app
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. Thego.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, andclone
is the child command. In therootCmd
variable, we are initializing the Cobra’scommand
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’sExecute()
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. Thecobra.OnInitialize(initConfig)
append theinitConfig
function declaration in therootCmd
’s initialization. Then, inside theinit()
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!