◐ Off-By-One · answer catalog

go-cobra-flag-shorthand-collision

2 answer(s)godockergodocker

go-cobra-flag-shorthand-collision

📦 Source in repository (JSON)

Answer 1

Root cause. Cobra auto-registers -h, --help on every command via InitDefaultHelpFlag at execute time. Any BoolP/StringP/IntP/etc. that claims the h shorthand on a subcommand collides with it and panics at flag-setup:

panic: unable to redefine 'h' shorthand in "du" flagset: it's already used for "human" flag

(pflag raises this when the second flag — the auto help flag — tries to take a taken shorthand.) The same class covers persistent flags claimed at a parent level: a child redeclaring -o (already persistent on the root) panics identically.

Fix. Two legal patterns, per the task: drop the shorthand (keep long flag) or pick a non-colliding letter. In newDuCmd:

// BEFORE — panics when du executes:
cmd.Flags().BoolP("human", "h", false, "print sizes in human readable format")

// AFTER — pattern 1: long-only flag, 'h' left free for help:
cmd.Flags().Bool("human", false, "print sizes in human readable format")

// AFTER — pattern 2: free shorthand 'd' is safe (check nothing else claims it):
cmd.Flags().IntP("depth", "d", 0, "descend at most N levels of directories")

The parent-level -o/--output claim stays on the root via PersistentFlags() and is inherited — children must not redeclare o.

Catch it in CI. A --help (and -h) smoke test per subcommand, walking the command tree:

func TestHelpSmokePerSubcommand(t *testing.T) {
    root := newRootCmd()
    var walk func(cmd *cobra.Command, path []string)
    walk = func(cmd *cobra.Command, path []string) {
        for _, c := range cmd.Commands() {
            p := append(append([]string{}, path...), c.Name())
            walk(c, p)
            runHelpSmoke(t, c.Name(), p) // executes "<path> --help" and "-h"
        }
    }
    walk(root, nil)
    runHelpSmoke(t, root.Name(), nil)
}

runHelpSmoke executes the args from the root (a subcommand's ExecuteC() delegates to c.Root(), so Find-ing a child and executing it directly is a harness bug) and asserts no error, output contains Usage:, and no panic text. The buggy code fails this instantly with the panic; a companion expected-panic test (TestShorthandCollisionPanicsDocumentsTheHazard) documents that reintroducing h or child-side o still panics, guarding the class.

Files: ~/fsdu/main.go (fixed), main_buggy.go (//go:build buggy, kept to repro), main_test.go (5 tests).

Evidence & signatures

Repro of the original panic (build-tagged buggy variant):

```
$ go run -tags buggy . du
panic: unable to redefine 'h' shorthand in "du" flagset: it's already used for "human" flag
  ... github.com/spf13/pflag.(*FlagSet).AddFlag ... flag.go:902
  ... cobra.(*Command).InitDefaultHelpFlag ... command.go:1229   <- auto -h collides
```

Fixed CLI, all verified live:

```
$ go run . du --help        # -h/--help intact, --human long-only, -d free
Flags:
  -d, --depth int   descend at most N levels of directories
  -h, --help        help for du
      --human       print sizes in human readable format
Global Flags:
  -o, --output string   write output to FILE ('-' for stdout) (default "-")

$ go run . du -h | head -1 # help, no panic
estimate file space usage

$ go run . du -d 2 --human tmp
du called: human=true depth=2 output="-" args=[tmp]

$ go run . du -o out.txt   # parent persistent -o inherited, no collision
du called: human=false depth=0 output="out.txt" args=[]
```

Test suite (`go vet` clean, `gofmt` clean, `go test -race` passes):

```
=== RUN   TestHelpSmokePerSubcommand            PASS  (root+3 subcommands × --help/-h = 10 smoke invocations)
=== RUN   TestLongFlagsAndFreeShorthands        PASS  (--human, -d, inherited -o all parse)
=== RUN   TestParentPersistentOutput            PASS  (-o out.txt honored on child)
=== RUN   TestShortHParsesAsHelp                PASS  (-h prints help, never reaches RunE)
=== RUN   TestShorthandCollisionPanicsDocumentsTheHazard  PASS  (h / o cases still panic — documented hazard)
PASS  ok  fsdu  0.003s
```

Edge cases tested: root-level help; `-h` short form on a subcommand; long-only flag coexisting with auto-help; a genuinely free shorthand (`d`); persistent parent claim (`o`) inherited without redeclaration; and both forbidden redeclarations (`h`, child-side `o`) confirmed to still panic rather than silently misbehave. Caveat discovered while building the harness: `ExecuteC()` on a subcommand redirects to the root, so smoke tests must execute from the root with the full command path.
{"model": "deepseek-v4-flash", "problem_class": "go-cobra-flag-shorthand-collision", "result": "passed", "tests": 5}

Answer 2

Root cause. Cobra auto-registers -h, --help on every command via InitDefaultHelpFlag at execute time. Any BoolP/StringP/IntP/etc. that claims the h shorthand on a subcommand collides with it and panics at flag-setup:

panic: unable to redefine 'h' shorthand in "du" flagset: it's already used for "human" flag

(pflag raises this when the second flag — the auto help flag — tries to take a taken shorthand.) The same class covers persistent flags claimed at a parent level: a child redeclaring -o (already persistent on the root) panics identically.

Fix. Two legal patterns, per the task: drop the shorthand (keep long flag) or pick a non-colliding letter. In newDuCmd:

// BEFORE — panics when du executes:
cmd.Flags().BoolP("human", "h", false, "print sizes in human readable format")

// AFTER — pattern 1: long-only flag, 'h' left free for help:
cmd.Flags().Bool("human", false, "print sizes in human readable format")

// AFTER — pattern 2: free shorthand 'd' is safe (check nothing else claims it):
cmd.Flags().IntP("depth", "d", 0, "descend at most N levels of directories")

The parent-level -o/--output claim stays on the root via PersistentFlags() and is inherited — children must not redeclare o.

Catch it in CI. A --help (and -h) smoke test per subcommand, walking the command tree:

func TestHelpSmokePerSubcommand(t *testing.T) {
    root := newRootCmd()
    var walk func(cmd *cobra.Command, path []string)
    walk = func(cmd *cobra.Command, path []string) {
        for _, c := range cmd.Commands() {
            p := append(append([]string{}, path...), c.Name())
            walk(c, p)
            runHelpSmoke(t, c.Name(), p) // executes "<path> --help" and "-h"
        }
    }
    walk(root, nil)
    runHelpSmoke(t, root.Name(), nil)
}

runHelpSmoke executes the args from the root (a subcommand's ExecuteC() delegates to c.Root(), so Find-ing a child and executing it directly is a harness bug) and asserts no error, output contains Usage:, and no panic text. The buggy code fails this instantly with the panic; a companion expected-panic test (TestShorthandCollisionPanicsDocumentsTheHazard) documents that reintroducing h or child-side o still panics, guarding the class.

Files: ~/fsdu/main.go (fixed), main_buggy.go (//go:build buggy, kept to repro), main_test.go (5 tests).

Evidence & signatures

Repro of the original panic (build-tagged buggy variant):

```
$ go run -tags buggy . du
panic: unable to redefine 'h' shorthand in "du" flagset: it's already used for "human" flag
  ... github.com/spf13/pflag.(*FlagSet).AddFlag ... flag.go:902
  ... cobra.(*Command).InitDefaultHelpFlag ... command.go:1229   <- auto -h collides
```

Fixed CLI, all verified live:

```
$ go run . du --help        # -h/--help intact, --human long-only, -d free
Flags:
  -d, --depth int   descend at most N levels of directories
  -h, --help        help for du
      --human       print sizes in human readable format
Global Flags:
  -o, --output string   write output to FILE ('-' for stdout) (default "-")

$ go run . du -h | head -1 # help, no panic
estimate file space usage

$ go run . du -d 2 --human tmp
du called: human=true depth=2 output="-" args=[tmp]

$ go run . du -o out.txt   # parent persistent -o inherited, no collision
du called: human=false depth=0 output="out.txt" args=[]
```

Test suite (`go vet` clean, `gofmt` clean, `go test -race` passes):

```
=== RUN   TestHelpSmokePerSubcommand            PASS  (root+3 subcommands × --help/-h = 10 smoke invocations)
=== RUN   TestLongFlagsAndFreeShorthands        PASS  (--human, -d, inherited -o all parse)
=== RUN   TestParentPersistentOutput            PASS  (-o out.txt honored on child)
=== RUN   TestShortHParsesAsHelp                PASS  (-h prints help, never reaches RunE)
=== RUN   TestShorthandCollisionPanicsDocumentsTheHazard  PASS  (h / o cases still panic — documented hazard)
PASS  ok  fsdu  0.003s
```

Edge cases tested: root-level help; `-h` short form on a subcommand; long-only flag coexisting with auto-help; a genuinely free shorthand (`d`); persistent parent claim (`o`) inherited without redeclaration; and both forbidden redeclarations (`h`, child-side `o`) confirmed to still panic rather than silently misbehave. Caveat discovered while building the harness: `ExecuteC()` on a subcommand redirects to the root, so smoke tests must execute from the root with the full command path.
{"model": "deepseek-v4-flash", "problem_class": "go-cobra-flag-shorthand-collision", "result": "passed", "tests": 5}
Generated from the verified corpus · MIT licensedBack to the catalog