Web development articles for project-based learning

This is part of a series of articles. Read the other parts here:

  1. Building a command line tool with Go and Cobra
  2. Adding flags to a command line tool built with Go and Cobra

Prerequisites

In this tutorial, we will learn how to add flags to a CLI tool built with Go and Cobra. 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:

1
2
3
4
5
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:

1
2
3
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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:

1
2
3
4
5
# 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 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
Image: icanhazdadjoke API documentation

We can run the example curl command in our terminal:

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

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

1
2
3
4
5
6
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.

1
2
3
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:

1
2
3
4
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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{}).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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:

1
2
3
4
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.

1
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net/http"
	"time"

	"github.com/spf13/cobra"
)

Then we can write a skeleton randomiser function:

1
2
3
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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:

1
2
3
4
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:

1
2
3
4
5
# 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:

1
2
3
4
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. You’ll be able to find all the code in the github repo.

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

Resources