Building Beautiful CLI Tools with Go

Download PDF

Go has become the language of choice for building command-line tools. From Docker to Kubernetes, many of the tools we use daily are written in Go. Let’s explore why Go excels at CLI development and how to build your own professional tools.

Why Go for CLI Tools?

  1. Single binary deployment - No runtime dependencies
  2. Cross-platform compilation - Build for any OS from any OS
  3. Fast startup time - Important for CLI tools
  4. Great standard library - Many CLI features built-in
  5. Strong ecosystem - Libraries like Cobra, Viper, and more

Getting Started with Cobra

Cobra is the most popular library for building CLI tools in Go:

package main

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

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "A brief description of your application",
    Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application.`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Hello from myapp!")
    },
}

func main() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Adding Subcommands

One of Cobra’s strengths is its support for subcommands:

var serveCmd = &cobra.Command{
    Use:   "serve",
    Short: "Start the server",
    Run: func(cmd *cobra.Command, args []string) {
        port, _ := cmd.Flags().GetInt("port")
        fmt.Printf("Starting server on port %d\n", port)
    },
}

func init() {
    rootCmd.AddCommand(serveCmd)
    serveCmd.Flags().IntP("port", "p", 8080, "Port to run server on")
}

Configuration with Viper

Viper integrates seamlessly with Cobra for configuration management:

import "github.com/spf13/viper"

func initConfig() {
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath(".")
    viper.AddConfigPath("$HOME/.myapp")
    
    viper.SetEnvPrefix("MYAPP")
    viper.AutomaticEnv()
    
    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    }
}

Beautiful Output

Making your CLI output beautiful and informative:

import (
    "github.com/fatih/color"
    "github.com/olekukonko/tablewriter"
)

// Colored output
color.Green("✓ Operation successful")
color.Red("✗ Operation failed")

// Tables
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Name", "Status", "Duration"})
table.Append([]string{"Task 1", "Complete", "1.2s"})
table.Append([]string{"Task 2", "Failed", "0.8s"})
table.Render()

Progress Indicators

Show progress for long-running operations:

import "github.com/schollz/progressbar/v3"

bar := progressbar.Default(100)
for i := 0; i < 100; i++ {
    bar.Add(1)
    time.Sleep(10 * time.Millisecond)
}

Interactive Prompts

Get user input with style:

import "github.com/AlecAivazis/survey/v2"

var qs = []*survey.Question{
    {
        Name:     "name",
        Prompt:   &survey.Input{Message: "What is your name?"},
        Validate: survey.Required,
    },
    {
        Name: "color",
        Prompt: &survey.Select{
            Message: "Choose a color:",
            Options: []string{"red", "blue", "green"},
        },
    },
}

answers := struct {
    Name  string
    Color string
}{}

survey.Ask(qs, &answers)

Best Practices

  1. Use persistent flags for global options

    rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
    
  2. Provide helpful examples

    cmd.Example = `  myapp serve --port 8080
      myapp serve -p 8080 --host localhost`
    
  3. Validate inputs early

    cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
        if len(args) < 1 {
            return fmt.Errorf("requires at least one argument")
        }
        return nil
    }
    
  4. Support common conventions

    • -h/--help for help
    • -v/--version for version
    • --config for config file
    • Environment variables for configuration
  5. Provide shell completion

    rootCmd.AddCommand(genCompletionCmd())
    

Testing CLI Applications

func TestServeCommand(t *testing.T) {
    cmd := rootCmd
    b := bytes.NewBufferString("")
    cmd.SetOut(b)
    cmd.SetErr(b)
    cmd.SetArgs([]string{"serve", "--port", "9090"})
    
    err := cmd.Execute()
    assert.NoError(t, err)
    
    out, _ := ioutil.ReadAll(b)
    assert.Contains(t, string(out), "9090")
}

Distribution

  1. Use goreleaser for releases
  2. Provide installation scripts
  3. Submit to package managers (Homebrew, apt, etc.)
  4. Include man pages
  5. Provide Docker images

Conclusion

Building CLI tools in Go is a joy. The combination of Go’s strengths and the excellent ecosystem of libraries makes it possible to create professional, user-friendly command-line applications quickly. Start with Cobra, add some color and interactivity, and you’ll have users loving your tools in no time!