CLI frameworks for Go: from simple utilities to TUI interfaces
Go (Golang) has become the #1 language for creating console utilities and DevOps tools. Thanks to fast compilation, built-in concurrency support, and simple distribution (a single binary file), Go is ideal for CLI. However, writing an argument parser, error handling, and colored output from scratch is a labor-intensive task. CLI frameworks come to the rescue. In this article, we will cover three key tools: Cobra, urfave/cli, and Bubble Tea.
1. Cobra — the industry standard
Cobra (github.com/spf13/cobra) is the most popular framework for Go. It is used by Kubernetes, Hugo, Docker, GitHub CLI. Cobra provides a powerful command generator, nested subcommands, autocompletion for bash/zsh/fish, and integration with Viper (configuration management).
Installation
go get -u github.com/spf13/cobra/cobraExample: a simple application with subcommands
package main
import ( "fmt" "github.com/spf13/cobra")
func main() { var rootCmd = &cobra.Command{ Use: "app", Short: "Example application on Cobra", Run: func(cmd *cobra.Command, args []string) { fmt.Println("Root handler launched") }, }
var helloCmd = &cobra.Command{ Use: "hello [name]", Short: "Greet the user", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { fmt.Printf("Hello, %s!", args[0]) }, }
rootCmd.AddCommand(helloCmd) rootCmd.Execute()}Key features:
- Automatic help generation (--help)
- Support for flags (persistent, local)
- Argument validation
- Generation of autocompletion scripts
- Integration with Viper for configuration
2. Urfave/cli — simplicity and flexibility
Urfave/cli (github.com/urfave/cli/v2) is a lightweight alternative to Cobra. The framework is declarative: you describe commands, flags, and actions in structures. Ideal for small and medium-sized utilities.
Installation
go get github.com/urfave/cli/v2Example: a file utility
package main
import ( "fmt" "log" "os" "github.com/urfave/cli/v2")
func main() { app := &cli.App{ Name: "fileutil", Usage: "utility for working with files", Commands: []*cli.Command{ { Name: "read", Aliases: []string{"r"}, Usage: "read a file", Action: func(cCtx *cli.Context) error { if cCtx.NArg() == 0 { return fmt.Errorf("specify the file name") } data, _ := os.ReadFile(cCtx.Args().First()) fmt.Println(string(data)) return nil }, }, { Name: "write", Aliases: []string{"w"}, Usage: "write a string to a file", Flags: []cli.Flag{ &cli.StringFlag{ Name: "content", Aliases: []string{"c"}, Usage: "content to write", }, }, Action: func(cCtx *cli.Context) error { content := cCtx.String("content") if content == "" { return fmt.Errorf("specify --content") } return os.WriteFile(cCtx.Args().First(), []byte(content), 0644) }, }, }, }
if err := app.R