How to print JSON output from your CLI

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

Leave a Reply

Your email address will not be published. Required fields are marked *