How to add flags to a CLI tool built with Go and Cobra
Tags: go, cli, cobra, tutorial
Published: 2021-04-13 14:50:59

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

{% youtube kT7Z02bR1IY %}

## Prerequisites

In the first part of this tutorial series, we created a dad joke command line application that gives us a random dad joke right in our terminal. In this second part, we will be learning how we can use a flag to retrieve dad jokes that contain a specific term.

If you did not follow the first tutorial, but would like to follow along with this tutorial, you can use the finished code from the first part as your starting point. You can find that in the repo for the first tutorial.

## Adding a flag

If you're familiar with command-line tools, you will recognise the use of flags. A flag provides a way for the user to modify the behaviour of a command. In this tutorial, we will be modifying the `random` command to allow us to search for a random joke that includes a specific `term`.

Cobra has two types of flags: 

- Persistent flags - available to the command it is assigned to, as well as all its sub-commands
- Local flags - only assigned to a specific command

Our example is small enough that this distinction does not have a real impact. We will, however, choose to apply a persistent flag because in an imaginary future, we'd like all sub-commands of `random` to be able to take in a `term`.

In the `init` function of our `cmd/random.go` file, we can add a persistent flag. We have named it `term` and given it a description:

```go
func init() {
	rootCmd.AddCommand(randomCmd)

	randomCmd.PersistentFlags().String("term", "", "A search term for a dad joke.")
}
```

That's all it takes to add a flag to a command. There are several ways we can make use of this flag. Here's how we will approach it:

- First we check for the presence of a term
- If the `random` command was run with the `term` flag, we will run a function that knows how to handle search terms
- But if the `random` command is run without any flags, we will run another function that merely returns a random joke, without knowing anything about search terms. This function is called `getRandomJoke()` and was created in the previous tutorial

Let's put together the initial skeleton of the function we need to handle search terms. In `cmd/random.go`, we will add this new function. For now, all it does is print out the search term to the terminal. We will build up the function as we proceed:

```go
func getRandomJokeWithTerm(jokeTerm string) {
	log.Printf("You searched for a joke with the term: %v",  jokeTerm)
}
```

Now we can move on to check whether the user has used the `term` flag. In the `Run` function of our `randomCmd`, we can add the following check:

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

		if jokeTerm != "" {
			getRandomJokeWithTerm(jokeTerm)
		} else {
			getRandomJoke()
		}
	},
}
```

What's essentially happening here is:

- We are getting the value from our `term` flag and storing it in the variable `jokeTerm`
- If the `jokeTerm` value is not empty, we will run our `getRandomJokeWithTerm` method and pass the `jokeTerm` value in to it
- Else if `jokeTerm` is empty, we'll just get a random joke by running `getRandomJoke`. Again, this was a function we created in the previous article.

Now we can go into our terminal and run the random command with and without the `term` flag:

```bash
# random command without the flag
go run main.go random

# random command with the term flag
go run main.go random --term=hipster
```

## Understanding the response

As a reminder, we are using the [icanhazdadjoke API](https://icanhazdadjoke.com/api) for all our jokes data. If we take a look at the API documentation, we can see that we can pass a search term to our request

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



We can run the example curl command in our terminal:

```bash
curl -H "Accept: application/json" "https://icanhazdadjoke.com/search?term=hipster"
```

We can represent the JSON response as a struct in our code:

```go
type SearchResult struct {
	Results    json.RawMessage `json:"results"`
	SearchTerm string          `json:"search_term"`
	Status     int             `json:"status"`
	TotalJokes int             `json:"total_jokes"`
}
```

## Get data with search term

Now that we have a better understanding of the data that we'll be working with, let's move on and create a method get the data.

First we will need to create a skeleton method. To begin, we just want to be able to pass a search term to which we can pass in to the API call.

```go
func getJokeDataWithTerm(jokeTerm string) {
	
}
```

In the previous article, we had already created a method that helped us to get joke data. We even creatively called it `getJokeData()`. This method takes in an API `url` as it's only argument. Within the body of our newly created `getJokeDataWithTerm` function, we can add the following lines:

```go
func getJokeDataWithTerm(jokeTerm string) {
	url := fmt.Sprintf("https://icanhazdadjoke.com/search?term=%s", jokeTerm)
	responseBytes := getJokeData(url)
}
```

Then we want to unmarshal the returned `responseBytes`, following the shape of the `SearchResult{}` struct

```go
func getJokeDataWithTerm(jokeTerm string) {
	url := fmt.Sprintf("https://icanhazdadjoke.com/search?term=%s", jokeTerm)
	responseBytes := getJokeData(url)
	
	jokeListRaw := SearchResult{}

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

Unmarshalling the `responseBytes` gives us the `Results` as `json.RawMessage`. We will have to unmarshal this raw JSON, following the shape of `Joke{}`, which is a struct we had created in the first article.

Our `Results` may often contain more than one joke. We can store all the jokes in a `[]Joke{}` (slice of `Joke{}`).

```go
func getJokeDataWithTerm(jokeTerm string) {
	url := fmt.Sprintf("https://icanhazdadjoke.com/search?term=%s", jokeTerm)
	responseBytes := getJokeData(url)

	jokeListRaw := SearchResult{}

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

	jokes := []Joke{}
	if err := json.Unmarshal(jokeListRaw.Results, &jokes); err != nil {
		log.Printf("Could not unmarshal reponseBytes. %v", err)
	}
}
```

Next, we'll want to return all the jokes we process using this method. We also want to know how many jokes we got back as well. We can update the function to be able to return these two values:

```go
func getJokeDataWithTerm(jokeTerm string) (totalJokes int, jokeList []Joke) {
	url := fmt.Sprintf("https://icanhazdadjoke.com/search?term=%s", jokeTerm)
	responseBytes := getJokeData(url)

	jokeListRaw := SearchResult{}

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

	jokes := []Joke{}
	if err := json.Unmarshal(jokeListRaw.Results, &jokes); err != nil {
		log.Printf("Could not unmarshal reponseBytes. %v", err)
	}

	return jokeListRaw.TotalJokes, jokes
}
```

Now that our `getJokeDataWithTerm` function is fleshed out, we can use it within our `getRandomJokeWithTerm` function. Replace the contents as follows:

```go
func getRandomJokeWithTerm(jokeTerm string) {
	_, results := getJokeDataWithTerm(jokeTerm)
	fmt.Println(results)
}
```

For the time being, we throw away the `totalJoke` value by using an underscore (`_`). We are only doing this for demonstration purposes, so fret not, we will use it in the next section.

## Randomising the search results

If we head into the terminal now and test our command, we just keep getting all the search results.

```bash
go run main.go random --term=hipster
```

This isn't really what we're going for. We want to be able to get one *random* joke that contains a specified search *term* each time we run the command with the flag. We can achieve this by introducing a function to randomise our results.

First, though, let's import a couple of packages
```go
import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net/http"
	"time"

	"github.com/spf13/cobra"
)
```

Then we can write a skeleton randomiser function:
```go
func randomiseJokeList(length int, jokeList []Joke) {

}
```

Our `randomiseJokeList` function takes in 2 arguments:

- `length` - to be used as the ceiling for our random range
- `jokeList` - the data we want to randomise

We can update our `randomiseJokeList` method with the following code:

```go
func randomiseJokeList(length int, jokeList []Joke) {
	rand.Seed(time.Now().Unix())

	min := 0
	max := length - 1

	if length <= 0 {
		err := fmt.Errorf("No jokes found with this term")
		fmt.Println(err.Error())
	} else {
		randomNum := min + rand.Intn(max-min)
		fmt.Println(jokeList[randomNum].Joke)
	}
}
```

Here's what's going on in the above code snippet:

- We are getting a random value within a range
- If the number of jokes is less than or equal to zero, we let the user know that we weren't able to find any jokes with that term
- But if there are jokes present, then we print out a random one

With our newly created `randomiseJokeList` function all complete, we can return to our `getRandomJokeWithTerm` function and update it:

```go
func getRandomJokeWithTerm(jokeTerm string) {
	total, results := getJokeListWithTerm(jokeTerm)
	randomiseJokeList(total, results)
}
```

If we go into our terminal and test our flag now, we will be able to get a random dad joke that contains a specified term:

```bash
# get one random joke that contains the term "hipster"
go run main.go random --term=hipster

# get one random joke that contains the term "apple"
go run main.go random --term=apple
```

## Distributing your CLI tool

Everybody likes a good dad joke from time to time, so I'm sure your friends will want to use your tool. We will be able to distribute this dadjoke CLI tool as a `Go` package. In order to do this:

- Upload your code to a public repo
- Install it by running `go get <link-to-your-public-repo` e.g. `go get github.com/example/dadjoke`

Then you will be able to run your tool like this:

```bash
dadjoke random

# random joke with term
dadjoke random --term=hipster
```

## Conclusion

In this tutorial we learnt how to extend our dadjoke CLI tool so we could 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/dadjoke2 %}