[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1 @@
|
||||
XingfenD
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
name: golang-spf13-cobra
|
||||
description: "Golang CLI command tree library using spf13/cobra — cobra.Command, RunE vs Run, PersistentPreRunE hook chain, Args validators (NoArgs, ExactArgs, MatchAll, custom), persistent vs local flags, command groups, ValidArgsFunction, RegisterFlagCompletionFunc, ShellCompDirective, usage/help template customization, man-page and markdown doc generation, and testing with SetArgs/SetOut/SetErr. Apply when using or adopting spf13/cobra, or when the codebase imports `github.com/spf13/cobra`. For configuration layering alongside cobra, see the `samber/cc-skills-golang@golang-spf13-viper` skill. For general CLI architecture (project layout, exit codes, signal handling, I/O patterns), see `samber/cc-skills-golang@golang-cli`."
|
||||
user-invocable: true
|
||||
license: MIT
|
||||
compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang.
|
||||
metadata:
|
||||
author: samber
|
||||
version: "1.0.4"
|
||||
openclaw:
|
||||
emoji: "🐍"
|
||||
homepage: https://github.com/samber/cc-skills-golang
|
||||
requires:
|
||||
bins:
|
||||
- go
|
||||
install: []
|
||||
skill-library-version: "1.10.2"
|
||||
allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs Bash(godig:*) Bash(gopls:*) LSP mcp__gopls__*
|
||||
---
|
||||
|
||||
**Persona:** You are a Go CLI engineer building command trees that feel native to the Unix shell. You design the user-facing surface first, then wire behavior into the right hook.
|
||||
|
||||
**Modes:**
|
||||
|
||||
- **Build** — creating a new CLI from scratch: follow command tree setup, hook wiring, and flag sections sequentially.
|
||||
- **Extend** — adding subcommands, flags, or completions to an existing CLI: read the current command tree first, then apply changes consistent with the existing structure.
|
||||
- **Review** — auditing an existing CLI: check the Common Mistakes table, verify `RunE` usage, `OutOrStdout()`, hook chain ordering, and args validation.
|
||||
|
||||
# Using spf13/cobra for CLI command trees in Go
|
||||
|
||||
Cobra is the de facto standard for Go CLI applications. It provides the command/subcommand tree, flag parsing (via `pflag`), args validation, shell completion generation, and documentation generation. It does **not** handle configuration layering — that's viper's job.
|
||||
|
||||
**Official Resources:**
|
||||
|
||||
- [pkg.go.dev/github.com/spf13/cobra](https://pkg.go.dev/github.com/spf13/cobra)
|
||||
- [github.com/spf13/cobra](https://github.com/spf13/cobra)
|
||||
- [cobra.dev](https://cobra.dev)
|
||||
|
||||
This skill is not exhaustive. Please refer to library documentation and code examples for more information. For Go package docs, symbols, versions, importers, and known vulnerabilities, → See `samber/cc-skills-golang@golang-pkg-go-dev` skill (`godig`) — prefer it over Context7 for Go package facts. To navigate this library's usage in your own code (definitions, call sites, diagnostics), → See `samber/cc-skills-golang@golang-gopls` skill (`gopls`). Context7 remains a fallback for docs not indexed on pkg.go.dev.
|
||||
|
||||
```bash
|
||||
go get github.com/spf13/cobra@latest
|
||||
```
|
||||
|
||||
## Cobra vs. viper
|
||||
|
||||
These libraries do fundamentally different things and can be used independently.
|
||||
|
||||
| Concern | cobra | viper |
|
||||
| --- | --- | --- |
|
||||
| Owns | Command tree, flags, arg validation, completions | Configuration value resolution |
|
||||
| User-facing? | Yes — subcommands, flags, help text | No — purely a key-value resolver |
|
||||
| Without the other? | Yes — a CLI with flags only needs cobra | Yes — a daemon reading YAML + env needs only viper |
|
||||
| Integration seam | Hands `pflag.Flag` to viper via `BindPFlag` | Treats the cobra flag as the highest-precedence layer |
|
||||
|
||||
**Use cobra alone** when your binary takes flags and args but needs no config file or env resolution. **Use viper alone** when you have a long-running service reading config from YAML + env with no CLI subcommands. Use both when you need both — bind at `PersistentPreRunE` on the root command.
|
||||
|
||||
→ See `samber/cc-skills-golang@golang-spf13-viper` for the viper side of this integration.
|
||||
|
||||
## Command tree
|
||||
|
||||
Every cobra CLI has a root command plus zero or more subcommands registered with `AddCommand`. The root command name is the binary name.
|
||||
|
||||
```go
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "myapp",
|
||||
Short: "One-line summary",
|
||||
SilenceUsage: true, // ✓ prevents usage wall on every error
|
||||
SilenceErrors: true, // ✓ lets you control error output format
|
||||
}
|
||||
```
|
||||
|
||||
Use `AddGroup` to label subcommands in help output — register groups **before** the `AddCommand` calls that reference them; cobra does not retroactively assign groups.
|
||||
|
||||
## The Run\* family
|
||||
|
||||
Cobra commands have five run hooks executed in order:
|
||||
|
||||
```
|
||||
PersistentPreRunE → PreRunE → RunE → PostRunE → PersistentPostRunE
|
||||
```
|
||||
|
||||
Always use `*E` variants — the non-`E` forms cannot return errors. Key rules:
|
||||
|
||||
- `PersistentPreRunE` on the root runs before **every** subcommand — use it for config init and auth checks.
|
||||
- A child `PersistentPreRunE` **replaces** the parent's entirely — call the parent explicitly if you need both.
|
||||
- `PostRunE` runs only if `RunE` succeeded.
|
||||
|
||||
For the full lifecycle and inheritance rules, see [commands-and-args.md](references/commands-and-args.md).
|
||||
|
||||
## Args validators
|
||||
|
||||
Cobra validates positional arguments before `RunE` runs. Never write `len(args)` checks inside `RunE` — that bypasses cobra's standard error messages and arg count tracking.
|
||||
|
||||
Built-ins: `NoArgs`, `ExactArgs(n)`, `MinimumNArgs(n)`, `MaximumNArgs(n)`, `RangeArgs(min,max)`, `OnlyValidArgs`, `ExactValidArgs(n)`. Compose with `MatchAll(v1, v2)`. Custom validator: `func(cmd *cobra.Command, args []string) error`.
|
||||
|
||||
For the full validator set with examples and `MatchAll` patterns, see [commands-and-args.md](references/commands-and-args.md).
|
||||
|
||||
## Flags primer
|
||||
|
||||
Cobra delegates flag parsing to `pflag`. **Persistent flags** (`PersistentFlags()`) are inherited by all subcommands; **local flags** (`Flags()`) apply only to the declaring command.
|
||||
|
||||
```go
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file path") // inherited by all subcommands
|
||||
serveCmd.Flags().IntVar(&port, "port", 8080, "listen port") // local to serveCmd only
|
||||
serveCmd.MarkFlagRequired("port")
|
||||
serveCmd.MarkFlagsMutuallyExclusive("json", "yaml")
|
||||
```
|
||||
|
||||
For pflag types, custom flag values, flag groups, and viper binding, see [flags.md](references/flags.md).
|
||||
|
||||
## Completions primer
|
||||
|
||||
Cobra generates shell completions automatically. Extend them with:
|
||||
|
||||
- **`ValidArgs []string`** — static positional arg completion.
|
||||
- **`ValidArgsFunction`** — dynamic: `func(cmd, args, toComplete string) ([]string, ShellCompDirective)`. Return `ShellCompDirectiveNoFileComp` to suppress file fallback.
|
||||
- **`RegisterFlagCompletionFunc(name, fn)`** — flag value completion.
|
||||
|
||||
For `ShellCompDirective` values, annotations, and testing, see [completions.md](references/completions.md).
|
||||
|
||||
## Testing commands
|
||||
|
||||
Test commands by executing them programmatically. **Never use `os.Stdout` / `os.Stderr` directly** in command handlers — use `cmd.OutOrStdout()` / `cmd.ErrOrStderr()` so tests can redirect output.
|
||||
|
||||
```go
|
||||
func TestServeCmd(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
rootCmd.SetOut(buf)
|
||||
rootCmd.SetArgs([]string{"serve", "--port", "9090"})
|
||||
require.NoError(t, rootCmd.Execute())
|
||||
assert.Contains(t, buf.String(), "listening on :9090")
|
||||
}
|
||||
```
|
||||
|
||||
Cobra accumulates flag state across `Execute()` calls — build a fresh command tree per test. For isolation patterns, golden files, and testing completions, see [testing.md](references/testing.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use `RunE`, never `Run`** — `Run` cannot return an error; the only escape is `os.Exit` or panic, bypassing defers.
|
||||
2. **Put config initialization in `PersistentPreRunE`** — it runs before every subcommand; the right place for viper binding and auth checks.
|
||||
3. **Validate positional args with `Args`, not inside `RunE`** — `Args` gives cobra's standard error messages; `MatchAll` composes validators.
|
||||
4. **Use `cmd.OutOrStdout()` / `cmd.ErrOrStderr()` for all output** — direct `os.Stdout` writes cannot be captured by tests.
|
||||
5. **Re-create the command tree per test** — cobra accumulates flag state across `Execute()` calls on the same instance.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Why it fails | Fix |
|
||||
| --- | --- | --- |
|
||||
| Using `Run` instead of `RunE` | Cannot return an error — only escape is `os.Exit` or panic, bypassing defers | Use `RunE` — return the error, let cobra handle the exit |
|
||||
| Writing `len(args)` checks in `RunE` | Bypasses cobra's standard error messages ("accepts 1 arg, received 2") | Declare `Args: cobra.ExactArgs(1)` on the command |
|
||||
| Writing to `os.Stdout` directly | Tests cannot capture output — os-level file handles can't be redirected | Use `cmd.OutOrStdout()` / `cmd.ErrOrStderr()` |
|
||||
| Child `PersistentPreRunE` silently drops parent's | Cobra does not chain — the child replaces the parent's hook entirely | Call `parent.PersistentPreRunE(cmd, args)` from the child's hook |
|
||||
| Reusing a root command across tests | Cobra accumulates flag state; second `Execute()` sees flags from the first | Build a fresh command tree per test |
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [commands-and-args.md](references/commands-and-args.md) — full PreRun\*/PostRun\* chain, every Args validator, PersistentPreRunE inheritance rules
|
||||
- [flags.md](references/flags.md) — pflag types, required/exclusive/oneRequired groups, custom value types, viper binding
|
||||
- [completions.md](references/completions.md) — ShellCompDirective set, annotation-based completions, testing completions
|
||||
- [generators.md](references/generators.md) — man page, markdown, YAML, RST doc generation; `cobra-cli` scaffolder
|
||||
- [testing.md](references/testing.md) — isolation patterns, golden files, testing completions, table-driven command tests
|
||||
|
||||
## Cross-References
|
||||
|
||||
- → See `samber/cc-skills-golang@golang-cli` skill for general CLI architecture — project layout, exit codes, signal handling, I/O patterns
|
||||
- → See `samber/cc-skills-golang@golang-spf13-viper` skill for configuration layering alongside cobra (flag → env → file → default precedence)
|
||||
- → See `samber/cc-skills-golang@golang-testing` skill for general Go testing patterns
|
||||
|
||||
If you encounter a bug or unexpected behavior in spf13/cobra, open an issue at <https://github.com/spf13/cobra/issues>.
|
||||
@@ -0,0 +1,444 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "rune-vs-run-error-propagation",
|
||||
"description": "Tests use of RunE instead of Run for error propagation",
|
||||
"prompt": "I'm writing a cobra subcommand in Go that calls an external API. If the API returns an error, the command should exit non-zero. Should I use Run or RunE?",
|
||||
"trap": "Without the skill, the model may say both work, suggest using Run with os.Exit(1), or not explain why Run is problematic. The correct answer is always RunE — it propagates the error through cobra's error handling chain.",
|
||||
"assertions": [
|
||||
{ "id": "1.1", "text": "Recommends RunE, not Run" },
|
||||
{
|
||||
"id": "1.2",
|
||||
"text": "Explains that Run cannot return an error — you'd need os.Exit or panic"
|
||||
},
|
||||
{
|
||||
"id": "1.3",
|
||||
"text": "Shows RunE returning the error from the handler"
|
||||
},
|
||||
{ "id": "1.4", "text": "Does NOT suggest using os.Exit inside RunE" },
|
||||
{
|
||||
"id": "1.5",
|
||||
"text": "Mentions that returning error from RunE causes cobra to exit non-zero"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "args-validator-not-manual-check",
|
||||
"description": "Tests use of cobra Args validators instead of manual len(args) checks in RunE",
|
||||
"prompt": "I'm writing a Go CLI with cobra. My 'delete' command requires exactly one positional argument (the resource name). How should I validate this?",
|
||||
"trap": "Without the skill, the model writes len(args) != 1 check inside RunE. The correct approach is Args: cobra.ExactArgs(1) on the command definition, which validates before RunE runs and gives a standard error message.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "2.1",
|
||||
"text": "Sets Args: cobra.ExactArgs(1) on the command struct"
|
||||
},
|
||||
{ "id": "2.2", "text": "Does NOT write len(args) check inside RunE" },
|
||||
{
|
||||
"id": "2.3",
|
||||
"text": "Mentions that cobra prints a standard error message when validation fails"
|
||||
},
|
||||
{
|
||||
"id": "2.4",
|
||||
"text": "RunE body accesses args[0] directly without re-validating length"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "outOrStdout-not-os-stdout",
|
||||
"description": "Tests use of cmd.OutOrStdout() instead of os.Stdout for testable output",
|
||||
"prompt": "I'm writing a cobra command in Go that prints a table of results to the terminal. How should I write to stdout from inside RunE?",
|
||||
"trap": "Without the skill, the model uses fmt.Println or os.Stdout directly. The correct approach is fmt.Fprintln(cmd.OutOrStdout(), ...) which can be redirected to a buffer in tests.",
|
||||
"assertions": [
|
||||
{ "id": "3.1", "text": "Uses cmd.OutOrStdout() as the io.Writer target" },
|
||||
{ "id": "3.2", "text": "Does NOT use os.Stdout directly" },
|
||||
{
|
||||
"id": "3.3",
|
||||
"text": "Does NOT use fmt.Println (which hardcodes os.Stdout)"
|
||||
},
|
||||
{
|
||||
"id": "3.4",
|
||||
"text": "Mentions testability as the reason — SetOut can redirect the writer in tests"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "persistent-prerunE-hook-chain",
|
||||
"description": "Tests PersistentPreRunE on root for global config init and the child override trap",
|
||||
"prompt": "In my Go CLI with cobra, I want to initialize viper config before any subcommand runs. I also have one subcommand that needs its own PersistentPreRunE for extra setup. How do I make sure both run?",
|
||||
"trap": "Without the skill, the model defines PersistentPreRunE on both root and child without noting that the child's hook replaces the parent's — so root's config init never runs for that subcommand.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "4.1",
|
||||
"text": "Explains that a child's PersistentPreRunE replaces (not chains) the parent's"
|
||||
},
|
||||
{
|
||||
"id": "4.2",
|
||||
"text": "Shows explicitly calling the parent's PersistentPreRunE from inside the child's hook"
|
||||
},
|
||||
{ "id": "4.3", "text": "Does NOT claim both hooks run automatically" },
|
||||
{
|
||||
"id": "4.4",
|
||||
"text": "Uses PersistentPreRunE (the *E variant) not PersistentPreRun"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "silence-usage-and-errors",
|
||||
"description": "Tests SilenceUsage and SilenceErrors on root command",
|
||||
"prompt": "When my Go cobra CLI returns an error from RunE, the terminal shows the full usage/help text followed by the error. I only want to see the error message, not the usage. How do I fix this?",
|
||||
"trap": "Without the skill, the model may suggest overriding SetUsageTemplate or wrapping the error. The correct fix is SilenceUsage: true on the root command.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "5.1",
|
||||
"text": "Sets SilenceUsage: true on the root cobra.Command"
|
||||
},
|
||||
{
|
||||
"id": "5.2",
|
||||
"text": "Optionally mentions SilenceErrors: true (for custom error formatting)"
|
||||
},
|
||||
{
|
||||
"id": "5.3",
|
||||
"text": "Does NOT suggest removing or wrapping the error in RunE"
|
||||
},
|
||||
{
|
||||
"id": "5.4",
|
||||
"text": "Explains that SilenceUsage only suppresses usage on error, not on --help"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "command-group-registration-order",
|
||||
"description": "Tests that AddGroup must be called before AddCommand that references it",
|
||||
"prompt": "I want to group my cobra subcommands in the help output under labels like 'Core Commands:' and 'Management Commands:'. How do I set this up?",
|
||||
"trap": "Without the skill, the model calls AddCommand first and AddGroup after, which doesn't work — groups must be registered before the commands that reference them.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "6.1",
|
||||
"text": "Calls AddGroup before AddCommand for commands that use that group"
|
||||
},
|
||||
{
|
||||
"id": "6.2",
|
||||
"text": "Sets GroupID on the subcommand matching the Group's ID field"
|
||||
},
|
||||
{ "id": "6.3", "text": "Shows cobra.Group{ID: ..., Title: ...} struct" },
|
||||
{
|
||||
"id": "6.4",
|
||||
"text": "Does NOT call AddCommand before AddGroup for the same group"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "valid-args-function-dynamic-completion",
|
||||
"description": "Tests ValidArgsFunction for dynamic shell completion instead of static ValidArgs",
|
||||
"prompt": "My Go cobra 'get pod' command should complete pod names dynamically by querying the API server. ValidArgs only accepts a static list. How do I provide dynamic completions?",
|
||||
"trap": "Without the skill, the model tries to populate ValidArgs at startup (querying the API at init time) or doesn't know about ValidArgsFunction.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "7.1",
|
||||
"text": "Uses ValidArgsFunction (not ValidArgs) for dynamic completions"
|
||||
},
|
||||
{
|
||||
"id": "7.2",
|
||||
"text": "Function signature returns ([]string, cobra.ShellCompDirective)"
|
||||
},
|
||||
{
|
||||
"id": "7.3",
|
||||
"text": "Returns cobra.ShellCompDirectiveNoFileComp to prevent file fallback"
|
||||
},
|
||||
{
|
||||
"id": "7.4",
|
||||
"text": "Does NOT query the API at init() or in ValidArgs (static list)"
|
||||
},
|
||||
{
|
||||
"id": "7.5",
|
||||
"text": "Handles errors by returning cobra.ShellCompDirectiveError"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "register-flag-completion-func",
|
||||
"description": "Tests RegisterFlagCompletionFunc for flag value completion",
|
||||
"prompt": "My Go cobra command has an --output flag that accepts 'json', 'yaml', or 'table'. How do I make the shell complete valid values when the user types --output <TAB>?",
|
||||
"trap": "Without the skill, the model does not know about RegisterFlagCompletionFunc and instead documents the valid values only in the flag description string.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "8.1",
|
||||
"text": "Calls cmd.RegisterFlagCompletionFunc(\"output\", func(...) ...)"
|
||||
},
|
||||
{
|
||||
"id": "8.2",
|
||||
"text": "The completion function returns []string{\"json\", \"yaml\", \"table\"} (or similar)"
|
||||
},
|
||||
{ "id": "8.3", "text": "Returns cobra.ShellCompDirectiveNoFileComp" },
|
||||
{
|
||||
"id": "8.4",
|
||||
"text": "Does NOT rely only on the flag usage string for user guidance"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "test-isolation-fresh-root",
|
||||
"description": "Tests that a fresh command tree must be created per test to avoid flag state leakage",
|
||||
"prompt": "I'm writing tests for my Go cobra CLI. My first test runs 'myapp serve --port 9090' and passes. My second test runs 'myapp serve' without --port and expects the default 8080, but gets 9090. What's wrong and how do I fix it?",
|
||||
"trap": "Without the skill, the model may suggest resetting the flag value manually or calling ResetFlags(). The correct fix is to create a fresh command tree per test.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "9.1",
|
||||
"text": "Identifies the root cause as reusing the same cobra.Command instance across tests"
|
||||
},
|
||||
{
|
||||
"id": "9.2",
|
||||
"text": "Recommends building a new command tree per test (constructor function)"
|
||||
},
|
||||
{
|
||||
"id": "9.3",
|
||||
"text": "Shows a newRootCmd() or similar factory function pattern"
|
||||
},
|
||||
{
|
||||
"id": "9.4",
|
||||
"text": "Does NOT suggest ResetFlags() as the primary solution"
|
||||
},
|
||||
{
|
||||
"id": "9.5",
|
||||
"text": "Each test calls the factory to get a fresh *cobra.Command"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "match-all-validator-composition",
|
||||
"description": "Tests MatchAll for composing multiple arg validators",
|
||||
"prompt": "My Go cobra 'apply' command needs positional args that are all valid resource names (from a known list) AND there must be at least one. How do I express both constraints?",
|
||||
"trap": "Without the skill, the model writes a custom validator function that manually checks both conditions with if statements. MatchAll composes built-in validators without custom code.",
|
||||
"assertions": [
|
||||
{ "id": "10.1", "text": "Uses cobra.MatchAll to compose validators" },
|
||||
{
|
||||
"id": "10.2",
|
||||
"text": "Combines cobra.MinimumNArgs(1) (or ExactArgs) with cobra.OnlyValidArgs"
|
||||
},
|
||||
{ "id": "10.3", "text": "Sets ValidArgs with the known resource names" },
|
||||
{
|
||||
"id": "10.4",
|
||||
"text": "Does NOT write a fully manual validator function for the combined check"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "cobra-vs-viper-distinction",
|
||||
"description": "Tests understanding of what cobra does vs what viper does",
|
||||
"prompt": "I'm starting a Go CLI project. I need subcommands, flags, shell completions, AND the ability to read configuration from a YAML file and environment variables. I've heard of cobra and viper. Which library handles which concern?",
|
||||
"trap": "Without the skill, the model may conflate the two or understate how they integrate. The correct answer clearly assigns cobra=command tree/flags/completions and viper=layered config resolution, with BindPFlag as the integration seam.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "11.1",
|
||||
"text": "Assigns cobra to command tree, flags, arg validation, shell completions"
|
||||
},
|
||||
{
|
||||
"id": "11.2",
|
||||
"text": "Assigns viper to config file, env var, and layered value resolution"
|
||||
},
|
||||
{
|
||||
"id": "11.3",
|
||||
"text": "Identifies BindPFlag (or similar) as the integration seam between them"
|
||||
},
|
||||
{
|
||||
"id": "11.4",
|
||||
"text": "Explains they can be used independently (cobra without viper, or viper without cobra)"
|
||||
},
|
||||
{
|
||||
"id": "11.5",
|
||||
"text": "Does NOT say cobra reads config files or viper defines subcommands"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "cobra-cli-scaffolder",
|
||||
"description": "Tests knowledge of the cobra-cli scaffolding tool",
|
||||
"prompt": "I want to quickly scaffold a new Go CLI project with cobra. Is there a tool that generates the initial files and lets me add subcommands from the command line?",
|
||||
"trap": "Without the skill, the model may say to create files manually or use a generic project generator. The cobra-cli tool is the canonical scaffolder for cobra projects.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "12.1",
|
||||
"text": "Mentions cobra-cli (github.com/spf13/cobra-cli)"
|
||||
},
|
||||
{
|
||||
"id": "12.2",
|
||||
"text": "Shows 'cobra-cli init <project>' for initialization"
|
||||
},
|
||||
{
|
||||
"id": "12.3",
|
||||
"text": "Shows 'cobra-cli add <command>' for adding subcommands"
|
||||
},
|
||||
{
|
||||
"id": "12.4",
|
||||
"text": "Explains that cobra-cli is separate from cobra itself (different import path)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "stringarray-vs-stringslice-commas",
|
||||
"description": "Tests StringArray vs StringSlice when flag values contain commas",
|
||||
"prompt": "My Go cobra CLI has a --label flag that users pass multiple times like --label 'env=prod,region=us'. With my current setup, passing --label 'env=prod,region=us' results in two separate values ['env=prod', 'region=us'] instead of one. What flag type should I use?",
|
||||
"trap": "Without the skill, the model uses StringSlice which splits on commas. StringArray is the correct choice when values may legitimately contain commas.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "13.1",
|
||||
"text": "Recommends StringArray (or StringArrayVar) instead of StringSlice"
|
||||
},
|
||||
{
|
||||
"id": "13.2",
|
||||
"text": "Explains that StringSlice splits on commas while StringArray does not"
|
||||
},
|
||||
{
|
||||
"id": "13.3",
|
||||
"text": "Does NOT suggest quoting or escaping commas as the fix"
|
||||
},
|
||||
{
|
||||
"id": "13.4",
|
||||
"text": "Shows the correct flag definition using StringArray or StringArrayVar"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "mutually-exclusive-flags",
|
||||
"description": "Tests MarkFlagsMutuallyExclusive instead of manual RunE checks",
|
||||
"prompt": "My Go cobra command has --json and --yaml flags for output format. Users should only be able to pass one of them. How do I prevent both from being passed at the same time?",
|
||||
"trap": "Without the skill, the model writes an if statement checking both flags inside RunE. The correct approach is MarkFlagsMutuallyExclusive which cobra enforces at parse time before RunE.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "14.1",
|
||||
"text": "Calls cmd.MarkFlagsMutuallyExclusive(\"json\", \"yaml\")"
|
||||
},
|
||||
{
|
||||
"id": "14.2",
|
||||
"text": "Does NOT write a manual if-both-set check inside RunE"
|
||||
},
|
||||
{
|
||||
"id": "14.3",
|
||||
"text": "Explains cobra enforces this at flag parse time and returns a standard error"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "required-together-flags",
|
||||
"description": "Tests MarkFlagsRequiredTogether instead of manual RunE checks",
|
||||
"prompt": "My Go cobra command has --tls-cert and --tls-key flags. If a user provides one, they must provide the other. How do I enforce this constraint?",
|
||||
"trap": "Without the skill, the model writes manual validation in RunE checking if one is set without the other. MarkFlagsRequiredTogether enforces this at cobra's parse stage.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "15.1",
|
||||
"text": "Calls cmd.MarkFlagsRequiredTogether(\"tls-cert\", \"tls-key\")"
|
||||
},
|
||||
{
|
||||
"id": "15.2",
|
||||
"text": "Does NOT write manual if-one-without-the-other checks inside RunE"
|
||||
},
|
||||
{
|
||||
"id": "15.3",
|
||||
"text": "Explains cobra validates this before RunE runs"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "one-required-flag-group",
|
||||
"description": "Tests MarkFlagsOneRequired instead of manual RunE checks",
|
||||
"prompt": "My Go cobra command accepts input from either --file or --stdin. At least one must be provided. How do I enforce that the user passes at least one of them?",
|
||||
"trap": "Without the skill, the model checks flag presence inside RunE. MarkFlagsOneRequired enforces at parse time with a standard cobra error.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "16.1",
|
||||
"text": "Calls cmd.MarkFlagsOneRequired(\"file\", \"stdin\")"
|
||||
},
|
||||
{
|
||||
"id": "16.2",
|
||||
"text": "Does NOT write a manual check inside RunE for neither flag being set"
|
||||
},
|
||||
{
|
||||
"id": "16.3",
|
||||
"text": "Explains cobra enforces this before RunE runs"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "flag-changed-distinguish-explicit-zero",
|
||||
"description": "Tests cmd.Flags().Changed() to distinguish explicit zero from absent flag",
|
||||
"prompt": "My Go cobra command has a --timeout flag defaulting to 30s. Users can pass --timeout 0 to disable timeouts entirely. In RunE, how do I tell whether the user explicitly passed --timeout 0 or simply didn't pass --timeout at all?",
|
||||
"trap": "Without the skill, the model checks if timeout == 0, which conflates the two cases. The correct approach is cmd.Flags().Changed(\"timeout\") which returns true only when the user explicitly provided the flag.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "17.1",
|
||||
"text": "Uses cmd.Flags().Changed(\"timeout\") to detect explicit user input"
|
||||
},
|
||||
{
|
||||
"id": "17.2",
|
||||
"text": "Does NOT use if timeout == 0 as the sole distinguishing condition"
|
||||
},
|
||||
{
|
||||
"id": "17.3",
|
||||
"text": "Explains Changed() returns true only when the flag was explicitly set by the user"
|
||||
},
|
||||
{
|
||||
"id": "17.4",
|
||||
"text": "Shows the pattern: if Changed → apply value, else → use default behavior"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "postrunE-success-only-use-defer",
|
||||
"description": "Tests that PostRunE runs only on RunE success and defer is the right cleanup pattern",
|
||||
"prompt": "My Go cobra command opens a database connection early in RunE and I want to close it when the command finishes, whether it succeeds or fails. I added cleanup in PostRunE but noticed it doesn't run when RunE returns an error. What's the right pattern?",
|
||||
"trap": "Without the skill, the model may suggest PersistentPostRunE or not know PostRunE is success-only. The correct pattern is defer inside RunE for guaranteed cleanup regardless of outcome.",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "18.1",
|
||||
"text": "Uses defer inside RunE to guarantee cleanup on both success and failure"
|
||||
},
|
||||
{
|
||||
"id": "18.2",
|
||||
"text": "Explains PostRunE only runs when RunE returns nil (success)"
|
||||
},
|
||||
{
|
||||
"id": "18.3",
|
||||
"text": "Does NOT present PostRunE as a solution for failure cleanup"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "errOrStderr-not-os-stderr",
|
||||
"description": "Tests cmd.ErrOrStderr() instead of os.Stderr for capturable error output",
|
||||
"prompt": "My Go cobra command writes diagnostic details to stderr using fmt.Fprintf(os.Stderr, ...) before returning an error. This works fine at runtime but my tests can't capture the stderr output. How do I fix this?",
|
||||
"trap": "Without the skill, the model uses os.Stderr directly. The correct approach is cmd.ErrOrStderr() which tests can redirect via rootCmd.SetErr(buf).",
|
||||
"assertions": [
|
||||
{
|
||||
"id": "19.1",
|
||||
"text": "Replaces os.Stderr with cmd.ErrOrStderr() as the write target"
|
||||
},
|
||||
{ "id": "19.2", "text": "Does NOT use os.Stderr directly" },
|
||||
{
|
||||
"id": "19.3",
|
||||
"text": "Shows rootCmd.SetErr(buf) in the test to capture stderr output"
|
||||
},
|
||||
{
|
||||
"id": "19.4",
|
||||
"text": "Explains the symmetry with cmd.OutOrStdout() / SetOut for stdout"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,157 @@
|
||||
# Cobra Commands, Hooks, and Args Validators
|
||||
|
||||
## The Run\* lifecycle
|
||||
|
||||
Cobra commands have five run hooks. Cobra executes them in this fixed order:
|
||||
|
||||
```
|
||||
PersistentPreRunE → PreRunE → RunE → PostRunE → PersistentPostRunE
|
||||
```
|
||||
|
||||
Each `*E` hook returns `error`. The non-`*E` variants (`PersistentPreRun`, `PreRun`, `Run`, `PostRun`, `PersistentPostRun`) have signature `func(cmd *cobra.Command, args []string)` — they cannot signal failure without `os.Exit` or panic. **Always use the `*E` variants.**
|
||||
|
||||
### Which hook to use
|
||||
|
||||
| Hook | Scope | When to use |
|
||||
| --- | --- | --- |
|
||||
| `PersistentPreRunE` | Parent + all descendants | Config init, auth check, telemetry setup — must run before every subcommand |
|
||||
| `PreRunE` | This command only | Validation that runs only for this command before `RunE` |
|
||||
| `RunE` | This command only | Main handler — the primary business logic |
|
||||
| `PostRunE` | This command only | Cleanup that runs only if `RunE` succeeded |
|
||||
| `PersistentPostRunE` | Parent + all descendants | Global cleanup (close connections, flush buffers) |
|
||||
|
||||
### Inheritance rules
|
||||
|
||||
`PersistentPreRunE` defined on the root command runs before every subcommand. But if a child command defines **its own** `PersistentPreRunE`, it **replaces** (does not chain) the parent's hook. Call the parent explicitly if you need both:
|
||||
|
||||
```go
|
||||
var childCmd = &cobra.Command{
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// call parent's hook first
|
||||
if err := rootCmd.PersistentPreRunE(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
// child-specific logic
|
||||
return nil
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Execution stops on first error
|
||||
|
||||
If `PersistentPreRunE` returns an error, cobra stops — `RunE` and later hooks never run. Use this for fail-fast auth checks.
|
||||
|
||||
## Args validators
|
||||
|
||||
Args validators run before `RunE`. Cobra prints a clear error message and exits without calling `RunE` when validation fails.
|
||||
|
||||
### Built-in validators
|
||||
|
||||
```go
|
||||
cobra.NoArgs // fails if any positional args provided
|
||||
cobra.ArbitraryArgs // accepts any number of args (default)
|
||||
cobra.ExactArgs(n int) // requires exactly n args
|
||||
cobra.MinimumNArgs(n int) // requires at least n args
|
||||
cobra.MaximumNArgs(n int) // requires at most n args
|
||||
cobra.RangeArgs(min, max int) // requires between min and max args
|
||||
cobra.OnlyValidArgs // all args must be in ValidArgs list
|
||||
cobra.ExactValidArgs(n int) // exactly n args, all in ValidArgs
|
||||
```
|
||||
|
||||
### Composing validators with MatchAll
|
||||
|
||||
```go
|
||||
var deleteCmd = &cobra.Command{
|
||||
Use: "delete <resource>",
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
ValidArgs: []string{"pod", "service", "deployment"},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return doDelete(args[0])
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Custom validators
|
||||
|
||||
Signature: `func(cmd *cobra.Command, args []string) error`
|
||||
|
||||
```go
|
||||
func validatePositiveInt(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("requires exactly 1 arg, got %d", len(args))
|
||||
}
|
||||
n, err := strconv.Atoi(args[0])
|
||||
if err != nil || n <= 0 {
|
||||
return fmt.Errorf("argument must be a positive integer, got %q", args[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Args: validatePositiveInt,
|
||||
RunE: func(cmd *cobra.Command, args []string) error { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
Combine custom validators with built-in ones using `MatchAll`:
|
||||
|
||||
```go
|
||||
Args: cobra.MatchAll(cobra.MinimumNArgs(1), validateAllPositive),
|
||||
```
|
||||
|
||||
## Command registration and ordering
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// groups must be registered before AddCommand
|
||||
rootCmd.AddGroup(&cobra.Group{ID: "core", Title: "Core Commands:"})
|
||||
rootCmd.AddGroup(&cobra.Group{ID: "management", Title: "Management Commands:"})
|
||||
|
||||
serveCmd.GroupID = "core"
|
||||
migrateCmd.GroupID = "management"
|
||||
|
||||
rootCmd.AddCommand(serveCmd, migrateCmd, versionCmd)
|
||||
}
|
||||
```
|
||||
|
||||
`versionCmd` has no `GroupID` — it appears in the default section.
|
||||
|
||||
## Annotations
|
||||
|
||||
Cobra supports arbitrary command annotations for framework-level metadata:
|
||||
|
||||
```go
|
||||
var serveCmd = &cobra.Command{
|
||||
Annotations: map[string]string{
|
||||
"category": "network",
|
||||
"requires-auth": "true",
|
||||
},
|
||||
}
|
||||
|
||||
// read in a middleware hook:
|
||||
if serveCmd.Annotations["requires-auth"] == "true" {
|
||||
// enforce auth
|
||||
}
|
||||
```
|
||||
|
||||
## Hidden and deprecated commands
|
||||
|
||||
```go
|
||||
var internalCmd = &cobra.Command{
|
||||
Hidden: true, // not shown in help, still executable
|
||||
}
|
||||
|
||||
var oldCmd = &cobra.Command{
|
||||
Deprecated: "use `newcmd` instead", // shown in help, prints warning on use
|
||||
}
|
||||
```
|
||||
|
||||
## cobra.CheckErr
|
||||
|
||||
`cobra.CheckErr(err)` is a convenience function: if `err != nil`, it prints the error to `cmd.ErrOrStderr()` and calls `os.Exit(1)`. Use it only in `main()` where you want a hard exit — not inside `RunE` where returning the error is preferred.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
cobra.CheckErr(rootCmd.Execute())
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,119 @@
|
||||
# Cobra Shell Completions Reference
|
||||
|
||||
Cobra generates shell completion scripts for bash, zsh, fish, and PowerShell automatically. Subcommand names and flag names are completed for free. You add completions for flag values and positional arguments.
|
||||
|
||||
## Built-in completion command
|
||||
|
||||
Cobra registers a `completion` subcommand automatically:
|
||||
|
||||
```bash
|
||||
myapp completion bash # generate bash script
|
||||
myapp completion zsh # generate zsh script
|
||||
myapp completion fish # generate fish script
|
||||
myapp completion powershell
|
||||
|
||||
# Install (example for zsh):
|
||||
myapp completion zsh > "${fpath[1]}/_myapp"
|
||||
```
|
||||
|
||||
## ShellCompDirective
|
||||
|
||||
The `ShellCompDirective` controls shell behavior after your completion function returns:
|
||||
|
||||
| Directive | Meaning |
|
||||
| --- | --- |
|
||||
| `ShellCompDirectiveDefault` | Fall back to file completion after your results |
|
||||
| `ShellCompDirectiveNoFileComp` | Disable file completion fallback |
|
||||
| `ShellCompDirectiveNoSpace` | Don't add a space after the completion |
|
||||
| `ShellCompDirectiveFilterFileExt(exts)` | Only show files with given extensions |
|
||||
| `ShellCompDirectiveFilterDirs(dirs)` | Only show directories |
|
||||
| `ShellCompDirectiveError` | Signal an error (show no completions) |
|
||||
|
||||
Combine with bitwise OR: `cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace`.
|
||||
|
||||
Use `ShellCompDirectiveNoFileComp` whenever your list is exhaustive — it prevents the shell from appending irrelevant files.
|
||||
|
||||
## Static arg completions
|
||||
|
||||
```go
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get <resource>",
|
||||
ValidArgs: []string{"pod", "service", "deployment", "configmap"},
|
||||
Args: cobra.OnlyValidArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error { /* ... */ },
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic arg completions
|
||||
|
||||
```go
|
||||
var getCmd = &cobra.Command{
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) > 0 {
|
||||
// first arg already provided — no more completions
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
resources, err := listResources(toComplete)
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
return resources, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`toComplete` is the prefix the user has typed so far — filter your results by it for responsive completions.
|
||||
|
||||
## Flag value completions
|
||||
|
||||
```go
|
||||
func init() {
|
||||
rootCmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return []string{"json\tJSON output", "yaml\tYAML output", "table\tTable output"}, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
|
||||
rootCmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
ns, err := listNamespaces()
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
return ns, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Descriptions after `\t` are shown in zsh and fish menus.
|
||||
|
||||
## Completion annotations
|
||||
|
||||
Mark a flag to complete as a file or directory:
|
||||
|
||||
```go
|
||||
cmd.Flags().String("config", "", "config file")
|
||||
cmd.MarkFlagFilename("config", "yaml", "yml", "json") // only those extensions
|
||||
|
||||
cmd.Flags().String("dir", "", "output directory")
|
||||
cmd.MarkFlagDirname("dir")
|
||||
```
|
||||
|
||||
## Testing completions
|
||||
|
||||
```go
|
||||
func TestCompletion(t *testing.T) {
|
||||
rootCmd.SetArgs([]string{"__complete", "get", ""})
|
||||
buf := new(bytes.Buffer)
|
||||
rootCmd.SetOut(buf)
|
||||
rootCmd.Execute()
|
||||
assert.Contains(t, buf.String(), "pod")
|
||||
assert.Contains(t, buf.String(), "service")
|
||||
}
|
||||
```
|
||||
|
||||
`__complete` is cobra's internal completion request verb. Pass the partial args as additional arguments.
|
||||
|
||||
## Disabling the completion command
|
||||
|
||||
```go
|
||||
rootCmd.CompletionOptions.DisableDefaultCmd = true // remove the completion subcommand
|
||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true // keep it but hide from help
|
||||
```
|
||||
@@ -0,0 +1,125 @@
|
||||
# Cobra Flags Reference
|
||||
|
||||
Cobra delegates all flag parsing to `github.com/spf13/pflag`. `cobra.Command` exposes two `*pflag.FlagSet`s:
|
||||
|
||||
- `cmd.Flags()` — local flags, only available on this command.
|
||||
- `cmd.PersistentFlags()` — inherited by all subcommands.
|
||||
|
||||
## Common flag types
|
||||
|
||||
```go
|
||||
// String
|
||||
cmd.Flags().String("name", "default", "description")
|
||||
cmd.Flags().StringP("name", "n", "default", "description") // with shorthand
|
||||
|
||||
// With pointer binding (no Lookup needed later)
|
||||
var name string
|
||||
cmd.Flags().StringVar(&name, "name", "default", "description")
|
||||
cmd.Flags().StringVarP(&name, "name", "n", "default", "description")
|
||||
|
||||
// Other types follow the same pattern:
|
||||
cmd.Flags().Int / IntVar / IntVarP
|
||||
cmd.Flags().Bool / BoolVar / BoolVarP
|
||||
cmd.Flags().Float64 / Float64Var
|
||||
cmd.Flags().Duration / DurationVar // parses "1h30m", "500ms"
|
||||
cmd.Flags().StringSlice / StringSliceVar // comma-separated or repeated flags
|
||||
cmd.Flags().StringArray / StringArrayVar // repeated flags only (no comma splitting)
|
||||
cmd.Flags().IntSlice / IntSliceVar
|
||||
cmd.Flags().StringToString // --label key=value --label k2=v2
|
||||
```
|
||||
|
||||
## StringSlice vs StringArray
|
||||
|
||||
| Flag type | Input | Result |
|
||||
| ------------- | --------------------- | --------------------------------- |
|
||||
| `StringSlice` | `--tags a,b --tags c` | `["a", "b", "c"]` — commas split |
|
||||
| `StringArray` | `--tags a,b --tags c` | `["a,b", "c"]` — commas NOT split |
|
||||
|
||||
Use `StringArray` when values may legitimately contain commas.
|
||||
|
||||
## Flag constraints
|
||||
|
||||
```go
|
||||
// Fail if flag not provided
|
||||
cmd.MarkFlagRequired("output")
|
||||
|
||||
// Fail if both provided
|
||||
cmd.MarkFlagsMutuallyExclusive("json", "yaml", "table")
|
||||
|
||||
// Fail if none provided
|
||||
cmd.MarkFlagsOneRequired("file", "stdin")
|
||||
|
||||
// Require flag only if another flag is set
|
||||
cmd.MarkFlagsMutuallyExclusive("tls", "no-tls")
|
||||
```
|
||||
|
||||
## Persistent flag patterns
|
||||
|
||||
```go
|
||||
func init() {
|
||||
// global flags on root
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: $HOME/.myapp.yaml)")
|
||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "log level (debug, info, warn, error)")
|
||||
|
||||
// bind to viper immediately after defining
|
||||
viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
|
||||
viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level"))
|
||||
}
|
||||
```
|
||||
|
||||
## Custom flag value types
|
||||
|
||||
Implement `pflag.Value` to parse arbitrary types:
|
||||
|
||||
```go
|
||||
type enumValue struct {
|
||||
val string
|
||||
allowed []string
|
||||
}
|
||||
|
||||
func (e *enumValue) String() string { return e.val }
|
||||
func (e *enumValue) Type() string { return "enum" }
|
||||
func (e *enumValue) Set(s string) error {
|
||||
for _, a := range e.allowed {
|
||||
if s == a {
|
||||
e.val = s
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("must be one of %v", e.allowed)
|
||||
}
|
||||
|
||||
var outputFmt = &enumValue{val: "table", allowed: []string{"table", "json", "yaml"}}
|
||||
cmd.Flags().Var(outputFmt, "output", "output format (table, json, yaml)")
|
||||
```
|
||||
|
||||
## Flag groups (required together)
|
||||
|
||||
Mark a set of flags that must all be provided if any one of them is provided:
|
||||
|
||||
```go
|
||||
cmd.Flags().String("tls-cert", "", "TLS certificate file")
|
||||
cmd.Flags().String("tls-key", "", "TLS key file")
|
||||
cmd.MarkFlagsRequiredTogether("tls-cert", "tls-key")
|
||||
```
|
||||
|
||||
## Accessing flag values
|
||||
|
||||
Prefer pointer binding (`StringVar`, `IntVar`, etc.) for type-safe access. When you need the flag post-parse:
|
||||
|
||||
```go
|
||||
port, err := cmd.Flags().GetInt("port")
|
||||
name, err := cmd.Flags().GetString("name")
|
||||
tags, err := cmd.Flags().GetStringSlice("tags")
|
||||
```
|
||||
|
||||
## Flag changed vs default
|
||||
|
||||
```go
|
||||
if cmd.Flags().Changed("port") {
|
||||
// user explicitly provided --port
|
||||
// useful when distinguishing "user set 0" from "flag not provided"
|
||||
}
|
||||
```
|
||||
|
||||
`Changed()` is also how viper knows which flags are explicit overrides — it only promotes a flag to the highest precedence layer if `Changed()` is true.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Cobra Documentation and Scaffolding Generators
|
||||
|
||||
## Doc generation
|
||||
|
||||
Cobra can generate documentation from your command tree in multiple formats. Import the `cobra/doc` sub-package:
|
||||
|
||||
```bash
|
||||
go get github.com/spf13/cobra/doc
|
||||
```
|
||||
|
||||
### Markdown
|
||||
|
||||
```go
|
||||
import "github.com/spf13/cobra/doc"
|
||||
|
||||
err := doc.GenMarkdownTree(rootCmd, "/tmp/docs/")
|
||||
// generates /tmp/docs/myapp.md, /tmp/docs/myapp_serve.md, etc.
|
||||
|
||||
// Single command
|
||||
var buf bytes.Buffer
|
||||
doc.GenMarkdown(rootCmd, &buf)
|
||||
```
|
||||
|
||||
### Man pages
|
||||
|
||||
```go
|
||||
header := &doc.GenManHeader{
|
||||
Title: "MYAPP",
|
||||
Section: "1",
|
||||
Date: &time.Time{},
|
||||
Source: "myapp v1.0.0",
|
||||
Manual: "User Commands",
|
||||
}
|
||||
err := doc.GenManTree(rootCmd, header, "/usr/local/share/man/man1/")
|
||||
```
|
||||
|
||||
### YAML
|
||||
|
||||
```go
|
||||
err := doc.GenYamlTree(rootCmd, "/tmp/docs/")
|
||||
```
|
||||
|
||||
### RST (reStructuredText)
|
||||
|
||||
```go
|
||||
err := doc.GenReSTTree(rootCmd, "/tmp/docs/")
|
||||
```
|
||||
|
||||
## cobra-cli scaffolder
|
||||
|
||||
`cobra-cli` generates command files and wires them into your project:
|
||||
|
||||
```bash
|
||||
go get -tool github.com/spf13/cobra-cli@latest
|
||||
|
||||
# Initialize a new cobra project
|
||||
go tool cobra-cli init myapp
|
||||
|
||||
# Add a subcommand
|
||||
go tool cobra-cli add serve
|
||||
go tool cobra-cli add migrate
|
||||
|
||||
# Add with a parent other than root
|
||||
cobra-cli add list --parent serve
|
||||
```
|
||||
|
||||
Generated files follow the standard pattern:
|
||||
|
||||
```go
|
||||
// cmd/serve.go
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "A brief description of your command",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
```
|
||||
|
||||
`cobra-cli` is optional — many teams write command files by hand following the same pattern.
|
||||
|
||||
## Help and usage template customization
|
||||
|
||||
Override the default help template:
|
||||
|
||||
```go
|
||||
rootCmd.SetHelpTemplate(`
|
||||
Usage: {{.UseLine}}
|
||||
{{if .HasAvailableSubCommands}}
|
||||
Commands:
|
||||
{{range .Commands}}{{if .IsAvailableCommand}} {{rpad .Name .NamePadding }} {{.Short}}
|
||||
{{end}}{{end}}{{end}}
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimRightSpace}}
|
||||
`)
|
||||
```
|
||||
|
||||
Override the usage function entirely:
|
||||
|
||||
```go
|
||||
rootCmd.SetUsageFunc(func(cmd *cobra.Command) error {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Custom usage for %s\n", cmd.Name())
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
Common template functions available: `rpad`, `trimRightSpace`, `gt`, `eq`.
|
||||
@@ -0,0 +1,154 @@
|
||||
# Testing Cobra Commands
|
||||
|
||||
## Basic test pattern
|
||||
|
||||
```go
|
||||
func TestServeCmd(t *testing.T) {
|
||||
stdout := new(bytes.Buffer)
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
rootCmd.SetOut(stdout)
|
||||
rootCmd.SetErr(stderr)
|
||||
rootCmd.SetArgs([]string{"serve", "--port", "9090", "--dry-run"})
|
||||
|
||||
err := rootCmd.Execute()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, stdout.String(), "listening on :9090")
|
||||
assert.Empty(t, stderr.String())
|
||||
}
|
||||
```
|
||||
|
||||
## Isolation between tests
|
||||
|
||||
Cobra accumulates flag state across `Execute()` calls on the same command instance. Tests must be isolated.
|
||||
|
||||
### Option 1: Re-create the command tree per test (recommended for unit tests)
|
||||
|
||||
```go
|
||||
func newRootCmd() *cobra.Command {
|
||||
root := &cobra.Command{Use: "myapp", SilenceUsage: true, SilenceErrors: true}
|
||||
root.AddCommand(newServeCmd())
|
||||
return root
|
||||
}
|
||||
|
||||
func TestServeCmd(t *testing.T) {
|
||||
root := newRootCmd()
|
||||
root.SetArgs([]string{"serve", "--port", "9090"})
|
||||
err := root.Execute()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Reset flags between tests
|
||||
|
||||
```go
|
||||
func TestWithReset(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
rootCmd.ResetFlags()
|
||||
// re-define flags if needed
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Re-creating is safer — `ResetFlags` only clears the flag set, not subcommand state.
|
||||
|
||||
## Testing commands that write output
|
||||
|
||||
Commands must use `cmd.OutOrStdout()` / `cmd.ErrOrStderr()` instead of `os.Stdout` / `os.Stderr` for this to work.
|
||||
|
||||
```go
|
||||
// In command handler:
|
||||
func runServe(cmd *cobra.Command, args []string) error {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "Server started")
|
||||
fmt.Fprintln(cmd.ErrOrStderr(), "Debug: listening on port 8080")
|
||||
return nil
|
||||
}
|
||||
|
||||
// In test:
|
||||
buf := new(bytes.Buffer)
|
||||
rootCmd.SetOut(buf)
|
||||
rootCmd.Execute()
|
||||
assert.Contains(t, buf.String(), "Server started")
|
||||
```
|
||||
|
||||
## Golden file tests
|
||||
|
||||
For commands with structured or lengthy output, use golden files:
|
||||
|
||||
```go
|
||||
func TestOutputFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
rootCmd.SetOut(buf)
|
||||
rootCmd.SetArgs([]string{"list", "--output", "json"})
|
||||
require.NoError(t, rootCmd.Execute())
|
||||
|
||||
golden := "testdata/list-json.golden"
|
||||
if *update { // -update flag
|
||||
os.WriteFile(golden, buf.Bytes(), 0644)
|
||||
}
|
||||
want, _ := os.ReadFile(golden)
|
||||
assert.Equal(t, string(want), buf.String())
|
||||
}
|
||||
```
|
||||
|
||||
Run with `-update` to regenerate golden files after intentional output changes.
|
||||
|
||||
## Testing error paths
|
||||
|
||||
```go
|
||||
func TestInvalidArgs(t *testing.T) {
|
||||
stderr := new(bytes.Buffer)
|
||||
rootCmd.SetErr(stderr)
|
||||
rootCmd.SetArgs([]string{"delete"}) // missing required arg
|
||||
|
||||
err := rootCmd.Execute()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accepts 1 arg")
|
||||
}
|
||||
```
|
||||
|
||||
## Table-driven command tests
|
||||
|
||||
```go
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantOut string
|
||||
wantErr bool
|
||||
}{
|
||||
{"no flags", []string{"serve"}, "listening on :8080", false},
|
||||
{"custom port", []string{"serve", "--port", "9090"}, "listening on :9090", false},
|
||||
{"invalid port", []string{"serve", "--port", "abc"}, "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
root := newRootCmd() // fresh command tree per test
|
||||
buf := new(bytes.Buffer)
|
||||
root.SetOut(buf)
|
||||
root.SetArgs(tt.args)
|
||||
err := root.Execute()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, buf.String(), tt.wantOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Testing completions
|
||||
|
||||
```go
|
||||
func TestCompletion(t *testing.T) {
|
||||
root := newRootCmd()
|
||||
buf := new(bytes.Buffer)
|
||||
root.SetOut(buf)
|
||||
root.SetArgs([]string{"__complete", "delete", ""})
|
||||
root.Execute()
|
||||
|
||||
assert.Contains(t, buf.String(), "pod")
|
||||
assert.Contains(t, buf.String(), "service")
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user