Go CLI Patterns
Cobra + Viper Setup
// cmd/root.go
package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "A CLI tool for managing resources",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return viper.BindPFlags(cmd.Flags())
},
}
func Execute() error {
return rootCmd.Execute()
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().String("config", "", "config file (default: $HOME/.myapp.yaml)")
rootCmd.PersistentFlags().Bool("verbose", false, "enable verbose output")
rootCmd.PersistentFlags().String("output", "text", "output format: text|json|yaml")
viper.BindPFlags(rootCmd.PersistentFlags())
}
func initConfig() {
viper.SetConfigName(".myapp")
viper.SetConfigType("yaml")
viper.AddConfigPath("$HOME")
viper.AddConfigPath(".")
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
if cfgFile := viper.GetString("config"); cfgFile != "" {
viper.SetConfigFile(cfgFile)
}
viper.ReadInConfig()
}
Subcommand with Flags
// cmd/create.go
var createCmd = &cobra.Command{
Use: "create [name]",
Short: "Create a new resource",
Args: cobra.ExactArgs(1),
RunE: runCreate,
}
var createFlags struct {
dryRun bool
tags []string
ttl time.Duration
}
func init() {
rootCmd.AddCommand(createCmd)
createCmd.Flags().BoolVar(&createFlags.dryRun, "dry-run", false, "preview without making changes")
createCmd.Flags().StringSliceVar(&createFlags.tags, "tag", nil, "tags to attach (repeatable)")
createCmd.Flags().DurationVar(&createFlags.ttl, "ttl", 24*time.Hour, "time to live")
}
func runCreate(cmd *cobra.Command, args []string) error {
name := args[0]
if createFlags.dryRun {
fmt.Printf("Would create: %s (ttl=%s tags=%v)\n", name, createFlags.ttl, createFlags.tags)
return nil
}
return client.Create(cmd.Context(), name, createFlags.tags, createFlags.ttl)
}
Output Formatting
type OutputFormat string
const (
FormatText OutputFormat = "text"
FormatJSON OutputFormat = "json"
FormatYAML OutputFormat = "yaml"
)
func printResource(r Resource, format OutputFormat) error {
switch format {
case FormatJSON:
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(r)
case FormatYAML:
return yaml.NewEncoder(os.Stdout).Encode(r)
default:
fmt.Printf("Name: %s\nStatus: %s\nCreated: %s\n", r.Name, r.Status, r.CreatedAt.Format(time.RFC3339))
return nil
}
}
Reading from Stdin (Unix Pipelines)
func runProcess(cmd *cobra.Command, args []string) error {
var input io.Reader
if len(args) > 0 {
f, err := os.Open(args[0])
if err != nil { return err }
defer f.Close()
input = f
} else {
// Check if stdin has data (not a terminal)
fi, _ := os.Stdin.Stat()
if fi.Mode()&os.ModeCharDevice != 0 {
return errors.New("provide a file or pipe input via stdin")
}
input = os.Stdin
}
scanner := bufio.NewScanner(input)
for scanner.Scan() {
processLine(scanner.Text())
}
return scanner.Err()
}
Progress Indicators
import "github.com/schollz/progressbar/v3"
func processFiles(files []string) error {
bar := progressbar.NewOptions(len(files),
progressbar.OptionSetDescription("Processing..."),
progressbar.OptionShowCount(),
progressbar.OptionClearOnFinish(),
)
for _, f := range files {
if err := process(f); err != nil { return err }
bar.Add(1)
}
return nil
}
Shell Completion
// Auto-generate completion scripts
rootCmd.AddCommand(&cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return rootCmd.GenBashCompletion(os.Stdout)
case "zsh":
return rootCmd.GenZshCompletion(os.Stdout)
case "fish":
return rootCmd.GenFishCompletion(os.Stdout, true)
default:
return fmt.Errorf("unsupported shell: %s", args[0])
}
},
})
Common Anti-Patterns
os.Exit in commands — return errors instead; let Execute() handle exit codes
- Global vars for dependencies — inject dependencies through closures or command structs
- No
RunE (using Run) — RunE propagates errors properly; Run silently swallows them
- Not respecting
--output flag — check format flag in every command that produces output
- Long-running operations without context — use
cmd.Context() so Ctrl+C cancels the operation