How to print JSON output from your CLI

JSON Output Mode

C01t has been hard at work building the command line app for project Feijoa. He has taken great care to include all the tips from Naomi and Sam into the app. First of all, C01t chose Go as the programming language. He is planning to release the CLI app for all 3 major desktop operating system: Windows, Linux and OS X. In addition, he picked the spf13/cobra library to speed up development. C01t also decided to support human readable and JSON output from the start for every command.

Finally, C01t has been writing docs and READMEs to capture his coding choices along the way. So today he is documenting the command output pattern.

Command output

Most command line apps focus on producing human readable output. But having machine readable output, such as JSON output, is just as important. Machine readable output unlocks additional use cases, eg. scripting of the CLI, and improves testability. Furthermore, we aim to build helpers for rendering machine readable output from every command.

Output mode flag

To control how the CLI renders output, we define an “output mode” flag. Furthermore, we aim to implement the flag consistently across all commands.


const (
	// OutputModeFlag is the name for the flag that lets user specify the
	// desired output mode.
	OutputModeFlag = "output"

	// OutputModeFlagShorthand is the shorthand value for the OutputModeFlag.
	OutputModeFlagShorthand = "o"
)

In addition, we define the set of values for the flag. We start with human readable output, “standard”, and JSON output. In the future, we can extend the values list easily to support new output modes.


// Valid values for OutputModeFlag.
const (
	OutputModeStandard = "standard"
	OutputModeJSON = "json"
)

Finally, we add the output mode flag as a persistent flag to the root command. As a result, the flag can be set on every command.


rootCmd.PersistentFlags().StringP(
	OutputModeFlag,
	OutputModeFlagShorthand,
	OutputModeStandard,
	fmt.Sprintf(
		"Command output mode. One of: %s, %s", OutputModeStandard, OutputModeJSON),
)

The action function

First of all, each command must collect the data to be rendered in machine readable format. So action functions return both a result struct and error. The signature of an action function could look something like:


func MyAction(cmd *cobra.Command, args []string) (ActionResult, error)

The result struct serves as the collector of data produced as the action is executed. In addition, we use struct tags control the serialization of the results.


type ActionResult struct {
	Message string `json:"msg,omitempty"`
}

Finally, we can emit human readable output inside the action function body. The only limitation is that we must write all output to the writer attached to the cmd param:


fmt.Fprint(cmd.OutOrStdout(), "Hello world")

Since, the app displays information as a command is running, it fells more responsive and interactive. However, we can suppress this output by setting the writer on the cmd param to ioutil.Discard. Doing so keeps human readable output from interfering with the machine readable output.

The helpers

So far we have we have seen the base building blocks for implementing JSON output. Therefore it is now time to see how we tie it all together.

First we create a functions to map the action function we defined previously to the action function signatures expected for the spf13/cobra framework. The code sample below implements a function to map to the RunE action function:


type (
	// RunEAction defines the function signature for the action to be wrapped by
	// RunE.
	RunEAction func(cmd *cobra.Command, args []string) (interface{}, error)
)

func RunE(
	action RunEAction,
) (func(cmd *cobra.Command, args []string) error) {
	return func(cmd *cobra.Command, args []string) error {
		return runE(action, cmd, args)
	}
}

Most noteworthy in the code above is the runE function. The function determines the active output mode. Then it invokes helpers to execute the action and produce the output.


func runE(action RunEAction, cmd *cobra.Command, args []string) error {
	mode, err := cmd.Flags().GetString(OutputModeFlag)
	if err != nil {
		return errors.Wrap(err, "failed to determine output mode")
	}

	switch mode {
	case OutputModeStandard:
		err = runEStandard(action, cmd, args)
	case OutputModeJSON:
		err = runEJSON(action, cmd, args)
	default:
		err = errors.Errorf("unsupported output mode: '%s'", mode)
	}
	return err
}

runEStandard just calls the action function. runEJSON sets the output writer on cmd to ioutil.Discard. Then executes the action function. And renders the result as JSON.

Finally, we declare a command as follows:


cmd := &cobra.Command{
	Use: "my-cmd",
	RunE: RunE(MyAction),
}

Coda

The Green GUAVA cookiecutter-go template has a full implementation of the helpers described. Use the template to build CLI apps with a variety of output modes.


Image by: unsplash-logoJustin Peralta

Exploring the basic anatomy of a CLI

Explore the basic anatomy of a CLI

Our friend C01t is riding the elevator to the 15th floor for a meeting with Naomi, one of the system admins in charge of Guava Orchard’s IT infrastructure. After settling in as a member of project Feijoa, C01t has been tasked with building a command line app. Admins will use the tool to configure, maintain and troubleshoot the Feijoa service. He has never built a CLI before. Furthermore, no one on the team has a clear idea of what the requirements for the tool are. Therefore, Kaya suggested he meet with Naomi to get her input on the requirements.

“Why don’t I show you the Tamarillo command line app”, Naomi proposes once they are settled in the conference room. “Sam and I think its easily one of the best CLIs we use”, she continues nodding at the system admin sitting across from her. “I run Linux. Sam runs windows. Yet both of us can use the Tamarillo CLI from our desktops.” Then Naomi demos the intuitive command structure and contextual help feature of the app.

Next Sam opens one of the bash scripts they wrote to provision new users. To his surprise, C01t notices that the script invokes the same app that Naomi just demoed. “Scripting this tool was easy and straight forward. That capability alone simplifies our jobs a ton”, explains Sam.

“How long have you been using this app?”, asks C01t. “When did we deploy Tamarillo? Maybe 15 months ago”, replies Naomi. “That sounds about right”, confirms Sam. “And you know what has been truly amazing? In these 15 months they have released 22 updates to the command line app. That’s roughly a new version every 3 weeks. And not once did they break or regress existing functionality”, Naomi explains. “That is amazing”, agrees C01t. “They must have a huge army of testers.” Naomi gives him a knowing smile. “As a matter of fact 2 developers spend 20% of their time developing the app. That’s it. There is not army of testers. Automated tests validate all functionality”, she explains still smiling.

The anatomy of a CLI

Naomi and Sam introduced C10t to a well written CLI. The features they highlighted provide an excellent road-map for teasing out the characteristics of a good command line app.

Deliver portability

Our primary goal when developing a CLI is to make it available on all platforms that our customers use. Furthermore, we aim to make installation of the CLI as simple as possible. Therefore, go is our preferred language for the task.

With go you can write the code once and run it everywhere. Additionally, all go programs compile into binary executables. Since version 1.5 go has built-in support for cross-compiling. Check out The Go Cookbook for instructions on how to cross compile your code. In addition, you can find the list of supported compile targets in the official docs.

spf13/cobra and urfave/cli are 2 widely used libraries for building command line apps in the vein of the git command. Both provide a declarative way to define the command and sub-commands. Furthermore, both will handle the parsing and extraction of flags and command arguments. And both implement a contextual help system that enable you to build self documenting apps. Overall the libs are very similar. So you can confidently choose either one.

Design for usability

We aim to make the command line app easy to use. Therefore, we need to be mindful of how commands are organized and how complex the user input is.

Command structure

At the core of every CLI app there are 2 concepts: actions and entities those actions apply to. By organizing our command structure around these 2 concepts, we can build an intuitive self documenting application.

Consequently we can chose from 2 different approaches. The first approach is to build an action centric command structure. In this model the primary commands represent actions and the secondary commands are the entities. To illustrate this approach consider what command to start a service for the Tamarillo app would look like:


> tamarillo start service [args ...]

Another option is to build an entity centric command structure. In this case the primary commands all represent entities and the secondary commands are the actions. Now the same command to start a service would look like:


> tamarillo service start [args ...]

So how do we decide which structure to apply? First we consider the symmetry of the actions. Do all or a plurality of the actions apply to a plurality of the entities? Next we consider the cardinality of the 2 concepts. Does our domain have a large number of actions but only a handful of entities? Or vice versa? If the actions are symmetric we make the primary command the actions regardless of cardinality. Otherwise we use the concept with the lower cardinality as the primary command.

The key is consistency. A consistent structure makes it easier for a new customer to explore the commands. Therefore, we must apply the structure we choose consistently to all commands.

Input

To improve ease of use we avoid requiring the user to provide UUIDs as parameters to commands. Because UUIDs are the preferred entity identifiers in most distributed systems, satisfying this principle is more complicated than it seems.

We recommend using config files to load and save the UUID(s) of the “active” entity(s). If the app is similar to git, where commands are executed in the context of a working folder, then we use a config file per working folder. However, if the commands don’t have a folder execution context we can store any config files in the user’s home folder. To save the UUIDs we can implement explicit app commands for setting the active entity.


> tamarillo service set -id a0744c52-36fe-11e8-b467-0ed5f89f718b

However, our preferred way is to implicitly set the “active” entity to the last entity that was explicitly specified. Then all subsequent commands can omit specifying the UUID explicitly.


# Start the service
> tamarillo service start -id a0744c52-36fe-11e8-b467-0ed5f89f718b

# Query for the status of the serice
> tamarillo service status

# Restart the service
> tamarillo service restart

Another option is to accept entity names instead of unique identifiers. However, for this to work the service has to support entity names.

Output

There is no output harder to read than rows of data where the columns are misaligned. So we align tabular output produced by the app. We use the tablewriter library to format tabular output.

Another priciple is to make information stand out by leveraging colorful text. However, we must be judicious with the use of color. Too much color can actually impair the readability of your output. In a number of our applications we have used the color library to generate colorized output.

Build for versatility

Probably the most consequential omission during the initial design of a CLI is scriptability. Following a few simple patterns we can implement a command line app that is capable of producing JSON output in addition to the human readable one. And as a result one can now easily parse the output using jq in any shell script.

Here are some simple rules of thumb to follow:

  • Output a single JSON object. Even displaying multiple JSON objects with clear delimiters, e.g. “\n”, makes the output considerably harder to parse.
  • Suppress all other output. If we do not any additional output will interfere with parsing of the JSON output.
  • Display errors as valid JSON objects when the command fails.

Structure for testability

Finally, you want to write suite of automated tests to guard against regressions. To accomplish this goal we write both integration tests and unit tests.

We write the integration tests to exercise the app against real services/environments. Here we leverage all the work we did to make the CLI scriptable. Because we can emit JSON output from all commands, we can build robust test cases that validate app behavior. Furthermore, we are able to reliably implement complex test scenarios that requires us to pass parameters between commands.

On the other hand, we use unit-tests to ensure all execution paths are covered, especially failure paths. However, to write unit tests we have to carefully structure the app. First of all, we use client abstractions to interact with external services. During unit-test execution we replace the clients with mocks. Then we make the command action function easy to unit-test by following some simple rules such as: do not access global variables; return detailed results that can be asserted on; propagate errors through return values – never use os.Exit or panic; and pass in an io.Writer to receive all output.