Building Beautiful CLI Tools with Go
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?
- Single binary deployment - No runtime dependencies
- Cross-platform compilation - Build for any OS from any OS
- Fast startup time - Important for CLI tools
- Great standard library - Many CLI features built-in
- 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
-
Use persistent flags for global options
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") -
Provide helpful examples
cmd.Example = ` myapp serve --port 8080 myapp serve -p 8080 --host localhost` -
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 } -
Support common conventions
-h/--helpfor help-v/--versionfor version--configfor config file- Environment variables for configuration
-
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
- Use goreleaser for releases
- Provide installation scripts
- Submit to package managers (Homebrew, apt, etc.)
- Include man pages
- 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!