diff --git a/command_parse.go b/command_parse.go index 92c58eeaac..2b9a481df2 100644 --- a/command_parse.go +++ b/command_parse.go @@ -86,6 +86,11 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { // stop parsing once we see a "--" if firstArg == "--" { + // In shell completion mode, preserve "--" so that completion can detect + // when the user is completing "--" itself vs. completing after "--" + if cmd.Root().shellCompletion { + posArgs = append(posArgs, firstArg) + } posArgs = append(posArgs, rargs[1:]...) return &stringSliceArgs{posArgs}, nil } @@ -166,6 +171,12 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { // not a bool flag so need to get the next arg if flagVal == "" && !valFromEqual { if len(rargs) == 1 { + // In shell completion mode, preserve the flag so that DefaultCompleteWithFlags can use it + // as lastArg and offer suggestions for it. + if cmd.Root().shellCompletion { + posArgs = append(posArgs, rargs...) + return &stringSliceArgs{posArgs}, nil + } return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", argumentNotProvidedErrMsg, firstArg) } flagVal = rargs[1] @@ -182,6 +193,12 @@ func (cmd *Command) parseFlags(args Args) (Args, error) { // no flag lookup found and short handling is disabled if !shortOptionHandling { + // In shell completion mode, preserve the partial flag so that DefaultCompleteWithFlags can use it + // as lastArg and offer suggestions that match the prefix. + if cmd.Root().shellCompletion { + posArgs = append(posArgs, rargs...) + return &stringSliceArgs{posArgs}, nil + } return &stringSliceArgs{posArgs}, fmt.Errorf("%s%s", providedButNotDefinedErrMsg, flagName) } diff --git a/command_test.go b/command_test.go index dafee0bee6..89a618a38d 100644 --- a/command_test.go +++ b/command_test.go @@ -599,8 +599,8 @@ func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) { testArgs *stringSliceArgs expectedOut string }{ - {testArgs: &stringSliceArgs{v: []string{"--undefined"}}, expectedOut: "found 0 args"}, - {testArgs: &stringSliceArgs{v: []string{"--number"}}, expectedOut: "found 0 args"}, + {testArgs: &stringSliceArgs{v: []string{"--undefined"}}, expectedOut: "found 1 args"}, + {testArgs: &stringSliceArgs{v: []string{"--number"}}, expectedOut: "found 1 args"}, {testArgs: &stringSliceArgs{v: []string{"--number", "forty-two"}}, expectedOut: "found 0 args"}, {testArgs: &stringSliceArgs{v: []string{"--number", "42"}}, expectedOut: "found 0 args"}, {testArgs: &stringSliceArgs{v: []string{"--number", "42", "newArg"}}, expectedOut: "found 1 args"}, diff --git a/completion_test.go b/completion_test.go index b53e4b90f8..a2b9995882 100644 --- a/completion_test.go +++ b/completion_test.go @@ -121,14 +121,13 @@ func TestCompletionSubcommand(t *testing.T) { }, }, { - name: "subcommand flag no completion", + name: "subcommand double dash shows long flags", args: []string{"foo", "bar", "--", completionFlag}, - contains: "l1", - msg: "Expected output to contain shell name %[1]q", + contains: "--l1", + msg: "Expected output to contain flag %[1]q", msgArgs: []any{ - "l1", + "--l1", }, - notContains: true, }, { name: "sub sub command general completion", @@ -150,14 +149,13 @@ func TestCompletionSubcommand(t *testing.T) { }, }, { - name: "sub sub command no completion", + name: "sub sub command double dash shows flags", args: []string{"foo", "bar", "xyz", "--", completionFlag}, - contains: "-g", + contains: "--help", msg: "Expected output to contain flag %[1]q", msgArgs: []any{ - "-g", + "--help", }, - notContains: true, }, { name: "sub sub command no completion extra args", @@ -169,6 +167,24 @@ func TestCompletionSubcommand(t *testing.T) { }, notContains: true, }, + { + name: "subcommand partial double dash flag completion", + args: []string{"foo", "bar", "--l", completionFlag}, + contains: "--l1", + msg: "Expected output to contain flag %[1]q", + msgArgs: []any{ + "--l1", + }, + }, + { + name: "sub sub command partial double dash flag completion", + args: []string{"foo", "bar", "xyz", "--he", completionFlag}, + contains: "--help", + msg: "Expected output to contain flag %[1]q", + msgArgs: []any{ + "--help", + }, + }, } for _, test := range tests { diff --git a/help.go b/help.go index f07fe4ff04..1fba6edf0c 100644 --- a/help.go +++ b/help.go @@ -256,11 +256,6 @@ func DefaultCompleteWithFlags(ctx context.Context, cmd *Command) { lastArg = args[argsLen-1] } - if lastArg == "--" { - tracef("No completions due to termination") - return - } - if lastArg == completionFlag { lastArg = "" } @@ -485,10 +480,12 @@ func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) { return false, arguments } - // If arguments include "--", shell completion is disabled - // because after "--" only positional arguments are accepted. + // If arguments include "--" before the token being completed, shell completion + // is disabled because after "--" only positional arguments are accepted. // https://unix.stackexchange.com/a/11382 - if slices.Contains(arguments, "--") { + // Note: The token being completed is at position pos-1 (immediately before completionFlag). + // We only check arguments before that position, so completing "--" itself still works. + if pos >= 1 && slices.Contains(arguments[:pos-1], "--") { return false, arguments[:pos] } diff --git a/help_test.go b/help_test.go index 6ff88fe4e4..97f657508e 100644 --- a/help_test.go +++ b/help_test.go @@ -1325,7 +1325,7 @@ func TestDefaultCompleteWithFlags(t *testing.T) { expected: "", }, { - name: "flag-suggestion-end-args", + name: "flag-suggestion-double-dash-shows-all-flags", cmd: &Command{ Flags: []Flag{ &BoolFlag{Name: "excitement"}, @@ -1344,7 +1344,7 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }, argv: []string{"cmd", "--e", "--", completionFlag}, env: map[string]string{"SHELL": "bash"}, - expected: "", + expected: "--excitement\n--hat-shape\n", }, { name: "typical-command-suggestion", @@ -1907,6 +1907,15 @@ func Test_checkShellCompleteFlag(t *testing.T) { wantShellCompletion: true, wantArgs: []string{"foo"}, }, + { + name: "double dash is the token being completed", + arguments: []string{"foo", "--", completionFlag}, + cmd: &Command{ + EnableShellCompletion: true, + }, + wantShellCompletion: true, + wantArgs: []string{"foo", "--"}, + }, } for _, tt := range tests {