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.")
}
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 theterm
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 calledgetRandomJoke()
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)
}
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()
}
},
}
What's essentially happening here is:
- We are getting the value from our
term
flag and storing it in the variablejokeTerm
- If the
jokeTerm
value is not empty, we will run ourgetRandomJokeWithTerm
method and pass thejokeTerm
value in to it - Else if
jokeTerm
is empty, we'll just get a random joke by runninggetRandomJoke
. 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
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
We can run the example curl command in our terminal:
curl -H "Accept: application/json" "https://icanhazdadjoke.com/search?term=hipster"
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"`
}
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) {
}
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)
}
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)
}
}
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)
}
}
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
}
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)
}
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
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"
)
Then we can write a skeleton randomiser function:
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:
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:
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:
# 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:
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.
Congratulations, you did great. Keep learning and keep coding. Bye for now.
divrhino / dadjoke2
Add flags to a CLI tool built with Go and Cobra. Video tutorial available on the Div Rhino YouTube channel.
Dadjoke CLI Tool - Part 2: Adding flags
- Text tutorial: https://divrhino.com/articles/add-flags-to-command-line-tool-go-cobra/
- Video tutorial: https://www.youtube.com/watch?v=kT7Z02bR1IY
Top comments (6)
Thanks for the detailed info.
I have followed and learn cobra basics from the above tutorial.
Keep doing good work.
github.com/srinivasKandukuri/DadJo...
Thanks for your comment! Glad you found this useful :)
Very nice tutorial. Helped me brush up on net/http, log, json, and Cobra.
Thank you.
Wonderful! So glad you found this tutorial useful 🙌
Great tutorial!
However, there is a bug in the code. What happens when the API returns a list of jokes with only one entry?
Thank you for your comment! I'll look into it when I get a chance :)