Skip to main content
  1. Posts/
  2. Go/

CLIs in Go with Cobra

··863 words·5 mins·

In this document, I’ll demonstrate how I use Steve Francia’s cobra library, which enables the interchangeable usage of environment variable and a configuration file for the same Go program.

For the purposes of simplicity, I assume that all of the code snippets shown here are part of the main package. However, I recommend authors of large codebases to employ a multi-package taxonomy, such that their code is more organised and maintainable.

Parent-level Commands #

The parent level is the command that is in the root of the CLI command tree or any command with sub-commands registered to it. In Cobra command can have separate functions for its set-up, validation an execution stages, along with metadata about it that can be used to print support documentation.

import "github.com/spf13/cobra"

var (
	Version = "development"
	ParentCommand = cobra.Command{
		Use:     "parent",
		Short:   "parent command",
		Long:    "parent command",
		Version: Version,
		PreRunE: func(cmd *cobra.Command, args []string) error {
			// prepare environment (such as files) and return error/nil ...
		},
		Args: func(cmd *cobra.Command, args []string) error {
			// check command input and return error/nil ...
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			// do your thing and return error/nil ...
		},
	}
)

func init() {
	ParentCommand.SetVersionTemplate("{{.Version}}\n")
}

The version template is overridden in order to print just the version (when using the --version flag), thus allowing easier parsing of the version by other programs.

Version Command #

I like to include a custom version sub-command that can include more information such as commit hash and platform when using the --verbose/-v flag.

package commands

import (
	"fmt"
	"runtime"
	"github.com/spf13/cobra"
)

var (
	Hash           = "development"
	verbose        bool
	versionCommand = &cobra.Command{
		Use:   "version",
		Short: "print version",
		Long:  "print version",
		Run: func(cmd *cobra.Command, args []string) {
			if verbose {
				if Version != "development" {
					fmt.Printf("version: \t%s\n", Version)
				}
				if Hash != "development" {
					fmt.Printf("commit: \t%s\n", Hash)
				}
				fmt.Printf("compiler: \t%s (%s)\n", runtime.Version(), runtime.Compiler)
				fmt.Printf("platform: \t%s/%s\n", runtime.GOOS, runtime.GOARCH)
			} else {
				fmt.Println(Version)
			}
		},
	}
)

func init() {
	versionCommand.Flags().BoolVarP(&verbose, "verbose", "v", false, "version, git commit hash, compiler version & platform")
	ParentCommand.AddCommand(versionCommand)
}

If you want to inject release-specific values to Go’s global memory during build time, I wrote about using GoReleaser for this purpose, and how I integrate it to my continuous integration pipeline.

Child-level Commands #

Some CLIs (such as git) require multiple sub-commands, each with different functionality in order to maintain usability. This can be achieved by initialising a new cobra.Command object and registering as a sub-command to its parent in an init function. In addition, flags that accept more complex data types (such as duration) can be integrated with a given command by binding it to a variable (ideally in the same init function mentioned above).

import "github.com/spf13/cobra"

var (
	duration time.Duration
	childCommand = cobra.Command{
		Use:   "child",
		Short: "child command",
		Long:  "child command",
		// ...
	}
)

func init() {
	childCommand.Flags().DurationVarP(&duration, "duration", "d", 0, "duration with units ns, us (or ┬Ás), ms, s, m, h")
	ParentCommand.AddCommand(&childCommand)
}

Execution #

In order to parse and execute any Cobra command or its children, it can be executed in any stage of the program’s runtime. Most CLI-type application would execute their root-level command directly from the main function. For a clearer user experience, a the execution function of a command prints the error to standard output (if it’s non-nil) before returning it to be examined and to be dealt with.

package main

import "os"

func main() {
	if err := ParentCommand.Execute(); err != nil {
		os.Exit(1)
	}
}

Additional Resources #

Before building and packaging the CLI, I tend to run the following commands and append their output to a text files, which are automatically packaged with the binary later in the continuous integration process.

Manual Page #

With Christian Muehlhaeuser’s mango library, a user manual page can be generated for a Cobra command and all of its child commands. I tend to this by defining a new cobra.Command object that reads the root command and uses the above-mentioned library to print the manual page to standard output (which can be tested by piping the output to man -l -).

package commands

import (
	"fmt"
	mango "github.com/muesli/mango-cobra"
	"github.com/muesli/roff"
	"github.com/spf13/cobra"
)

var manualCommand = &cobra.Command{
	Use:   "manual",
	Short: "print manual page",
	Long:  "print manual page to standard output",
	RunE: func(cmd *cobra.Command, args []string) error {
		manualPage, err := mango.NewManPage(1, ParentCommand)
		if err != nil {
			return err
		}

		manualPage.WithSection("Bugs", fmt.Sprintf("Please report bugs to our GitHub page https://github.com/AppleGamer22/%s/issues", manualPage.Root.Name))
		manualPage.WithSection("Authors", "Omri Bornstein <omribor@gmail.com>")
		manualPage.WithSection("Copyright", `cocainate is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version.
cocainate is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.`)

		_, err = fmt.Println(manualPage.Build(roff.NewDocument()))
		return err
	},
}

func init() {
	RootCommand.AddCommand(manualCommand)
}

Shell Completion Scripts #

Cobra also ships with a completion sub-command for generating completion scripts for bash, zsh, fish and PowerShell, which enables the user to discover and understand your CLI more quickly.