How to Implement Verbose Mode in a Go CLI App Using the Standard Library

A step-by-step guide to adding verbose mode to your Go CLI app using the standard library.

How to Implement Verbose Mode in a Go CLI App Using the Standard Library

A lot of CLI tools provide a verbose mode to get logs with additional information, which is helpful for scenarios when something breaks or when a user wants to understand in more detail what is happening in the background.

It is an essential part of the standard experience expected of a CLI app.

One such example is the cURL command. Following is a gif of the difference in output when using the curl command with the verbose flag and without.

asciicast

Today we'll try to see how to implement such a feature in a CLI app using Go.

Defining the feature

Let's define the flag we are going to implement

  • It should work for all commands and subcommands in the application

  • When provided, it should show additional logs of all the commands, in addition, to the output that is shown by default.

  • It should be easy to maintain, every command or every log statement must not need to handle or know about this flag

Luckily for us, Go has amazing standard packages we can compose to create this effect with ease.

Tres Amigos

We will use three packages to accomplish this.

  1. flag: We will use it to easily parse the arguments provided by the user

  2. log: We will use the standard package to log stuff

  3. io: We will utilise a very special Writer provided by this package.

flag

The flag parsing works in three steps which need to be followed in the exact same order

  1. Define the expected flags

  2. Invoke the Parse() method

  3. Start accessing the values

func main() {
    // Define the flag that is expected 
    // We can pass the name, a default value & a description
    // It will return a pointer which is guranteed to have value 
    // Only after flag.Parse is called
    verboseP := flag.Bool("v", false, "Enable verbosity")

    // From the docs: Must be called after all flags are defined 
    // and before flags are accessed by the program.
    flag.Parse()

    // Access the value after 
    isVerbose := *verboseP
}

log

Log package contains a function called SetOutput which can be set to anything that implements the Writer interface.

We can use this to redirect all the logs to a specific writer.

log.setOutput(SomeWriter)

io

IO will provide us with the last missing piece, Writer which does nothing.

Basically discards any logs sent to it. It's called io.Discard

Putting it all together

package main

import (
    "flag"
    "fmt"
    "io"
    "log"
)

func main() {
    // Define the flags
    verboseP := flag.Bool("v", false, "Enable verbosity")

    // Invoke the parser
    flag.Parse()

    // Access it 
    isVerbose := *verboseP

    // If verbosity is not required discard all upcoming logs
    if !isVerbose {
        log.SetOutput(io.Discard)
    }

    fmt.Println("Hello world!")
    log.Println("I'm tired of printing this")
}

asciicast

Try it on repl.it