DEV Community

Cover image for Golang - Writing CLI App in Golang with Cobra
Satyajiijt Roy
Satyajiijt Roy

Posted on • Edited on

Golang - Writing CLI App in Golang with Cobra

Golang has good amount libraries in-build and maintained by community. One of them is Cobra, which is pretty good to write any cli apps. Today will try to see how we can use the library and what all functionality it provides.

The goal is to understand how to write a cli app in Golang using Cobra. So the app will only print strings for sub-commands executions.*

Requirements Hypothetical

Say we are trying to build a simple go cli app. Which will have 2 sub-commands check-url and check-status. Both the commands will take a url/api as argument

Go Project Structure

We will call our app url-monitor and this is how the whole folder structure will look like

url-monitor
├── commands
   ├── check-status.go
   ├── check-url.go
   ├── helpers.go
   └── root.go
├── go.mod
└── main.go
Enter fullscreen mode Exit fullscreen mode

Let’s Code then

main.go ➡ We will to start with main.go which will be under the package called main and it should also have the main{} function to initiate the app and also import the additional packages and sub folders. Something like this

package main

import "url-monitor/commands"

func main() {
   commands.Execute()
}
Enter fullscreen mode Exit fullscreen mode

Note: Don’t worry about commands.Execute() which we haven’t defined yet. Basically it is calling the Execute() function from package commands.

root.go ➡ let’s check the root.go which will have the init() function which will be responsible to run some of the stuff we need to be executed first. To read more about go init() function check my previous blog.

This is how my root.go looks

package commands

import (
   "fmt"
   "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
   Use:   "url-monitor",
   Short: "url-monitor",
   Long: `url-monitor`,
}


/*
The init function is responsible to run things
which we will require before anything else
say
  - Fetch API Keys
  - Set Logging level
  - Setup any environment variable required for the app
 */

func init() {
   rootCmd.AddCommand(checkUrlCmd, checkStatusCmd)
}

func Execute() {
   if err := rootCmd.Execute(); err != nil {
      log.Fatal(err)
   }
}
Enter fullscreen mode Exit fullscreen mode

Let me explain couple of things here

Now I have variable called rootCmd which has type of &cobra.Command{} type. The &cobra.Command{} type basically has fields like Use, Short and Long to be populated. Which are nothing but the name of the cli app and short and long description for the app which will be displayed when we run the app. Read more about Command Type.

var rootCmd = &cobra.Command{
   Use:   "url-monitor",
   Short: "url-monitor is used monitor api urls",
   Long: `url-monitor is used monitor api urls and some more detail stuff along with it`,
}
Enter fullscreen mode Exit fullscreen mode

Next important thing is init() function which is calling another method of cobra's AddCommand(). Here I added both the sub-commands I needed for my app checkUrlsCmd and checkStatusCmd. These are just variables so it doesn’t matter what you name them. To read more about go init() function check my previous blog.

func init() {
   rootCmd.AddCommand(checkUrlsCmd, checkStatusCmd)
}
Enter fullscreen mode Exit fullscreen mode

Finally I have the Execute() which if you remember was called in main.go, executing the command.Execute() method inside it and I am checking for error in return.

func Execute() {
   if err := rootCmd.Execute(); err != nil {
      log.Fatal(err)
   }
}
Enter fullscreen mode Exit fullscreen mode

Now I have to work on the actual sub-commands checkUrlCmd and checkStatusCmd which will be placed inside the commands folder.

check-url.go ➡ This command will be responsible to check the given urls/api endpoint and return successful on receiving status as 200 for it. It will take urls/api as argument to the sub-commands

package commands

import (
   log "github.com/sirupsen/logrus"
   "github.com/spf13/cobra"
)

var checkUrlCmd = &cobra.Command{
   Use:   "check-url",
   Short: "check-url",
   Long:  `check-url`,
   Run: func(cmd *cobra.Command, args []string) {
      if len(args) > 1{
         log.Fatal("subcommand check-url only take one argument as url")
      }
      if _, err := url.ParseRequestURI(args[0]); err != nil {
         log.Fatalf("wrong url type [%s]", err)
      }
      err := checkUrl(args)
      if err != nil {
         log.Fatal(err)
      }
   },
}

func checkUrl(args []string) error {
   fmt.Printf("HI!! From check-url sub-command with %s as argument", args[0])
   return nil
}
Enter fullscreen mode Exit fullscreen mode

Here I have variable checkUrlCmd type of &cobra.Command{} using the field like Use , Short and Long . As you can see that we have one more field here called Run which take an anonymous function as value and run another function checkUrl(). Function checkUrl() is responsible to perform all the checks and return an error if there are any. Run function are executed in following order. Learn more here

PersistentPreRun()
PreRun()
Run()
PostRun()
PersistentPostRun()
Enter fullscreen mode Exit fullscreen mode

All functions get the same args, the arguments after the

check-status.go ➡ This command will responsible to print the status payload of the url/apis based on the url/apis provided as argument.

package commands

import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var checkStatusCmd = &cobra.Command{
   Use:   "check-status",
   Short: "check-status",
   Long:  `check-status`,
   Run: func(cmd *cobra.Command, args []string) {
      if len(args) > 1{
         log.Fatal("subcommand check-url only take one argument as url")
      }
      if _, err := url.ParseRequestURI(args[0]); err != nil {
         log.Fatalf("wrong url type [%s]", err)
      }
      err := checkStatus(args)
      if err != nil {
         log.Fatal(err)
      }
   },
}

func checkStatus(args []string) error {
   fmt.Printf("HI!! From check-status sub-command with %s as argument", args[0])
   return nil
}
Enter fullscreen mode Exit fullscreen mode
Now let’s fetch all dependent package by running go mod init and then go mod tidy
>> go mod init url-monitor
go: creating new go.mod: module url-monitor
go: to add module requirements and sums: go mod tidy

>> go mod tidy
go: finding module for package github.com/spf13/cobra
go: finding module for package github.com/sirupsen/logrus
go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.8.1
go: found github.com/spf13/cobra in github.com/spf13/cobra v1.4.0
go: downloading github.com/inconshreveable/mousetrap v1.0.0
go: downloading github.com/stretchr/testify v1.2.2
go: downloading github.com/pmezard/go-difflib v1.0.0
Enter fullscreen mode Exit fullscreen mode

Cool!! We are ready to build the app by running go build . with the url-monitor folder. This should generate a binary file call url-monitor

url-monitor
├── commands
│   ├── check-status.go
│   ├── check-urls.go
│   ├── helpers.go
│   └── root.go
├── go.mod
├── go.sum
├── main.go
└── url-monitor                                   // the binary
Enter fullscreen mode Exit fullscreen mode

Lets try to run url-monitor binary and see what happens

>> ./url-monitor
url-monitor

Usage:
  url-monitor [command]

Available Commands:
  check-status check-status
  check-urls   check-urls
  completion   Generate the autocompletion script for the specified shell
  help         Help about any command

Flags:
  -h, --help   help for url-monitor

Use "url-monitor [command] --help" for more information about a command.
Enter fullscreen mode Exit fullscreen mode
Let run the first sub-command check-url
>> ./url-monitor check-urls https://example.com/apis/v1/get-status

HI!! From check-urls sub-command with https://example.com/apis/v1/get-status as argument
Enter fullscreen mode Exit fullscreen mode
Let run the second sub-command check-status
>> ./url-monitor check-status https://example.com/apis/v1/get-status

HI!! From check-status sub-command with https://example.com/apis/v1/get-status as argument
Enter fullscreen mode Exit fullscreen mode
Lets try a wrong URL
>> ./url-monitor check-status example.com/apis/v1/get-status

FATL 2022-03-25 12:43:07 wrong url type [parse "example.com/apis/v1/get-status": invalid URI for request]
Enter fullscreen mode Exit fullscreen mode

Here are few projects where Cobra has been used Github CLI, Docker (distribution), Etcd, GoReleaser, Helm, Kubernetes etc. The whole list can be found here

There are alternatives available for Cobra as well i.e. mitchellh/cli, go-flags, urfave/cli etc.

Hopefully this gives an idea about the Cobra package and how to use it.

Happy Coding!!

Top comments (0)