DEV Community

Cover image for How to add flags to a CLI tool built with Go and Cobra
Div Rhino
Div Rhino

Posted on • Updated on

How to add flags to a CLI tool built with Go and Cobra

Originally posted on divrhino.com

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:

func init() {
    rootCmd.AddCommand(randomCmd)

    randomCmd.PersistentFlags().String("term", "", "A search term for a dad joke.")
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

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()
        }
    },
}
Enter fullscreen mode Exit fullscreen mode

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:

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

# random command with the term flag
go run main.go random --term=hipster
Enter fullscreen mode Exit fullscreen mode

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

We can run the example curl command in our terminal:

curl -H "Accept: application/json" "https://icanhazdadjoke.com/search?term=hipster"
Enter fullscreen mode Exit fullscreen mode

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

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

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.

func getJokeDataWithTerm(jokeTerm string) {

}
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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{}).

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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
}
Enter fullscreen mode Exit fullscreen mode

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

func getRandomJokeWithTerm(jokeTerm string) {
    _, results := getJokeDataWithTerm(jokeTerm)
    fmt.Println(results)
}
Enter fullscreen mode Exit fullscreen mode

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.

go run main.go random --term=hipster
Enter fullscreen mode Exit fullscreen mode

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

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "math/rand"
    "net/http"
    "time"

    "github.com/spf13/cobra"
)
Enter fullscreen mode Exit fullscreen mode

Then we can write a skeleton randomiser function:

func randomiseJokeList(length int, jokeList []Joke) {

}
Enter fullscreen mode Exit fullscreen mode

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:

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

func getRandomJokeWithTerm(jokeTerm string) {
    total, results := getJokeListWithTerm(jokeTerm)
    randomiseJokeList(total, results)
}
Enter fullscreen mode Exit fullscreen mode

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:

# 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
Enter fullscreen mode Exit fullscreen mode

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:

dadjoke random

# random joke with term
dadjoke random --term=hipster
Enter fullscreen mode Exit fullscreen mode

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.

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

GitHub logo divrhino / dadjoke2

Add flags to a CLI tool built with Go and Cobra. Video tutorial available on the Div Rhino YouTube channel.

Top comments (6)

Collapse
 
c4r4x35 profile image
Srinivas Kandukuri

Thanks for the detailed info.
I have followed and learn cobra basics from the above tutorial.

Keep doing good work.

github.com/srinivasKandukuri/DadJo...

Collapse
 
divrhino profile image
Div Rhino

Thanks for your comment! Glad you found this useful :)

Collapse
 
kentbull profile image
Kent Bull

Very nice tutorial. Helped me brush up on net/http, log, json, and Cobra.
Thank you.

Collapse
 
divrhino profile image
Div Rhino

Wonderful! So glad you found this tutorial useful 🙌

Collapse
 
dagheyman profile image
Dag Heyman Kajevic

Great tutorial!

However, there is a bug in the code. What happens when the API returns a list of jokes with only one entry?

Collapse
 
divrhino profile image
Div Rhino

Thank you for your comment! I'll look into it when I get a chance :)