mcpField
G
go-cli

go-cli

github.com/eduardo-slDevelopment8395 installsUpdated 2w ago
Repository

About go-cli

A Go skill reference for designing command-line programs, aimed at developers building testable tools for people and scripts. It covers stream handling, flags, exit codes, cancellation, testing, version reporting, and shell completions.

Key features

  1. 01Testable run function structure
  2. 02stdout and stderr conventions
  3. 03Flags, arguments, and exit codes
  4. 04Context cancellation on signals
  5. 05Human- and machine-readable output
  6. 06Testing, versioning, and completions

Use cases

  1. 01Structuring a CLI for direct unit testing
  2. 02Keeping piped output separate from diagnostics
  3. 03Making command cancellation respond to Ctrl+C
  4. 04Verifying CLI behavior and exit codes

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-cli

After installing, type/go-cliin Claude Code to confirm it loaded.

Bundled filesreferences/testing-and-versioning.md

Details

From the project README

Go CLI Design

A good CLI is a well-behaved Unix citizen: flags before magic, stdout for data, stderr for diagnostics, exit codes that scripts can trust, and Ctrl+C that actually stops it.

Detailed reference material, loaded on demand:

  • references/testing-and-versioning.md — exit-code mapping in main, golden-file and testscript tests, version from build info, completions.

Read a reference file only when the summary below is not enough.

1. Structure: Testable main

func main() {
    ctx, stop := signal.NotifyContext(context.Background(),
        os.Interrupt, syscall.SIGTERM)
    defer stop()

    if err := run(ctx, os.Args[1:], os.Stdin, os.Stdout, os.Stderr); err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1)
    }
}

func run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error {
    fs := flag.NewFlagSet("mytool", flag.ContinueOnError)
    fs.SetOutput(stderr)
    verbose := fs.Bool("v", false, "verbose output")
    out := fs.String("o", "-", "output file (- for stdout)")
    if err := fs.Parse(args); err != nil {
        return err
    }
    // ...
    _ = verbose
    _ = out
    return nil
}
  • signal.NotifyContext makes Ctrl+C cancel the context — every long operation takes ctx and stops cleanly. Call stop() once the context is done so a second Ctrl+C kills a stuck program immediately.
  • run receives args and streams — tests call it directly with strings.Reader/bytes.Buffer, no subprocess needed.
  • os.Exit only in main (it skips defers).

2. stdout vs stderr

  • stdout: the program's output — data, results, the thing you pipe.
  • stderr: logs, progress, warnings, usage errors.
  • --json or detecting a pipe (!term.IsTerminal(int(os.Stdout.Fd()))) should silence decorations, never change the data.
// ✅ Good — result to stdout, progress to stderr
fmt.Fprintf(stderr, "processed %d files\n", n)
fmt.Fprintln(stdout, result)

// ❌ Bad — mixing both into stdout breaks every pipe
fmt.Printf("processing...\ndone: %s\n", result)

3. Exit Codes

CodeMeaning
0Success
1Generic runtime failure
2Usage error (bad flags/arguments) — flag package's convention
>2Tool-specific, documented meanings (e.g. grep's 1 = no match)

Map errors to codes in one place (main), not scattered os.Exit calls. With flag.ContinueOnError, -h returns flag.ErrHelp — exit 0 for it, and wrap parse errors in a usage error type so main can exit 2. Exit 130 after Ctrl+C. If scripts will branch on distinct failures, define sentinel errors and translate: errors.Is(err, ErrNoMatch) → 1.

4. Flags and Arguments

  • Flags for options, positional args for the primary operands: mytool -v convert input.yaml, not mytool --input=input.yaml.
  • Every flag has a usage string; -h/-help output is your primary UX.
  • Accept - as "stdin/stdout" for file arguments.
  • Defaults must be safe: destructive behavior behind explicit flags (--force), never default-on.
  • Read secrets from env or files, never from flags (ps leaks argv).

5. Subcommands

Standard library, fine up to a handful of commands:

switch fs.Arg(0) {
case "serve":
    return runServe(ctx, fs.Args()[1:], stdout, stderr)
case "migrate":
    return runMigrate(ctx, fs.Args()[1:], stdout, stderr)
default:
    fmt.Fprintln(stderr, usage)
    return fmt.Errorf("unknown command %q", fs.Arg(0))
}

Adopt Cobra when you need nested commands, generated help/completions, and many flags — the structure pays for the dependency:

var rootCmd = &cobra.Command{Use: "mytool", SilenceUsage: true}

var serveCmd = &cobra.Command{
    Use:   "serve",
    Short: "Start the server",
    RunE: func(cmd *cobra.Command, args []string) error {
        return serve(cmd.Context(), addr) // RunE returns errors; no os.Exit
    },
}

func init() {
    serveCmd.Flags().StringVar(&addr, "addr", ":8080", "listen address")
    rootCmd.AddCommand(serveCmd)
}

Cobra rules: always RunE (never Run + os.Exit), set SilenceUsage: true so runtime errors don't dump help, pass cmd.Context() down. Add Viper only when layered config (flags > env > file) is a real requirement — for most tools flag + os.Getenv is enough.

6. Output for Humans and Machines

  • --json flag for machine consumption; table/text default for humans.
  • Never emit ANSI colors when stdout is not a terminal or NO_COLOR is set.
  • Progress bars/spinners go to stderr and only when it's a terminal.

7. Testing

Test run directly with buffers, comparing stdout against golden files regenerated with -update. For end-to-end behavior — real binary, exit codes, stdin — use testscript (.txtar scripts), the format the Go command tests itself with.

8. Version Information

mytool version reads debug.ReadBuildInfo(): since Go 1.24 the main module's version comes from the VCS tag, and vcs.revision from the build settings. Fall back to -ldflags "-X main.version=..." only when building without VCS metadata.

9. Shell Completions

Cobra generates completion bash|zsh|fish|powershell; add ValidArgsFunction for dynamic values. Without Cobra, ship a static completion script with the release.

Examples for sections 3 and 7-9: references/testing-and-versioning.md.

Verification Checklist

  1. run(ctx, args, stdin, stdout, stderr) pattern — logic testable without subprocess
  2. signal.NotifyContext wired; long operations respect ctx cancellation
  3. Data on stdout, diagnostics on stderr — verified by piping
  4. Exit codes: 0 success, 2 usage, documented codes otherwise; os.Exit only in main
  5. Every flag has usage text; -h output reviewed
  6. - accepted for stdin/stdout where files are taken
  7. Destructive actions require explicit flags
  8. No secrets via argv
  9. Cobra (if used): RunE everywhere, SilenceUsage, context propagated
  10. Colors/spinners disabled for non-TTY and NO_COLOR
  11. -h exits 0, usage errors exit 2, Ctrl+C exits 130
  12. version output comes from build info; golden or testscript tests cover the CLI surface

Comments

Markdown supported