Build a CLI app in Go

last updated: June 23, 2022

Go is a great language for building CLI apps, and there is a popular Go-module called Cobra that provides some powerful tools to quickly build a powerful CLI application.

There are many popular CLI projects that are built with Cobra, such as:

In this article we will create a simple CLI app using the Cobra module, we will cover some of the core concepts of CLI apps, and we will also show how to use the Cobra module to help us build one.

Getting started

Our CLI app will make a request to the haveibeenpwned password API to check if a password has been exposed in a data breach.

# Create a project folder
mkdir pwned
cd pwned

# initialize the project
go mod init pwned

# create folders in the project
mkdir cmd
mkdir pkg/pwned

# create initial files in the project
touch cmd/root.go
touch cmd/pw.go
touch pkg/pwned/pwned.go
touch main.go

# add Cobra as a dependency
go get -u github.com/spf13/cobra@latest

We have now initialized the project and created the project structure we can build on.

├── pwned
|   ├── cmd
|   │   ├── pw.go
|   │   └── root.go
|   |
|   ├── pkg
|   │   └── pwned
|   │       └── pwned.go
|   |
|   └── main.go

Using cobra we can define each command within our cmd folder, lets first create our root command.

cmd/root.go
package cmd

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

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "pwned",
	Short: "Have I Been Pwned",
}

// Execute adds all child commands to the root command and sets flags appropriately.
// It only needs to happen once to the rootCmd.
func Execute() {
	cobra.CheckErr(rootCmd.Execute())
}

Next we will add a command to the root command, this command will be used to check if a password has been exposed in a data breach. For now we will just print to the console that the command has been called.

cmd/pw.go
package cmd

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

// pwCmd represents the pw command
var pwCmd = &cobra.Command{
	Use:     "pw",
	Aliases: []string{"password"},
	Short:   "Check if a password has been exposed in a data breach.",
	Long:    `Check if a password has been exposed in a data breach, it will return the number of a times the provided password has been exposed.`,
	Example: `pwned pw <password>`,
	Run: func(cmd *cobra.Command, args []string) {

		if len(args) == 0 {
			fmt.Println("Please provide a password.")
			return
		}

		fmt.Println("pw command successfully called")
	},
}

func init() {
	rootCmd.AddCommand(pwCmd)
}

This will now allow us to run the command pwned pw <password> and it will print pw command successfully called to the console.

The important properties we defined are:

  • Use: This is the name of the command, this is what the user will type to call the command.
  • Aliases: This is a list of aliases for the command, this is useful if you want to call the command with a different name.
  • Short: This is a short description of the command, this will be shown when the user calls pwned help <command>.
  • Run: This is the function that will be called when the command is run, this is where we will do our work.

Create our API

Lets create an API that will check if a password has been exposed in a data breach, we will then hook this API to our CLI app.

pkg/pwned/pwned.go
package pwned

import (
	"crypto/sha1"
	"encoding/hex"
	"io/ioutil"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

// PwnedPassword returns the number of times the provided password has been exposed in a data breach.
func PwnedPassword(password string) (count int, err error) {

	h := sha1.New()
	h.Write([]byte(password))
	bs := h.Sum(nil)

	prefix := strings.ToUpper(hex.EncodeToString(bs)[:5])
	suffix := strings.ToUpper(hex.EncodeToString(bs)[5:])

	res, err := PwnedPasswordRange(prefix)
	if err != nil {
		return 0, err
	}

	count, err = FindSuffix(suffix, res)
	if err != nil {
		return 0, err
	}

	return count, nil

}

// FindSuffix finds the number of times the provided suffix has been found is a slice of PasswordRange.
func FindSuffix(suffix string, passwordRange []PasswordRange) (int, error) {
	for _, p := range passwordRange {
		if p.Suffix == suffix {
			return p.Count, nil
		}
	}
	return 0, nil
}

type PasswordRange struct {
	Suffix string
	Count  int
}

// PwnedPasswordRange returns a slice of PasswordRange for the provided prefix.
func PwnedPasswordRange(prefix string) (passwordRange []PasswordRange, err error) {
	u, err := url.Parse("https://api.pwnedpasswords.com/range/")
	if err != nil {
		return nil, err
	}

	u.Path += prefix

	res, err := ApiCall(u.String())

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {

	}
	defer res.Body.Close()

	// slice from new lines
	newLines := strings.Split(string(body), "\n")

	// split newLines by colon
	for _, line := range newLines {
		split := strings.Split(line, ":")
		if len(split) == 2 {
			count, err := strconv.Atoi(strings.TrimRight(split[1], "\r\n"))
			if err != nil {
				return nil, err
			}
			t := PasswordRange{split[0], count}

			passwordRange = append(passwordRange, t)
		}
	}

	return passwordRange, nil
}

// ApiCall makes a call to the provided url and returns the response.
func ApiCall(endpoint string) (*http.Response, error) {
	client := &http.Client{}

	u, err := url.Parse(endpoint)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", u.String(), nil)
	if err != nil {
		return nil, err
	}

	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	return res, nil
}

I wont go into the details of the API, but you can read more details on the Pwned Passwords API.

Now we need to add the PwnedPassword function to our CLI app:

cmd/pw.go
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"

	"pwned/pkg/pwned"
)

// pwCmd represents the pw command
var pwCmd = &cobra.Command{
	Use:     "pw",
	Aliases: []string{"password"},
	Short:   "Check if a password has been exposed in a data breach.",
	Long:    `Check if a password has been exposed in a data breach, it will return the number of a times the provided password has been exposed.`,
	Example: `pwned pw <password>`,
	Run: func(cmd *cobra.Command, args []string) {

		if len(args) == 0 {
			fmt.Println("Please provide a password.")
			return
		}

		password := args[0]

		passwordCount, err := pwned.PwnedPassword(password)

		if err != nil {
			fmt.Println(err)
			return
		}

		fmt.Printf("%s has been exposed %d times\n", password, passwordCount)
	},
}

func init() {
	rootCmd.AddCommand(pwCmd)
}

Now when we call the pw command with a provided password, it will return the number of times the password has been exposed in a data breach by calling our API.

We can give our CLI app a try:


go run main.go pw pass123

# it should return the number of times pass123 has been exposed in a data breach
> pass123 has been exposed XXX times

Summary

We now have built a CLI application with Go and Cobra, we only touched the surface of what the Cobra feature set has. It is a great module to build out a powerful CLI application, and there is also a Cobra Generator which can be used to generate some boilerplate code parts of the Cobra CLI application for you.

Source Code

If you want to view the source code, it is available on Github

Hi! I'm Niall McKenna

Technologist, Maker & Software Developer