diff --git a/completion_partial_regression_test.go b/completion_partial_regression_test.go index 77beea7f66..158eb6bc19 100644 --- a/completion_partial_regression_test.go +++ b/completion_partial_regression_test.go @@ -36,10 +36,11 @@ func TestPartialFlagCompletionAfterPositionalArgument(t *testing.T) { for _, scope := range []struct { name string path []string + want string }{ - {"app", nil}, - {"sub", []string{"sub"}}, - {"nested", []string{"sub", "nested"}}, + {"app", nil, "--param-app-one\n--param-app-two\n"}, + {"sub", []string{"sub"}, "--param-sub-one\n--param-sub-two\n--param-app-one\n--param-app-two\n"}, + {"nested", []string{"sub", "nested"}, "--param-nested-one\n--param-nested-two\n--param-app-one\n--param-app-two\n--param-sub-one\n--param-sub-two\n"}, } { for _, positional := range [][]string{nil, {"value"}, {"first", "second"}} { t.Run(scope.name+"/"+strings.Join(positional, "_"), func(t *testing.T) { @@ -53,9 +54,8 @@ func TestPartialFlagCompletionAfterPositionalArgument(t *testing.T) { if err := cmd.Run(context.Background(), args); err != nil { t.Fatal(err) } - want := "--param-" + scope.name + "-one\n--param-" + scope.name + "-two\n" - if got := output.String(); got != want { - t.Errorf("completion for %q = %q, want %q", args, got, want) + if got := output.String(); got != scope.want { + t.Errorf("completion for %q = %q, want %q", args, got, scope.want) } if ran { t.Error("completion executed the command action") diff --git a/help.go b/help.go index 7a2480e565..fea814c37a 100644 --- a/help.go +++ b/help.go @@ -272,7 +272,15 @@ func DefaultCompleteWithFlags(ctx context.Context, cmd *Command) { if strings.HasPrefix(lastArg, "-") { tracef("printing flag suggestion for flag[%v] on command %[1]q", lastArg, cmd.Name) - printFlagSuggestions(lastArg, cmd.Flags, cmd.Root().Writer) + flags := cmd.allFlags() + if !cmd.SkipFlagParsing { + for _, fl := range cmd.VisiblePersistentFlags() { + if !hasFlag(flags, fl) { + flags = append(flags, fl) + } + } + } + printFlagSuggestions(lastArg, flags, cmd.Root().Writer) return } diff --git a/help_test.go b/help_test.go index 56f727efb0..8c96a682b8 100644 --- a/help_test.go +++ b/help_test.go @@ -1411,6 +1411,18 @@ func TestHideHelp_SubcommandCannotOptBackIn(t *testing.T) { r.ErrorContains(cmd.Run(buildTestContext(t), []string{"cli.test", "nully", "--help"}), providedButNotDefinedErrMsg) } +type completionOnlyFlag struct { + name string +} + +func (f *completionOnlyFlag) String() string { return "" } +func (f *completionOnlyFlag) Get() any { return nil } +func (f *completionOnlyFlag) PreParse() error { return nil } +func (f *completionOnlyFlag) PostParse() error { return nil } +func (f *completionOnlyFlag) Set(string, string) error { return nil } +func (f *completionOnlyFlag) Names() []string { return []string{f.name} } +func (f *completionOnlyFlag) IsSet() bool { return false } + func TestDefaultCompleteWithFlags(t *testing.T) { origArgv := os.Args t.Cleanup(func() { os.Args = origArgv }) @@ -1429,6 +1441,24 @@ func TestDefaultCompleteWithFlags(t *testing.T) { env: map[string]string{"SHELL": "bash"}, expected: "", }, + { + name: "persistent-flag-after-positional-argument", + cmd: &Command{ + Name: "add", + Arguments: []Argument{ + &StringArg{Name: "arg1"}, + }, + parent: &Command{ + Name: "app", + Flags: []Flag{ + &StringFlag{Name: "config", Usage: "path to config file"}, + }, + }, + }, + argv: []string{"app", "add", "value", "--conf", completionFlag}, + env: map[string]string{"SHELL": "bash"}, + expected: "--config:path to config file\n", + }, { name: "typical-flag-suggestion", cmd: &Command{ @@ -1449,7 +1479,56 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }, argv: []string{"cmd", "--e", completionFlag}, env: map[string]string{"SHELL": "bash"}, - expected: "--excitement\n", + expected: "--excitement\n--everybody-jump-on\n", + }, + { + name: "shared-local-and-persistent-flag-is-suggested-once", + cmd: func() *Command { + shared := &BoolFlag{Name: "verbose"} + return &Command{ + Flags: []Flag{shared}, + parent: &Command{Name: "app", Flags: []Flag{shared}}, + } + }(), + argv: []string{"app", "sub", "--ver", completionFlag}, + env: map[string]string{"SHELL": "bash"}, + expected: "--verbose\n", + }, + { + name: "custom-flag-without-visible-flag-is-suggested", + cmd: &Command{ + Flags: []Flag{&completionOnlyFlag{name: "custom"}}, + }, + argv: []string{"app", "--cus", completionFlag}, + env: map[string]string{"SHELL": "bash"}, + expected: "--custom\n", + }, + { + name: "mutually-exclusive-flags-are-suggested", + cmd: &Command{ + MutuallyExclusiveFlags: []MutuallyExclusiveFlags{{ + Flags: [][]Flag{ + {&BoolFlag{Name: "alpha"}}, + {&BoolFlag{Name: "also"}}, + }, + }}, + }, + argv: []string{"app", "--al", completionFlag}, + env: map[string]string{"SHELL": "bash"}, + expected: "--alpha\n--also\n", + }, + { + name: "skip-flag-parsing-does-not-suggest-parent-flags", + cmd: &Command{ + SkipFlagParsing: true, + parent: &Command{ + Name: "app", + Flags: []Flag{&BoolFlag{Name: "root-flag"}}, + }, + }, + argv: []string{"app", "sub", "--r", completionFlag}, + env: map[string]string{"SHELL": "bash"}, + expected: "", }, { name: "typical-flag-suggestion-hidden-bool", @@ -1471,7 +1550,7 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }, argv: []string{"cmd", "--e", completionFlag}, env: map[string]string{"SHELL": "bash"}, - expected: "", + expected: "--everybody-jump-on\n", }, { name: "typical-flag-suggestion-hidden-non-bool", @@ -1493,7 +1572,7 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }, argv: []string{"cmd", "--e", completionFlag}, env: map[string]string{"SHELL": "bash"}, - expected: "--excitement\n", + expected: "--excitement\n--everybody-jump-on\n", }, { name: "typical-flag-suggestion-hidden-bool-with-inverse", @@ -1515,7 +1594,7 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }, argv: []string{"cmd", "--e", completionFlag}, env: map[string]string{"SHELL": "bash"}, - expected: "--excitement\n", + expected: "--excitement\n--everybody-jump-on\n", }, { name: "flag-suggestion-double-dash-shows-all-flags", @@ -1537,7 +1616,7 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }, argv: []string{"cmd", "--e", "--", completionFlag}, env: map[string]string{"SHELL": "bash"}, - expected: "--excitement\n--hat-shape\n", + expected: "--excitement\n--hat-shape\n--happiness\n--everybody-jump-on\n", }, { name: "typical-command-suggestion", @@ -1603,7 +1682,7 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }, argv: []string{"cmd", "putz", "-e", completionFlag}, env: map[string]string{"SHELL": "zsh"}, - expected: "--excitement:an exciting flag\n", + expected: "--excitement:an exciting flag\n--everybody-jump-on\n", }, { name: "zsh-autocomplete-with-empty-flag-descriptions", @@ -1623,7 +1702,7 @@ func TestDefaultCompleteWithFlags(t *testing.T) { }, argv: []string{"cmd", "putz", "-e", completionFlag}, env: map[string]string{"SHELL": "zsh"}, - expected: "--excitement\n", + expected: "--excitement\n--everybody-jump-on\n", }, } { t.Run(tc.name, func(ct *testing.T) {