In the previous post, we've taken a look at the basics of creating CLI tools in Go using cobra library. This time we'll dive a bit deeper while creating something actually useful. We'll build a small Redis client that helps us to manage the state of our cache.

The story

Imagine that we have some kind of cache, where we store configurations for our clients. The database that has it persisted is pretty slow, so we decided to use Redis to make the requests a faster. From time to time we need to make some important changes to the data, and we'd like to see their effect immediately, so we need to clear the cache manually from time to time. Let's build a small app then, where we will be able to list the state of cache and delete specific key-value pairs.

Common code for commands

First of all, we need to realize that for each and every command we are going to execute, we will need an active connection to the database. It would be tedious to write it in each command's Run function, but fortunately, there is a way to have it defined in just a single place. That would be PersistentPreRun which calls its value function every time the user picks that command or any of its subcommands:

// cmd/root.go
...
var redisClient *redis.Client

var RootCmd = &cobra.Command{
    Use:              "cacher",
    PersistentPreRun: redisConnect,
}

func redisConnect(cmd *cobra.Command, args []string) {
    client, err := redis.NewClient(...)
    if err != nil {
        fmt.Println("✘ Failed to connect to Redis, configuration is not correct")
        os.Exit(1)
    }
    redisClient = client
}

Flag types and their storage

Another thing we need to figure out is how to handle flags that are being passed to our application. There are two types of flags:

  • persistent which are defined on a command and are applicable to all subcommands as well:

    RootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
    
  • local which are applicable only to a command they are defined on

    clearCmd.Flags().StringVarP(&client, "client", "c", "", "client ID")
    

Also, there are two ways of extracting the values from flags. You can either assign the value to a variable defined on package level using a pointer and one of the functions shown above. You may, however, extract the value in the Run manually:

clearCmd.Flags().StringP("client", "c", "", "client ID")
...
Run: func(cmd *cobra.Command, args []string) {
    clientID := cmd.Flag("client").Value.String()
    ...
},

Handling arguments

Last, but not least, we need to mention arguments and two things we should keep in mind when handling them.

First of all, we should validate the number of arguments that are passed. There are some cases where you may want to allow a custom number of them, but in the vast majority, you can make some basic checks here. This results in cobra taking care of that and showing a human-friendly error message, because you can use a predefined validator like MaximumNArgs(n), ExactArgs(n), etc. You can also define a custom function, as long as it adheres to a declaration of func(cmd *Command, args []string) error. For now, will stick to a predefined one:

Use:   "list",
...
// require at most one argument
Args:  cobra.MaximumNArgs(1),

Finally, we can go the second and final step, which is actually using the values. This one is much simpler since each command get a list of arguments (as string values) as one of the parameters. Now it all comes down to accessing them and doing your magic:

// cmd/list.go
Use:   "list",
...
Run: func(cmd *cobra.Command, args []string) {
    if len(args) == 1 {
        cacheKey := fmt.Sprintf("cache:clients:%s", args[0])
        ks, err := redisClient.Keys(cacheKey).Result()
        if err != nil {
            fmt.Println("Failed to list clients cache")
            os.Exit(1)
        }
        if len(ks) == 0 {
            fmt.Printf("Client %s is not present in cache\n", args[0])
            os.Exit(0)
        }
        fmt.Printf("Client %s is present in cache\n", args[0])
        os.Exit(0)
    }

    ks, _ := redisClient.Keys("cache:clients:*").Result()
    fmt.Println("Current cache summary:")
    fmt.Printf("‣ clients cached: %d\n", len(ks))

    if Verbose {
        for _, k := range ks {
            siteID := strings.Replace(k, "cache:clients:", "", -1)
            fmt.Printf("\t‣ %s\n", siteID)
        }
    }

},

Cacher in action

You can find a full source code of the example app, cacher, on Github.

In order to see what you could do with the tool, you can type:

$ cacher
Usage:
cacher [command]

Available Commands:
clear       Clears cache contents
help        Help about any command
list        Summary of cache contents

To see what is currently cached:

$ cacher list
Current cache summary:
‣ clients cached: 4

That might not be enough, we'd like to see exactly which clients are currently cached:

$ cacher list -v
Current cache summary:
‣ clients cached: 4
        ‣ clientB
        ‣ clientD
        ‣ clientA
        ‣ clientC

Since we changed some important attribute in clientC configuration, we'd like to remove the cached key for it:

$ cacher clear -c clientC
Clearing cached item of clientC... ✔
$ cacher list -v
Current cache summary:
‣ clients cached: 3
        ‣ clientB
        ‣ clientD
        ‣ clientA

Actually, let's clear everything:

$ cacher clear all -v
Clearing 3 clients cache entries
Clearing cached item of clientB... ✔
Clearing cached item of clientD... ✔
Clearing cached item of clientA... ✔
$  cacher list all
Current cache summary:
‣ clients cached: 0

Building apps with spf13/cobra again proved to be a pretty simple thing to do, that can bring impressive and useful results in no time.