Align shared debug help and completion

This commit is contained in:
2026-11-08 15:08:50 +00:00
parent a54ff03aef
commit d914295ced
5 changed files with 99 additions and 3 deletions

View File

@@ -35,8 +35,7 @@ type completionFlagState struct {
func completionFlags(context completionContext) []commandmeta.Flag {
flags := make([]commandmeta.Flag, 0, len(context.command.Flags)+len(context.command.CoreFlags)+1)
flags = append(flags, commandmeta.HelpFlag())
flags = append(flags, context.command.CoreFlags...)
flags = append(flags, context.command.Flags...)
flags = append(flags, commandmeta.EffectiveFlags(context.command.Flags, context.command.CoreFlags)...)
return flags
}

View File

@@ -70,6 +70,34 @@ func TestCompletionFlagAndValuePositions(t *testing.T) {
}
}
func TestSharedDebugCompletionHints(t *testing.T) {
for _, tc := range []struct {
name string
hint CompletionHint
want commandmeta.CompletionKind
}{
{"none", NoCompletion(), commandmeta.CompletionNone},
{"choices", CompleteChoices(CompletionChoice{Value: "true"}), commandmeta.CompletionChoices},
{"files", CompleteFiles(), commandmeta.CompletionFiles},
{"directories", CompleteDirectories(), commandmeta.CompletionDirectories},
} {
t.Run(tc.name, func(t *testing.T) {
command := NewCommand(CommandSpec[struct{}]{Name: "run", Flags: []Flag[struct{}]{BoolFlag(FlagMeta{Name: "debug", Short: "d", Completion: tc.hint}, false, func(*struct{}, Supplied[bool]) {})}, Run: func(Invocation, struct{}) Outcome { return Success() }})
app, err := New(Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
snapshot := app.routeSnapshot()
for _, current := range []string{"--debug=", "-d="} {
target := completionTargetFor(snapshot, resolveCompletionContext(snapshot, []string{"run"}, current))
if target.kind != completionTargetValue || target.hint.Kind != tc.want {
t.Fatalf("%s: %#v", current, target)
}
}
})
}
}
func TestCompletionOperandAllocation(t *testing.T) {
arguments := []commandmeta.Argument{
{Name: "optional", Min: 0, Max: 1, Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionFiles}},

View File

@@ -120,3 +120,51 @@ func TestCompletionQueryDiscovery(t *testing.T) {
t.Fatalf("discovery outcome=%#v reply=%q", outcome, output.String())
}
}
func TestSharedDebugHelpAndCompletion(t *testing.T) {
var factories, setters, checks, handlers int
command := NewCommand(CommandSpec[struct{}]{
Name: "run",
NewInput: func() struct{} { factories++; return struct{}{} },
Flags: []Flag[struct{}]{BoolFlag(FlagMeta{Name: "debug", Short: "d", Usage: "application tracing", Required: true, Completion: CompleteChoices(CompletionChoice{Value: "true"}, CompletionChoice{Value: "trace"})}, true, func(*struct{}, Supplied[bool]) { setters++ })},
CheckInput: func(struct{}) error { checks++; return nil },
Run: func(Invocation, struct{}) Outcome { handlers++; return Success() },
})
app, err := New(Identity{Name: "example"}, command, Completion("example"), Alias("shortcut", "run"))
if err != nil {
t.Fatal(err)
}
for _, args := range [][]string{{"run", "--help"}, {"help", "run"}, {"shortcut", "--help"}, {"help", "shortcut"}, {"run", "--unknown"}, {"shortcut", "--unknown"}} {
var output, diagnostic bytes.Buffer
result := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostic})
text := output.String() + diagnostic.String()
if strings.Count(text, "--debug") != 1 || !strings.Contains(text, "-d") || !strings.Contains(text, "application tracing") || !strings.Contains(text, "application input defaults to true") || !strings.Contains(text, "required") || !strings.Contains(text, "default: false") {
t.Fatalf("%v: outcome=%#v text=%q", args, result, text)
}
}
for _, tc := range []struct {
args []string
want string
count int
}{
{[]string{"__complete", "run", "--deb"}, "--debug", 1},
{[]string{"__complete", "run", "-"}, "\n-d\t", 1},
{[]string{"__complete", "run", "--debug=tr"}, "trace", 1},
{[]string{"__complete", "run", "-d=tr"}, "trace", 1},
} {
var output bytes.Buffer
result := app.Execute(context.Background(), tc.args, Streams{Out: &output, Err: io.Discard})
if result.Kind != OutcomeSuccess || strings.Count(output.String(), tc.want) != tc.count {
t.Fatalf("%v: %#v %q", tc.args, result, output.String())
}
}
if factories != 0 || setters != 0 || checks != 0 || handlers != 0 {
t.Fatalf("callbacks: %d %d %d %d", factories, setters, checks, handlers)
}
if result := app.Execute(context.Background(), []string{"run", "--debug=invalid", "--help"}, Streams{Out: io.Discard, Err: io.Discard}); result.Kind != OutcomeUsage {
t.Fatalf("help skipped boolean validation: %#v", result)
}
if result := app.Execute(context.Background(), []string{"run", "--debug=trace"}, Streams{Out: io.Discard, Err: io.Discard}); result.Kind != OutcomeUsage {
t.Fatalf("suggestion bypassed boolean parsing: %#v", result)
}
}

View File

@@ -223,6 +223,11 @@ capture, selection, application and rename, with visibility still independent.
## Stage 5 — Present one debug option in help and completion
**Complete.** Completion uses the same effective declarations as parsing and
help. Canonical and alias help and usage failures expose the combined option;
focused tests cover local shorts, requiredness, application defaults, choices,
file and directory hints, boolean validation, and idle application callbacks.
Primary files: [completion_flags.go](../../completion_flags.go),
[internal/cobra/parse.go](../../internal/cobra/parse.go),
[internal/commandmeta](../../internal/commandmeta/metadata.go).

View File

@@ -252,7 +252,7 @@ func displayCommandPath(rootName string, path []string) string {
return strings.Join(parts, " ")
}
// UsageLine returns Cobra's usage line for an exact path on this session.
// UsageLine returns usage for an exact path on this session.
func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure) {
if session == nil || session.root == nil {
return "", &commandmeta.Failure{Message: "command tree is nil"}
@@ -267,6 +267,22 @@ func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure)
if selected.HasSubCommands() {
line += " [command]"
}
for _, flag := range declaration.Flags {
if flag.Name != "debug" || !flag.Boolean {
continue
}
for _, effective := range commandmeta.EffectiveFlags(declaration.Flags, declaration.CoreFlags) {
if effective.Name == "debug" {
spelling := "--debug"
if effective.Short != "" {
spelling = "-" + effective.Short + ", " + spelling
}
line += "\n " + spelling + " " + flagUsage(effective)
break
}
}
break
}
return line, nil
}