Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix:(issue_1814) Fix completions for subcommands #1824

Merged
merged 1 commit into from
Nov 27, 2023
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
31 changes: 18 additions & 13 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ type Command struct {
isInError bool
// track state of defaults
didSetupDefaults bool
// whether in shell completion mode
shellCompletion bool
}

// FullName returns the full name of the command.
Expand Down Expand Up @@ -244,7 +246,7 @@ func (cmd *Command) setupDefaults(osArgs []string) {
cmd.SuggestCommandFunc = suggestCommand
}

if cmd.EnableShellCompletion {
if cmd.EnableShellCompletion || cmd.Root().shellCompletion {
completionCommand := buildCompletionCommand()

if cmd.ShellCompletionCommandName != "" {
Expand Down Expand Up @@ -346,16 +348,19 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
cmd.parent = v
}

// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
// flag name as the value of the flag before it which is undesirable
// note that we can only do this because the shell autocomplete function
// always appends the completion flag at the end of the command
enableShellCompletion, osArgs := checkShellCompleteFlag(cmd, osArgs)
if cmd.parent == nil {
// handle the completion flag separately from the flagset since
// completion could be attempted after a flag, but before its value was put
// on the command line. this causes the flagset to interpret the completion
// flag name as the value of the flag before it which is undesirable
// note that we can only do this because the shell autocomplete function
// always appends the completion flag at the end of the command
tracef("checking osArgs %v (cmd=%[2]q)", osArgs, cmd.Name)
cmd.shellCompletion, osArgs = checkShellCompleteFlag(cmd, osArgs)

tracef("setting cmd.EnableShellCompletion=%[1]v from checkShellCompleteFlag (cmd=%[2]q)", enableShellCompletion, cmd.Name)
cmd.EnableShellCompletion = enableShellCompletion
tracef("setting cmd.shellCompletion=%[1]v from checkShellCompleteFlag (cmd=%[2]q)", cmd.shellCompletion && cmd.EnableShellCompletion, cmd.Name)
cmd.shellCompletion = cmd.EnableShellCompletion && cmd.shellCompletion
}

tracef("using post-checkShellCompleteFlag arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name)

Expand Down Expand Up @@ -418,7 +423,7 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
return nil
}

if cmd.After != nil && !cmd.EnableShellCompletion {
if cmd.After != nil && !cmd.Root().shellCompletion {
defer func() {
if err := cmd.After(ctx, cmd); err != nil {
err = cmd.handleExitCoder(ctx, err)
Expand All @@ -445,7 +450,7 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
}
}

if cmd.Before != nil && !cmd.EnableShellCompletion {
if cmd.Before != nil && !cmd.Root().shellCompletion {
if err := cmd.Before(ctx, cmd); err != nil {
deferErr = cmd.handleExitCoder(ctx, err)
return deferErr
Expand Down Expand Up @@ -666,7 +671,7 @@ func (cmd *Command) parseFlags(args Args) (Args, error) {

tracef("parsing flags iteratively tail=%[1]q (cmd=%[2]q)", args.Tail(), cmd.Name)

if err := parseIter(cmd.flagSet, cmd, args.Tail(), cmd.Root().EnableShellCompletion); err != nil {
if err := parseIter(cmd.flagSet, cmd, args.Tail(), cmd.Root().shellCompletion); err != nil {
return cmd.Args(), err
}

Expand Down
27 changes: 27 additions & 0 deletions completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,33 @@ func TestCompletionShell(t *testing.T) {
}
}

func TestCompletionSubcommand(t *testing.T) {
out := &bytes.Buffer{}

cmd := &Command{
EnableShellCompletion: true,
Writer: out,
Commands: []*Command{
{
Name: "bar",
Commands: []*Command{
{
Name: "xyz",
},
},
},
},
}

r := require.New(t)

r.NoError(cmd.Run(buildTestContext(t), []string{"foo", "bar", "--generate-shell-completion"}))
r.Containsf(
out.String(), "xyz",
"Expected output to contain shell name %[1]q", "xyz",
)
}

func TestCompletionInvalidShell(t *testing.T) {
cmd := &Command{
EnableShellCompletion: true,
Expand Down
20 changes: 14 additions & 6 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,16 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {

func DefaultCompleteWithFlags(cmd *Command) func(ctx context.Context, cmd *Command) {
return func(_ context.Context, cmd *Command) {
if len(os.Args) > 2 {
lastArg := os.Args[len(os.Args)-2]
args := os.Args
if cmd != nil && cmd.flagSet != nil && cmd.parent != nil {
args = cmd.Args().Slice()
tracef("running default complete with flags[%v] on command %[1]q", args, cmd.Name)
} else {
tracef("running default complete with os.Args flags")
}
argsLen := len(args)
if argsLen > 2 {
lastArg := args[argsLen-2]

if strings.HasPrefix(lastArg, "-") {
if cmd != nil {
Expand All @@ -235,11 +243,10 @@ func DefaultCompleteWithFlags(cmd *Command) func(ctx context.Context, cmd *Comma
}

if cmd != nil {
tracef("printing command suggestions on command %[1]q", cmd.Name)
printCommandSuggestions(cmd.Commands, cmd.Root().Writer)
return
}

printCommandSuggestions(cmd.Commands, cmd.Root().Writer)
}
}

Expand Down Expand Up @@ -431,7 +438,7 @@ func checkVersion(cmd *Command) bool {
}

func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) {
if !c.EnableShellCompletion {
if (c.parent == nil && !c.EnableShellCompletion) || (c.parent != nil && !c.Root().shellCompletion) {
return false, arguments
}

Expand All @@ -448,7 +455,7 @@ func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) {
func checkCompletions(ctx context.Context, cmd *Command) bool {
tracef("checking completions on command %[1]q", cmd.Name)

if !cmd.EnableShellCompletion {
if !cmd.Root().shellCompletion {
return false
}

Expand All @@ -461,6 +468,7 @@ func checkCompletions(ctx context.Context, cmd *Command) bool {
}

if cmd.ShellComplete != nil {
tracef("running shell completion func for command %[1]q", cmd.Name)
cmd.ShellComplete(ctx, cmd)
}

Expand Down