Skip to content

Commit

Permalink
Merge pull request #1824 from dearchap/issue_1814
Browse files Browse the repository at this point in the history
Fix:(issue_1814) Fix completions for subcommands
  • Loading branch information
dearchap committed Nov 27, 2023
2 parents e8abd76 + 2f3036b commit 1f0e15c
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 19 deletions.
31 changes: 18 additions & 13 deletions command.go
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
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
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

0 comments on commit 1f0e15c

Please sign in to comment.