diff --git a/internal/cli/CLAUDE.md b/internal/cli/CLAUDE.md index 2829fa8..5a0b032 100644 --- a/internal/cli/CLAUDE.md +++ b/internal/cli/CLAUDE.md @@ -4,9 +4,9 @@ Argument-dispatch core shared by every verb. Defines the `Command` interface, th ## Files -- `cli.go` — `Command`, `IO`, `DefaultIO`, `Group` (nested-verb dispatcher with auto-generated help listing), and `dispatchChild` (the Group/Dispatch handoff that scans a resolved leaf's args for `--help`/`-h` and renders its `Help()` before `Run` is called, so every leaf gets the flag for free; Groups are skipped so the flag reaches the deepest leaf, and bare positional `help` is left alone so leaves can receive it as an argument). +- `cli.go` — `Command`, `IO`, `DefaultIO`, `Group` (nested-verb dispatcher with auto-generated help listing; optional `Flags` closure declares group-level flags that descend to every leaf via `WithAncestorFlags`), `dispatchChild` (the Group/Dispatch handoff that scans a resolved leaf's args for `--help`/`-h` and renders its `Help()` before `Run` is called — and if the leaf implements `Flagger`, appends a `Flags:` block enumerating own + ancestor flags via `renderFlagsAsText`; Groups are skipped so the flag reaches the deepest leaf, and bare positional `help` is left alone so leaves can receive it as an argument), `Flagger` (opt-in interface: leaves that implement `Flags(fs)` get the ancestor-aware `Flags:` block in `--help`), and `renderFlagsAsText` (sorted `--name usage (default: X)` enumeration). Precedence when ancestor and leaf declare the same name: **leaf wins**, because leaves call `ApplyAncestorFlags` AFTER declaring their own flags, and ancestor registrars use `DeclareString` / `DeclareBool` Lookup-guards that skip already-declared names (stdlib `flag.FlagSet.StringVar` panics on duplicate names, verified empirically). - `dispatch.go` — `Dispatch` (root entry: short-circuits help, parses globals, routes to the matching verb via `dispatchChild`) and `RootHelp`. -- `root.go` — `Global` shape, `WithGlobal`/`GlobalFrom` context helpers (both require a non-nil ctx per stdlib `context.WithValue` convention — nil panics), and `ParseGlobal` (strips known root flags from argv before verb dispatch). +- `root.go` — `Global` shape, `WithGlobal`/`GlobalFrom` context helpers (both require a non-nil ctx per stdlib `context.WithValue` convention — nil panics), `ParseGlobal` (strips known root flags from argv before verb dispatch), and the Phase 2 flag-registrar stack: `WithAncestorFlags(ctx, reg)` / `ApplyAncestorFlags(ctx, fs)` (context-carried slice of `func(*flag.FlagSet)` closures that `Group.Run` appends to and leaves replay on their own `FlagSet`), plus `DeclareString` / `DeclareBool` (Lookup-guarded wrappers ancestor closures use instead of raw `StringVar` / `BoolVar` to avoid the stdlib redeclaration panic when a leaf already declared the same name). - `flags.go` — `ParseFlags`, which tolerates positional args interleaved with flags (stdlib `FlagSet.Parse` stops at the first non-flag, silently dropping later flags); `FlagWasSet`, the `fs.Visit` wrapper partial-update verbs use to tell "user left this alone" from "user explicitly passed the zero value"; `RequireFlags`, which emits a single sorted `missing required flags: --a, --b` usage error for any name not explicitly set on fs; and three typed `flag.Value` constructors — `EnumFlag` (allow-list validation at parse time), `IntListFlag` (CSV → `[]int` with whitespace tolerance), `SinceFlag` (accepts non-negative `time.ParseDuration` or RFC3339, stored UTC via an injected clock). The stdlib `flag.Parse` re-wraps `Set` errors with `%v`, so the `ErrUsage` chain survives only through the outer `ParseFlags` wrap — tests that exercise these helpers must go through `ParseFlags`, not bare `fs.Parse`. - `token.go` — `Token` (named `string` type whose `String`/`Format` always render the `RedactToken` mask, so any accidental `%s`/`%v`/`%+v`/`%#v`/`%q` on a logger or error can't leak a bearer token) and its `Value()` escape hatch. `config.Profile.Token` and `auth.Config.Token` are declared as `cli.Token`; the only authorized raw-emit call sites are `cmd/ana/main.go`'s `tokenFn` (Authorization header) and `auth/keys.go:emitPlaintextToken` (one-shot API key print). Since the underlying kind is `string`, `encoding/json` marshals/unmarshals transparently and untyped string literals still work in tests (`cli.Token("t")` — or just `"t"` in a `Token`-typed field). - `helpers.go` — shared verb-package helpers extracted from per-verb duplicates: `NewFlagSet`, `UsageErrf`, `WriteJSON`, `Remarshal`, `RenderOutput` (generic JSON-vs-typed-render dispatch: `--json` → `WriteJSON`; else `Remarshal` into `*T` then call the render closure), `RequireStringID`, `RequireIntID`, `RenderTwoCol`, `ReadToken` (shared login/profile-add stdin reader with JWT-sized buffer boost; trims surrounding whitespace), `ReadPassword` (same JWT-sized buffer but strips ONLY the trailing line terminator — passwords can legitimately begin or end with whitespace, so surrounding bytes must not be mutated), `NewTableWriter` (canonical tabwriter config), `FirstLine`, `DashIfEmpty` (table-cell rendering primitives; fall-through chains use stdlib `cmp.Or`), `RedactToken` (masks bearer tokens for human-readable echoes — `(unset)` or `********** (last 4: xxxx)`). Phase 0 of the shared-cli-kit refactor; Phases 1–10 migrate each verb package over and delete its local copies. diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 12b4fd4..ea35228 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -6,6 +6,7 @@ package cli import ( "context" + "flag" "fmt" "io" "os" @@ -47,19 +48,34 @@ type Command interface { // Group is a Command that dispatches its first argument to a named child // Command. A Group can itself be registered as a child, enabling nested verbs // (e.g. `ana chat send ...`). +// +// Flags, if set, declares group-level flags that every descendant leaf +// inherits via the ctx-carried registrar stack (see WithAncestorFlags in +// root.go). The closure runs on a leaf's *flag.FlagSet AFTER the leaf has +// declared its own flags, so use DeclareString / DeclareBool (or an +// equivalent Lookup guard) to avoid the stdlib flag-redeclaration panic — the +// guard lets the leaf override a name when it wants to. type Group struct { Summary string + Flags func(*flag.FlagSet) Children map[string]Command } // Run dispatches to a child command. With no args or an explicit help flag it // prints Help() to stdout and returns ErrHelp (exit 0). An unknown child name // writes to stderr and returns ErrUsage (exit 1). +// +// If Flags is non-nil, Run appends it to the ctx-carried ancestor-flag stack +// before delegating so every descendant leaf can ApplyAncestorFlags and pick +// up the group's declared flags. func (g *Group) Run(ctx context.Context, args []string, stdio IO) error { if len(args) == 0 || isHelpArg(args[0]) { fmt.Fprintln(stdio.Stdout, g.Help()) return ErrHelp } + if g.Flags != nil { + ctx = WithAncestorFlags(ctx, g.Flags) + } name := args[0] child, ok := g.Children[name] if !ok { @@ -89,6 +105,17 @@ func dispatchChild(ctx context.Context, cmd Command, args []string, stdio IO) er for _, a := range args { if a == "-h" || a == "--help" { fmt.Fprintln(stdio.Stdout, cmd.Help()) + if fl, ok := cmd.(Flagger); ok { + fs := flag.NewFlagSet("help", flag.ContinueOnError) + fs.SetOutput(io.Discard) + fl.Flags(fs) + ApplyAncestorFlags(ctx, fs) + if block := renderFlagsAsText(fs); block != "" { + fmt.Fprintln(stdio.Stdout) + fmt.Fprintln(stdio.Stdout, "Flags:") + fmt.Fprint(stdio.Stdout, block) + } + } return ErrHelp } } @@ -96,8 +123,53 @@ func dispatchChild(ctx context.Context, cmd Command, args []string, stdio IO) er return cmd.Run(ctx, args, stdio) } +// renderFlagsAsText enumerates fs's flags sorted by name and renders one +// ` --name usage (default: X)` row per flag. Returns "" if fs has +// no flags. The trailing newline is included so callers can Fprint without +// worrying about terminator placement. +func renderFlagsAsText(fs *flag.FlagSet) string { + type row struct { + name, typ, usage, def string + } + var rows []row + fs.VisitAll(func(f *flag.Flag) { + typ, usage := flag.UnquoteUsage(f) + rows = append(rows, row{name: f.Name, typ: typ, usage: usage, def: f.DefValue}) + }) + if len(rows) == 0 { + return "" + } + slices.SortFunc(rows, func(a, b row) int { return strings.Compare(a.name, b.name) }) + nameWidth := 0 + for _, r := range rows { + w := len(r.name) + if r.typ != "" { + w += 1 + len(r.typ) + } + if w > nameWidth { + nameWidth = w + } + } + var b strings.Builder + for _, r := range rows { + head := "--" + r.name + if r.typ != "" { + head += " " + r.typ + } + fmt.Fprintf(&b, " %-*s %s", nameWidth+2, head, r.usage) + if r.def != "" && r.def != "false" && r.def != "0" { + fmt.Fprintf(&b, " (default: %s)", r.def) + } + b.WriteByte('\n') + } + return b.String() +} + // Help renders the group's summary (if set) followed by a sorted, two-column // listing of child commands and the first line of each child's own Help(). +// When Flags is set, a trailing "Flags:" block enumerates the group-level +// flags so `ana --help` surfaces inheritable flags even when the +// user hasn't drilled into a leaf. func (g *Group) Help() string { var b strings.Builder if g.Summary != "" { @@ -120,6 +192,15 @@ func (g *Group) Help() string { first := FirstLine(g.Children[n].Help()) fmt.Fprintf(&b, " %-*s %s\n", width, n, first) } + if g.Flags != nil { + fs := flag.NewFlagSet("help", flag.ContinueOnError) + fs.SetOutput(io.Discard) + g.Flags(fs) + if block := renderFlagsAsText(fs); block != "" { + b.WriteString("\nFlags:\n") + b.WriteString(block) + } + } // Trim trailing newline so callers can Fprintln without doubling blanks. return strings.TrimRight(b.String(), "\n") } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 52baf93..3b63fa2 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "flag" "fmt" "io" "os" @@ -594,3 +595,312 @@ func TestExitCode(t *testing.T) { var _ io.Reader = DefaultIO().Stdin var _ io.Writer = DefaultIO().Stdout var _ io.Writer = DefaultIO().Stderr + +// flagLeaf is a Flagger-implementing leaf used by the Phase 2 tests. It +// declares its own flags in Flags, then in Run declares them on a private +// FlagSet, calls ApplyAncestorFlags, and records what parsed. Keeping the +// declaration inside Flags (rather than repeating it inline in Run) mirrors +// the contract the real verb packages will adopt when they migrate. +type flagLeaf struct { + declareOwn func(fs *flag.FlagSet) + ancestorFS *flag.FlagSet + parsedArgs []string + ran bool +} + +func (l *flagLeaf) Flags(fs *flag.FlagSet) { + if l.declareOwn != nil { + l.declareOwn(fs) + } +} + +func (l *flagLeaf) Help() string { return "leaf help line" } + +func (l *flagLeaf) Run(ctx context.Context, args []string, _ IO) error { + fs := flag.NewFlagSet("leaf", flag.ContinueOnError) + fs.SetOutput(io.Discard) + l.Flags(fs) + ApplyAncestorFlags(ctx, fs) + l.ancestorFS = fs + if err := fs.Parse(args); err != nil { + return err + } + l.parsedArgs = fs.Args() + l.ran = true + return nil +} + +func TestWithAncestorFlagsPreservesOrder(t *testing.T) { + t.Parallel() + var order []string + ctx := context.Background() + ctx = WithAncestorFlags(ctx, func(fs *flag.FlagSet) { order = append(order, "outer") }) + ctx = WithAncestorFlags(ctx, func(fs *flag.FlagSet) { order = append(order, "inner") }) + fs := flag.NewFlagSet("t", flag.ContinueOnError) + ApplyAncestorFlags(ctx, fs) + if len(order) != 2 || order[0] != "outer" || order[1] != "inner" { + t.Errorf("order=%v want [outer inner]", order) + } +} + +func TestApplyAncestorFlagsNoRegistrars(t *testing.T) { + t.Parallel() + // Zero-registrar ctx must not panic — the absence path matters for + // leaves dispatched directly (not under a Group with Flags set). + fs := flag.NewFlagSet("t", flag.ContinueOnError) + ApplyAncestorFlags(context.Background(), fs) + // sanity: no flags were declared + count := 0 + fs.VisitAll(func(*flag.Flag) { count++ }) + if count != 0 { + t.Errorf("VisitAll count=%d want 0", count) + } +} + +func TestDeclareStringGuardsAgainstRedeclare(t *testing.T) { + t.Parallel() + fs := flag.NewFlagSet("t", flag.ContinueOnError) + var leafT, ancT string + fs.StringVar(&leafT, "foo", "leafdef", "leaf usage") + DeclareString(fs, &ancT, "foo", "ancdef", "anc usage") + if err := fs.Parse([]string{"--foo", "x"}); err != nil { + t.Fatalf("parse err=%v", err) + } + if leafT != "x" { + t.Errorf("leafT=%q want x", leafT) + } + if ancT != "" { + t.Errorf("ancT=%q want '' (ancestor should not have been bound)", ancT) + } +} + +func TestDeclareBoolGuardsAgainstRedeclare(t *testing.T) { + t.Parallel() + fs := flag.NewFlagSet("t", flag.ContinueOnError) + var leafT, ancT bool + fs.BoolVar(&leafT, "v", false, "leaf usage") + DeclareBool(fs, &ancT, "v", false, "anc usage") + if err := fs.Parse([]string{"--v"}); err != nil { + t.Fatalf("parse err=%v", err) + } + if !leafT { + t.Errorf("leafT=false want true") + } + if ancT { + t.Errorf("ancT=true want false (ancestor should not have been bound)") + } +} + +func TestDeclareBoolFreshDeclaration(t *testing.T) { + t.Parallel() + // When no prior declaration exists, DeclareBool must bind the target. + fs := flag.NewFlagSet("t", flag.ContinueOnError) + var target bool + DeclareBool(fs, &target, "v", false, "usage") + if err := fs.Parse([]string{"--v"}); err != nil { + t.Fatalf("parse err=%v", err) + } + if !target { + t.Errorf("target=false want true") + } +} + +func TestDeclareStringFreshDeclaration(t *testing.T) { + t.Parallel() + fs := flag.NewFlagSet("t", flag.ContinueOnError) + var target string + DeclareString(fs, &target, "s", "def", "usage") + if err := fs.Parse([]string{"--s", "x"}); err != nil { + t.Fatalf("parse err=%v", err) + } + if target != "x" { + t.Errorf("target=%q want x", target) + } +} + +func TestRenderFlagsAsTextDefaultAndEmptyType(t *testing.T) { + t.Parallel() + fs := flag.NewFlagSet("t", flag.ContinueOnError) + // Bool with zero-value default "false" → no "(default: X)" suffix + fs.Bool("v", false, "verbose flag") + // String with non-empty default → suffix emitted + fs.String("name", "mydef", "a `NAME`") + // String with empty default → no suffix + fs.String("other", "", "other flag") + got := renderFlagsAsText(fs) + if !strings.Contains(got, "--name") || !strings.Contains(got, "(default: mydef)") { + t.Errorf("expected --name with default: %q", got) + } + if strings.Contains(got, "--v ") && strings.Contains(got, "(default: false)") { + t.Errorf("bool false default should not render: %q", got) + } + if strings.Contains(got, "--other ") && strings.Contains(got, "(default:") { + t.Errorf("empty string default should not render: %q", got) + } +} + +func TestGroupFlagsPropagateToLeaf(t *testing.T) { + t.Parallel() + var leafFoo string + leaf := &flagLeaf{declareOwn: func(fs *flag.FlagSet) { + // leaf-only flag so ancestor's declaration wins via DeclareString + fs.StringVar(new(string), "leaf-only", "", "leaf-only flag") + }} + g := &Group{ + Flags: func(fs *flag.FlagSet) { + DeclareString(fs, &leafFoo, "foo", "", "inherited foo flag") + }, + Children: map[string]Command{"leaf": leaf}, + } + stdio, _, _ := testIO() + err := g.Run(context.Background(), []string{"leaf", "--foo", "x", "--leaf-only", "y"}, stdio) + if err != nil { + t.Fatalf("err=%v", err) + } + if !leaf.ran { + t.Fatalf("leaf did not run") + } + if leafFoo != "x" { + t.Errorf("leafFoo=%q want x", leafFoo) + } +} + +func TestGroupFlagsVisibleInLeafHelp(t *testing.T) { + t.Parallel() + leaf := &flagLeaf{declareOwn: func(fs *flag.FlagSet) { + fs.String("leaf-only", "", "leaf flag `NAME`") + }} + g := &Group{ + Flags: func(fs *flag.FlagSet) { + DeclareString(fs, new(string), "foo", "", "the foo `VALUE`") + }, + Children: map[string]Command{"leaf": leaf}, + } + stdio, out, _ := testIO() + err := g.Run(context.Background(), []string{"leaf", "--help"}, stdio) + if !errors.Is(err, ErrHelp) { + t.Fatalf("err=%v want ErrHelp", err) + } + s := out.String() + if !strings.Contains(s, "--foo") { + t.Errorf("leaf --help missing ancestor --foo: %q", s) + } + if !strings.Contains(s, "--leaf-only") { + t.Errorf("leaf --help missing leaf --leaf-only: %q", s) + } + if !strings.Contains(s, "Flags:") { + t.Errorf("leaf --help missing Flags: header: %q", s) + } +} + +func TestGroupFlagsNestedTwoLevels(t *testing.T) { + t.Parallel() + var outerV, middleV string + leaf := &flagLeaf{declareOwn: func(fs *flag.FlagSet) { + fs.String("leaf-only", "", "leaf-only flag") + }} + middle := &Group{ + Flags: func(fs *flag.FlagSet) { + DeclareString(fs, &middleV, "middle", "", "middle flag") + }, + Children: map[string]Command{"leaf": leaf}, + } + outer := &Group{ + Flags: func(fs *flag.FlagSet) { + DeclareString(fs, &outerV, "outer", "", "outer flag") + }, + Children: map[string]Command{"mid": middle}, + } + stdio, _, _ := testIO() + err := outer.Run(context.Background(), + []string{"mid", "leaf", "--outer", "o", "--middle", "m", "--leaf-only", "l"}, stdio) + if err != nil { + t.Fatalf("err=%v", err) + } + if !leaf.ran { + t.Fatalf("leaf did not run") + } + if outerV != "o" { + t.Errorf("outerV=%q want o", outerV) + } + if middleV != "m" { + t.Errorf("middleV=%q want m", middleV) + } +} + +func TestGroupFlagsPrecedenceLeafWins(t *testing.T) { + t.Parallel() + // Both ancestor and leaf declare --foo; leaf declares FIRST (via its + // Flags method called from Run), so DeclareString in the ancestor + // registrar is a no-op. The leaf's target pointer receives the value. + var ancestorV string + var leafV string + leaf := &flagLeaf{declareOwn: func(fs *flag.FlagSet) { + fs.StringVar(&leafV, "foo", "leafdef", "leaf foo") + }} + g := &Group{ + Flags: func(fs *flag.FlagSet) { + DeclareString(fs, &ancestorV, "foo", "ancdef", "ancestor foo") + }, + Children: map[string]Command{"leaf": leaf}, + } + stdio, _, _ := testIO() + err := g.Run(context.Background(), []string{"leaf", "--foo", "x"}, stdio) + if err != nil { + t.Fatalf("err=%v", err) + } + if leafV != "x" { + t.Errorf("leafV=%q want x", leafV) + } + if ancestorV != "" { + t.Errorf("ancestorV=%q want '' (ancestor target should not bind)", ancestorV) + } +} + +func TestGroupFlagsHelpForGroupItself(t *testing.T) { + t.Parallel() + g := &Group{ + Summary: "group sum", + Flags: func(fs *flag.FlagSet) { + DeclareString(fs, new(string), "groupflag", "", "the group flag") + }, + Children: map[string]Command{"c": &fakeCmd{help: "c help"}}, + } + stdio, out, _ := testIO() + // "--help" on the group itself shows group help (including group Flags) + err := g.Run(context.Background(), []string{"--help"}, stdio) + if !errors.Is(err, ErrHelp) { + t.Fatalf("err=%v want ErrHelp", err) + } + s := out.String() + if !strings.Contains(s, "group sum") { + t.Errorf("group help missing summary: %q", s) + } + if !strings.Contains(s, "Flags:") { + t.Errorf("group help missing Flags: block: %q", s) + } + if !strings.Contains(s, "--groupflag") { + t.Errorf("group help missing --groupflag: %q", s) + } +} + +func TestRenderFlagsAsTextEmpty(t *testing.T) { + t.Parallel() + fs := flag.NewFlagSet("t", flag.ContinueOnError) + if got := renderFlagsAsText(fs); got != "" { + t.Errorf("empty fs should render '', got %q", got) + } +} + +func TestRenderFlagsAsTextSorted(t *testing.T) { + t.Parallel() + fs := flag.NewFlagSet("t", flag.ContinueOnError) + fs.String("zebra", "", "the zebra") + fs.String("alpha", "", "the alpha") + got := renderFlagsAsText(fs) + ai := strings.Index(got, "--alpha") + zi := strings.Index(got, "--zebra") + if ai < 0 || zi < 0 || ai >= zi { + t.Errorf("flags should be sorted alpha→zebra: %q", got) + } +} diff --git a/internal/cli/root.go b/internal/cli/root.go index 0bf0ea3..d23e2e5 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -7,6 +7,78 @@ import ( "io" ) +// flagRegistrar declares flags on a target FlagSet. Used for Group.Flags +// closures and by WithAncestorFlags to stack ancestor contributions that +// every descendant leaf can pull in. +type flagRegistrar = func(*flag.FlagSet) + +// ancestorFlagsKey is the unexported ctx key for the stack of ancestor flag +// registrars accumulated by Group.Run as dispatch descends. Each entry is +// called on a leaf's FlagSet from ApplyAncestorFlags so common flags +// declared on a Group (e.g. --name on a dialect subtree) appear on every +// child without per-leaf duplication. +type ancestorFlagsKey struct{} + +// WithAncestorFlags appends reg to the ctx-carried slice of ancestor flag +// registrars and returns the new context. Groups call this during Run so +// child commands inherit the Group's declared flags; the slice preserves +// registration order (outermost ancestor first) so leaf tests can reason +// about precedence by inserting guards. +// +// Per stdlib context.WithValue contract, ctx must not be nil. +func WithAncestorFlags(ctx context.Context, reg func(*flag.FlagSet)) context.Context { + prior, _ := ctx.Value(ancestorFlagsKey{}).([]flagRegistrar) + next := make([]flagRegistrar, 0, len(prior)+1) + next = append(next, prior...) + next = append(next, reg) + return context.WithValue(ctx, ancestorFlagsKey{}, next) +} + +// ApplyAncestorFlags runs every registered ancestor registrar on fs in the +// order they were appended (outermost first). Leaves call this AFTER +// declaring their own flags so leaf declarations populate fs first and each +// ancestor registrar can Lookup-guard its own additions — stdlib +// flag.FlagSet panics on duplicate declarations, and this ordering makes +// "leaf wins" fall out naturally. +// +// Callers that build ancestor registrars should wrap StringVar/BoolVar in +// the DeclareString / DeclareBool helpers (or equivalent Lookup guards) so +// they're safe when the leaf declared the same name. +func ApplyAncestorFlags(ctx context.Context, fs *flag.FlagSet) { + regs, _ := ctx.Value(ancestorFlagsKey{}).([]flagRegistrar) + for _, r := range regs { + r(fs) + } +} + +// DeclareString is a Lookup-guarded wrapper around fs.StringVar. Ancestor +// Group.Flags closures should use this (rather than raw StringVar) so a +// leaf that already declared the same name isn't clobbered by a duplicate +// declaration — the stdlib flag package panics in that case. +func DeclareString(fs *flag.FlagSet, target *string, name, def, usage string) { + if fs.Lookup(name) == nil { + fs.StringVar(target, name, def, usage) + } +} + +// DeclareBool is the bool counterpart to DeclareString. Same guard against +// panicking on duplicate names. +func DeclareBool(fs *flag.FlagSet, target *bool, name string, def bool, usage string) { + if fs.Lookup(name) == nil { + fs.BoolVar(target, name, def, usage) + } +} + +// Flagger is an optional opt-in for leaf commands whose help should include +// a flag enumeration that stacks ancestor-declared flags with the leaf's +// own. Leaves that implement Flags(fs) get an automatic Flags: block +// appended to their --help output by dispatchChild; leaves that don't +// implement it keep the current hand-written Help() as their sole source +// of usage text. +type Flagger interface { + Flags(fs *flag.FlagSet) +} + // Global holds the root-level flags that apply to every verb. Command // implementations read it from context via GlobalFrom; ParseGlobal produces // it from raw argv.