# Golang CLI

> When to activate: Go CLI tools, cobra, pflag, subcommands, config files, viper, shell completion, stdin/stdout pipelines

- Skill: `mattakushi432/golang-cli` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/golang-cli`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/golang-cli/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/golang-cli

---


# Go CLI Patterns

## Cobra + Viper Setup

```go
// 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

```go
// 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

```go
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)

```go
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

```go
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

```go
// 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

