Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,52 @@ func TestCompletionSubcommand(t *testing.T) {
}
}

func TestCompletionAfterCompleteRootFlag(t *testing.T) {
// The root command completes the arguments passed to Run, which still
// hold the flags. Flag suggestions are printed for the last argument,
// and a flag that needs nothing more also lists the subcommands. The
// shell keeps only the suggestions that match the word being typed.

tests := []struct {
name string
arg string
want []string
}{
{name: "bool flag", arg: "--verbose", want: []string{"sub"}},
{name: "inverse bool flag", arg: "--no-color", want: []string{"sub"}},
{name: "value after equals", arg: "--format=json", want: []string{"sub"}},
{name: "flag waiting for a value", arg: "--format", want: []string{}},
{name: "start of a longer flag", arg: "--debug", want: []string{"--debug-level", "sub"}},
{name: "partial flag", arg: "--verb", want: []string{"--verbose"}},
{name: "short alias", arg: "-v", want: []string{"sub"}},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
out := &bytes.Buffer{}
cmd := &Command{
EnableShellCompletion: true,
HideHelp: true,
Writer: out,
Flags: []Flag{
&BoolFlag{Name: "verbose", Aliases: []string{"v"}},
&BoolWithInverseFlag{Name: "color"},
&StringFlag{Name: "format"},
&BoolFlag{Name: "debug"},
&StringFlag{Name: "debug-level"},
},
Commands: []*Command{
{Name: "sub"},
},
}

r := require.New(t)
r.NoError(cmd.Run(buildTestContext(t), []string{"foo", test.arg, completionFlag}))
r.Equal(test.want, strings.Fields(out.String()))
})
}
}

func TestCompletionAfterDoubleDashNeverRunsAction(t *testing.T) {
// Regression test for https://github.com/urfave/cli/issues/1993:
// pressing tab on a command line that holds a "--" must never execute
Expand Down
25 changes: 24 additions & 1 deletion help.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,23 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
}
}

// isCompleteFlag reports whether arg is a flag that needs nothing more: one
// that takes no value, or one given its value after "=".
func isCompleteFlag(arg string, flags []Flag) bool {
name, _, hasValue := strings.Cut(strings.TrimLeft(arg, "-"), "=")
for _, flag := range flags {
if !slices.Contains(flag.Names(), name) {
continue
}
if hasValue {
return true
}
docFlag, ok := flag.(DocGenerationFlag)
return ok && !docFlag.TakesValue()
}
return false
}

func DefaultCompleteWithFlags(ctx context.Context, cmd *Command) {
args := os.Args
if cmd != nil && cmd.parent != nil {
Expand Down Expand Up @@ -309,7 +326,13 @@ func DefaultCompleteWithFlags(ctx context.Context, cmd *Command) {
}
}
printFlagSuggestions(lastArg, flags, cmd.Root().Writer)
return
// The last argument may be the flag being typed, as in "app --debug<TAB>",
// or one before the cursor, as in "app --debug <TAB>". The bash, zsh and
// fish scripts keep only the suggestions that match the word being typed,
// so after a complete flag, suggest the subcommands too.
if !isCompleteFlag(lastArg, flags) {
return
}
}

if cmd != nil {
Expand Down
Loading