How to build a CLI tool with Go and Cobra
Tags: go, cobra, cli, tutorial
Published: 2021-04-13 14:26:14

_Originally posted on [divrhino.com](https://divrhino.com/articles/build-command-line-tool-go-cobra/)_

{% youtube -tO7zSv80UY %}

Go is very useful for building services and tools for productivity. CLI tools are extremely powerful. They are a great way to automate all sorts of different everyday tasks. And who doesn't need a Dadjoke at least once a day, right? We are going to learn how to build a little CLI tool that will use the [icanhazdadjoke](https://icanhazdadjoke.com/api) api to give us a random dad joke.

## Prerequisites

To follow along with this tutorial, you will need to have **Go** and **Cobra** installed.

Installation guides:

- [Go](https://golang.org/dl/)
- [Cobra generator](https://github.com/spf13/cobra/blob/master/cobra/README.md)

## Initialising the project

In the terminal, we can first create a new directory for our project. We can then immediately change into the new directory and generate a new app, giving it a package name. Usually, a package would be a url you own.

In this case, we've named it as a github repo. You can change the `example` to your own Github user name.

```bash
cd Sites
mkdir dadjoke
cd dadjoke
cobra init --pkg-name github.com/example/dadjoke
```

If we run the `ls` command in the terminal, we can see the files that the `cobra init` command created for us.

```bash
ls
```

We now have a license, a cmd folder and a main.go file

- LICENSE
- a `cmd` folder
- a `main.go` file

Cobra just uses the `main.go` file as an entry point. We won't be putting any of our `CLI application` code here. Instead, most of our code will be put in the `cmd` folder.

We will also want to use `Go modules` in our project, to handle our dependencies. We will run the `go mod init` command, in the terminal, to initialise `Go modules`. Here we are using the same package name we had used earlier when generating our `cobra` app.

```bash
go mod init github.com/example/dadjoke
```

This creates a `go.mod` file, which will help us manage our dependencies.

## Creating commands

If we run `go run main.go` in our terminal for the first time, all our dependencies will be installed and a `go.sum` file will also be created. This can be thought of as a `lock file`. It is used to verify that the `checksum` of dependencies have not changed. 

We will also see a print out about our CLI, including the description, usage and available commands. Right now, we only have the `help` command.

```bash
go run main.go
```

Cobra gives us some boilerplate content, including a description of what our app does. We should probably go and update this to use a description that better describes the dadjoke app we're building

Let's open up the `cmd/root.go` file and  and update the description of our newly-created `root` command. Replace the default content with your own `Short` and `Long` descriptions:

```go
var rootCmd = &cobra.Command{
	Use:   "dadjoke",
	Short: "Get random dad jokes in your terminal",
	Long:  `Dadjoke CLI is a tool that gives you a random dad joke`,
}
```

If we run our app now, `go run main.go`, we will see the description we just wrote. Currently, our app does not have any available commands to list.

So let's now create the `random` command. Cobra gives us the `add` command that allows us to do this, easily. In the terminal, make sure you're in your project root and run the following command:

```bash
cobra add random
```

The add command generates a new `cmd/random.go` file for us.

So if we run `go run main.go`, we will see that `random` is now one of our available commands. How cool is that?

```bash
go run main.go
```

If we run our `random` command right now, we'll see that it has some boilerplate description, just like the root command we saw previously. We will want to update this description too. Go into your `cmd/random.go` file and add a `Short` and `Long` description:

```go
var randomCmd = &cobra.Command{
	Use:   "random",
	Short: "Get a random dad joke",
	Long:  `This command fetches a random dad joke from the icanhazdadjoke api`,
	Run: func(cmd *cobra.Command, args []string) {
		...
	},
}
```

## The dadjoke API - curl

Let's take a look at the documentation for the API we will be consuming. We will be using the free [icanhazdadjoke API](https://icanhazdadjoke.com/api). This API doesn't require authentication. The creators are nice enough to let us use it for free. The only thing they're asking is that we add a custom `User-Agent` header. We can definitely do that.

![icanhazdadjoke API documentation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/odfvuufk6qv7uvnk6brc.png)

If we scroll down to the endpoints, we can see the cURL command. Let's run it in our terminal and see what we get.

```bash
curl -H "Accept: application/json" https://icanhazdadjoke.com/
```

Here we see that it returns an `ID`, a `joke` and a `status`. Let's quickly represent this in our code before we move on. Inside `cmd/random.go`, create a new `type Joke struct`:

```go
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var randomCmd = &cobra.Command{
	Use:   "random",
	Short: "Get a random dad joke",
	Long:  `This command fetches a random dad joke from the icanhazdadjoke api`,
	Run: func(cmd *cobra.Command, args []string) {
		...
	},
}

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

type Joke struct {
	ID     string `json:"id"`
	Joke   string `json:"joke"`
	Status int    `json:"status"`
}
```

## Get request in Go

Now let's try to make that API call in Go.

We will be doing most of our work in the `random.go` file. Right now, our `Run` function merely prints out a message. Let's create a function called `getRandomJoke`. We will call this function inside the `Run` method. And let's just print a message for now, just to see if it works.

In our `random.go` file, add a new `getRandomJoke()` method and call it from inside `Run`:

```go
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var randomCmd = &cobra.Command{
	Use:   "random",
	Short: "Get a random dad joke",
	Long:  `This command fetches a random dad joke from the icanhazdadjoke api`,
	Run: func(cmd *cobra.Command, args []string) {
		getRandomJoke()
	},
}

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

type Joke struct {
	ID     string `json:"id"`
	Joke   string `json:"joke"`
	Status int    `json:"status"`
}

func getRandomJoke() {
	fmt.Println("Get random dad joke :P")
}
```

If we run our `random` command in the terminal now, we will see our message from the `Println` on *line 25*

```bash
go run main.go random
```

## Looking at the http package
Next, let's create a function that will make a GET request to the API endpoint. We're going to use that to get our random joke data. We can use the `net/http` package to achieve this.

First things first, let's visit the `net/http` documentation to get a better idea of how we can use it. We can visit [https://golang.org/pkg/net/http/](https://golang.org/pkg/net/http/) and search for `func Get`. Since we know we want to make a `GET` request. Here, we see this line that says 

![http func GET documentation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ak0147qdrk1qxoiakp4w.png)

> To make a request with custom headers, use NewRequest and DefaultClient.Do.

If you remember, the API maintainers would like us to add a custom header to our app, so this is what we're looking for.

## The getJokeData() method

We will create a function that we can use to make `GET` requests to the [icanhazdadjoke API endpoint](https://icanhazdadjoke.com/api)

```go
package cmd

import (
	"fmt"

	"github.com/spf13/cobra"
)

var randomCmd = &cobra.Command{
	Use:   "random",
	Short: "Get a random dad joke",
	Long:  `This command fetches a random dad joke from the icanhazdadjoke api`,
	Run: func(cmd *cobra.Command, args []string) {
		getRandomJoke()
	},
}

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

type Joke struct {
	ID     string `json:"id"`
	Joke   string `json:"joke"`
	Status int    `json:"status"`
}

func getRandomJoke() {
	fmt.Println("Get random dad joke :P")
}

func getJokeData(baseAPI string) []byte {}
```

Inside the body of the `getJokeData()` function, we will create a new request using the `NewRequest()` method from the `net/http` package

```go
package cmd

import (
	"fmt"
	"net/http"
	"io/ioutil"

	"github.com/spf13/cobra"
)

var randomCmd = &cobra.Command{
	Use:   "random",
	Short: "Get a random dad joke",
	Long:  `This command fetches a random dad joke from the icanhazdadjoke api`,
	Run: func(cmd *cobra.Command, args []string) {
		getRandomJoke()
	},
}

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

type Joke struct {
	ID     string `json:"id"`
	Joke   string `json:"joke"`
	Status int    `json:"status"`
}

func getRandomJoke() {
	fmt.Println("Get random dad joke :P")
}

func getJokeData(baseAPI string) []byte {
	request, err := http.NewRequest(
		http.MethodGet, //method
		baseAPI,        //url
		nil,            //body
	)

	if err != nil {
		log.Printf("Could not request a dadjoke. %v", err)
	}

	request.Header.Add("Accept", "application/json")
	request.Header.Add("User-Agent", "Dadjoke CLI (https://github.com/example/dadjoke)")
}
```
New code explanations:
- Line 5
	- Import `net/http` package
- Line 6
	- Import `io/ioutil` package
- Line 35
	- Use the `http.NewRequest()` method to create a new request
- Line 36
	- First argument is an `HTTP` method
- Line 37
	- Second argument is a `url`
- Line 38
	- Third argument is a request body. Remember the comma at the end.
- Lines 41-43
	- Handle the error that is returned from `http.NewRequest()`
- Line 45
	- Add a header to tell the API we want our data returned as `JSON`
- Line 46
	- Add a custom `User-Agent` header to tell the API maintainers how we're using their API

The completed `getJokeData()` method:

```go
func getJokeData(baseAPI string) []byte {
	request, err := http.NewRequest(
		http.MethodGet, //method
		baseAPI,        //url
		nil,            //body
	)

	if err != nil {
		log.Printf("Could not request a dadjoke. %v", err)
	}

	request.Header.Add("Accept", "application/json")
	request.Header.Add("User-Agent", "Dadjoke CLI (https://github.com/example/dadjoke)")

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		log.Printf("Could not make a request. %v", err)
	}

	responseBytes, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Printf("Could not read response body. %v", err)
	}

	return responseBytes
}
```

New code explanations:
- Line 48
	- Pass the `request` to the `http.DefaultClient.Do()` method to get a `response`
- Lines 49-51
	- Handle error that is returned from `http.DefaultClient.Do()` method
- Line 53
	- Pass the `resonseBody` to the `ioutil.ReadAll()` to read it into `bytes`
- Lines 54-56
	- Handle error that is returned from `ioutil.ReadAll()` method
- Line 58
	- Return response as bytes


## Finishing the getRandomJoke() method

Let's re-visit our `getRandomJoke` method so we can use our `getJokeData` method.

```go
func getRandomJoke() {
	url := "https://icanhazdadjoke.com/"
	responseBytes := getJokeData(url)
	joke := Joke{}

	if err := json.Unmarshal(responseBytes, &joke); err != nil {
		fmt.Printf("Could not unmarshal reponseBytes. %v", err)
	}

	fmt.Println(string(joke.Joke))
}
```

New code explanations:
- Line 2
	- Store the API url in the `url` variable
- Line 3
	- Pass `url` into the `getJokeData()` method and store the returned reponse bytes in a variable
- Line 4
	- Create a new Joke struct. We will save data into this when we unmarshal the reponse
- Lines 6-8
	- Unmarshal the response, passing in `responseBytes` and `url` to `http.Unmarshal` as arguments
	- Also handle the error that is returned
- Line 10
	- Convert `joke.Joke` to a string and print it to the terminal


Let's go back to our terminal and run the command to get a random joke:

```bash
go run main.go random
```

## Conclusion

In this tutorial we learnt how to create a command-line application with Go and Cobra. In part 2, we will learn how to implement a flag for our random command.

If you enjoyed this article and you'd like more, consider following [Div Rhino on YouTube](https://www.youtube.com/c/DivRhino).

Congratulations, you did great. Keep learning and keep coding. Bye for now.

{% github divrhino/dadjoke %}