Applied Go - Creating a CLI application


Practice makes permanent - Bobby Robson

When learning a new programming language or any technology for that matter, we need to practice what we learn, and that it when it becomes permanent. I am starting this new series of posts to publish different things I tried/built to make sure what I learned – through different Go programming books, video courses, and community content – stays with me permanently.

The first one in this series is about using Cobra package to build a CLI application.

What you will learn today?

  • Using Cobra package to add command line parameters
  • The typical program structure when using Cobra package to create a CLI application
  • Converting command line arguments to the relevant data type using strconv package.

What is Cobra?

Cobra is the most popular and most powerful library for creating modern CLI applications. kubectl, docker, hugo, gh, and several other popular command line tools use Cobra library. Cobra lets us create CLI tools with nested subcommands, global, local and cascading flags among many other powerful features. Talk is cheap, so let us get started with a tiny command line calculator project to understand how to use the Cobra package. This may not be a useful utility but helps in understanding the basics of implementing a CLI application. We can build on this later by looking at more advanced use cases.

Creating a project

To start with, let us create the folder structure we need to build this project.

1
2
3
4
5
6
7
8
9
$ mkdir gcalc && cd gcalc

$ go mod init github.com/rchaganti/gcalc

$ touch main.go
$ mkdir -p cmd/gcalc
$ mkdir -p pkg/gcalc

$ go get -u github.com/spf13/cobra@latest

The main project folder is gcalc. We, then, run go mod init to initialize a module within this project folder. I have used a fully-qualified path to the GitHub repo but that is not necessary. The repo need not even exist if we don’t intend to share this external world. Finally, to complete the folder structure, we created two folders – cmd/gcalc for storing all command/subcommand logic and pkg/gcalc for storing the actual arithmetic functions that we will soon implement. The go get command at the end pulls Cobra package and stores it locally.

When using Cobra, all commands get structure as a tree with one root. So, the first thing we need to do is create the root command.

1
$ touch cmd/gcalc/root.go

Within root.go, we will add the root of the command structure that we need to implement. You can copy/paste the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package gcalc

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "gcalc",
	Short: "gcalc - a commandline calculator for basic arithmetic",
	Long: `gcalc is a simple yet powerful command line calculator

	You can use gcalc for quick calculations at the command line.`,
	Run: func(cmd *cobra.Command, args []string) {},
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintf(os.Stderr, "Ooops. There was an error while executing your CLI '%s'", err)
		os.Exit(1)
	}
}

In Cobra, every command that we need to add is represented using cobra.Command struct. This struct has many fields, but, for now, let us limit it to minimum needed to implement our calculator.

Tip: Always look at the package / library source code to understand the package better. This is not just better than documentation but also a good way to learn new techniques.

Within root.go, we start by declaring and initializing rootCmd variable. The Use, Short, and Long fields are related to the command help. The Run field is what identifies what happens when you invoke this command. Since this the root of the command tree, the Execute() function gets invoked in the main function. Let us see how the main.go looks like.

1
2
3
4
5
6
7
package main

import "github.com/rchaganti/gcalc/cmd/gcalc"

func main() {
	gcalc.Execute()
}

At this point in time, our CLI is ready for a trial.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ go run main.go --help
gcalc is a simple yet powerful command line calculator

        You can use gcalc for quick calculations at the command line.

Usage:
  gcalc [flags]

Flags:
  -h, --help   help for gcalc

Now that we have the CLI root command working well, we can start adding the commands for basic arithmetic. But, before that, let us add the functions that actually perform the arithmetic. We will do this in the pkg/gcalc/gcalc.go.

1
$ touch pkg/gcalc/gcalc.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package gcalc

import (
	"fmt"
	"strconv"
)

func Add(args []string) (sum int) {
	for _, i := range args {
		realInt, err := strconv.Atoi(i)
		if err != nil {
			fmt.Println(err)
		}
		sum += realInt
	}
	return sum
}

func Subtract(args []string) (diff int) {
	num1, err := strconv.Atoi(args[0])
	if err != nil {
		fmt.Println(err)
	}

	num2, err := strconv.Atoi(args[1])
	if err != nil {
		fmt.Println(err)
	}

	return num1 - num2
}

func Multiply(args []string) (product int) {
	product = 1
	for _, i := range args {
		realInt, err := strconv.Atoi(i)

		if err != nil {
			fmt.Println(err)
		}
		product *= realInt
	}

	return product
}

func Divide(args []string) (dividend int) {
	num1, err := strconv.Atoi(args[0])
	if err != nil {
		fmt.Println(err)
	}

	num2, err := strconv.Atoi(args[1])
	if err != nil {
		fmt.Println(err)
	}

	return num1 / num2
}

In this package, we define four functions – Add, Subtract, Multiply, and Divide. This is our business logic. Each function takes a slice of strings as input and return an integer value based on the arithmetic the function implements. Each function takes a slice of strings since that is what we get from the command line. Therefore, we need to convert the values to integer and we do that using the strconv.Atoi() function. The Add and Multiply functions can operate on any number of arguments but the Subtract and Divide functions operate on two arguments only.

Our package with business logic is ready. But, to consume these functions, we need the commands within our command tree. We can add additional commands by using multiple .go files under cmd/gcalc folder.

1
2
3
4
$ touch cmd/gcalc/add.go
$ touch cmd/gcalc/subtract.go
$ touch cmd/gcalc/multiply.go
$ touch cmd/gcalc/divide.go

While you can have all command registrations in a single file, I believe this a better and clean approach I have seen many CLIs implement.

Let us populate each of these files in the same order.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package gcalc

import (
	"fmt"

	"github.com/rchaganti/gcalc/pkg/gcalc"
	"github.com/spf13/cobra"
)

var addCmd = &cobra.Command{
	Use:     "add",
	Aliases: []string{"a"},
	Short:   "Adds a bunch of integers",
	Run: func(cmd *cobra.Command, args []string) {
		res := gcalc.Add(args)
		fmt.Println(res)
	},
}

func init() {
	rootCmd.AddCommand(addCmd)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package gcalc

import (
	"fmt"

	"github.com/rchaganti/gcalc/pkg/gcalc"
	"github.com/spf13/cobra"
)

var subtractCmd = &cobra.Command{
	Use:     "subtract",
	Aliases: []string{"s"},
	Short:   "Subtracts two integers",
	Args:    cobra.ExactArgs(2),
	Run: func(cmd *cobra.Command, args []string) {
		res := gcalc.Subtract(args)
		fmt.Println(res)
	},
}

func init() {
	rootCmd.AddCommand(subtractCmd)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package gcalc

import (
	"fmt"

	"github.com/rchaganti/gcalc/pkg/gcalc"
	"github.com/spf13/cobra"
)

var multiplyCmd = &cobra.Command{
	Use:     "multiply",
	Aliases: []string{"m"},
	Short:   "Multiplies a bunch of integers",
	Run: func(cmd *cobra.Command, args []string) {
		res := gcalc.Multiply(args)
		fmt.Println(res)
	},
}

func init() {
	rootCmd.AddCommand(multiplyCmd)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package gcalc

import (
	"fmt"

	"github.com/rchaganti/gcalc/pkg/gcalc"
	"github.com/spf13/cobra"
)

var divideCmd = &cobra.Command{
	Use:     "divide",
	Aliases: []string{"d"},
	Short:   "Divides two integers",
	Args:    cobra.ExactArgs(2),
	Run: func(cmd *cobra.Command, args []string) {
		res := gcalc.Divide(args)
		fmt.Println(res)
	},
}

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

The content of all these files is more or less same but there are a couple of things we should note.

Using cobra.ExactArgs() method, we can restrict how many command line arguments a user can pass to the command. This is useful within both subtract and divide commands. The second thing we should note here is the init() function in each of these files. This function gets invoked first and helps register the command in the command tree.

Here is how the final program folder structure should be once you add the remaining command program files.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ tree .
.
├── cmd
│   └── gcalc
│       ├── add.go
│       ├── divide.go
│       ├── multiply.go
│       ├── root.go
│       └── subtract.go
├── go.mod
├── go.sum
├── main.go
└── pkg
    └── gcalc
        └── gcalc.go

4 directories, 9 files

Alright! Time for another test ride.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
$ go run main.go --help
gcalc is a simple yet powerful command line calculator

        You can use gcalc for quick calculations at the command line.

Usage:
  gcalc [flags]
  gcalc [command]

Available Commands:
  add         Adds a bunch of integers
  completion  Generate the autocompletion script for the specified shell
  divide      Divides two integers
  help        Help about any command
  multiply    Multiplies a bunch of integers
  subtract    Subtracts two integers

Flags:
  -h, --help   help for gcalc

Use "gcalc [command] --help" for more information about a command.

This is good. We have the command help printed as we expect. Let us test each command.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ go build -o gcalc main.go 

$ ./gcalc add 1 2 3 4 5
15

$ ./gcalc subtract 1 2 3 4 5
Error: accepts 2 arg(s), received 5
Usage:
  gcalc subtract [flags]

Aliases:
  subtract, s

Flags:
  -h, --help   help for subtract

Ooops. There was an error while executing your CLI 'accepts 2 arg(s), received 5'

$ ./gcalc subtract 1 2
-1

$ ./gcalc multiply 1 2 3 4
24

$ ./gcalc divide 4 2
2

What if we pass float values instead of integers?

1
2
3
4
$ ./gcalc add 1 2.0 3.1 5.5
strconv.Atoi: parsing "2.0": invalid syntax
strconv.Atoi: parsing "3.1": invalid syntax
strconv.Atoi: parsing "5.5": invalid syntax

As expected. Our calculator can handle only integers. One method to address this is to consider input as floating point values and return floating point values. Here is how we can update the business logic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package gcalc

import (
	"fmt"
	"strconv"
)

func Add(args []string) (sum float64) {
	for _, i := range args {
		realValue, err := strconv.ParseFloat(i, 64)
		if err != nil {
			fmt.Println(err)
		}
		sum += realValue
	}
	return sum
}

func Subtract(args []string) (diff float64) {
	num1, err := strconv.ParseFloat(args[0], 64)
	if err != nil {
		fmt.Println(err)
	}

	num2, err := strconv.ParseFloat(args[1], 64)
	if err != nil {
		fmt.Println(err)
	}

	return num1 - num2
}

func Multiply(args []string) (product float64) {
	product = 1
	for _, i := range args {
		realValue, err := strconv.ParseFloat(i, 64)

		if err != nil {
			fmt.Println(err)
		}
		product *= realValue
	}

	return product
}

func Divide(args []string) (divident float64) {
	num1, err := strconv.ParseFloat(args[0], 64)
	if err != nil {
		fmt.Println(err)
	}

	num2, err := strconv.ParseFloat(args[1], 64)
	if err != nil {
		fmt.Println(err)
	}

	return num1 / num2
}

With this in place, our calculator can now take both floats and integers or a mix of both types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ go build -o gcalc main.go 

$ ./gcalc add 1 2.0 3.1 5.5
11.6

$ ./gcalc subtract 1.1 2
-0.8999999999999999

$ ./gcalc multiply 1 4
4

$ ./gcalc divide 3 4
0.75

What we have seen so far is a simple implementation of a CLI using Cobra library. You can add global and local flags and nest commands within commands. We shall see that in a future post with when we build something useful than just a calculator. Stay tuned.

Share on: