diff --git a/custom_completions.go b/custom_completions.go index f9e88e081..19863eb46 100644 --- a/custom_completions.go +++ b/custom_completions.go @@ -206,12 +206,26 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi return finalCmd, []string{}, ShellCompDirectiveDefault, err } + // Check if interspersed is false or -- was set on a previous arg. + // This works by counting the arguments. Normally -- is not counted as arg but + // if -- was already set or interspersed is false and there is already one arg then + // the extra added -- is counted as arg. + flagCompletion := true + _ = finalCmd.ParseFlags(append(finalArgs, "--")) + newArgCount := finalCmd.Flags().NArg() + // Parse the flags early so we can check if required flags are set if err = finalCmd.ParseFlags(finalArgs); err != nil { return finalCmd, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Error while parsing flags from args %v: %s", finalArgs, err.Error()) } - if flag != nil { + realArgCount := finalCmd.Flags().NArg() + if newArgCount > realArgCount { + // don't do flag completion (see above) + flagCompletion = false + } + + if flag != nil && flagCompletion { // Check if we are completing a flag value subject to annotations if validExts, present := flag.Annotations[BashCompFilenameExt]; present { if len(validExts) != 0 { @@ -238,7 +252,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi // When doing completion of a flag name, as soon as an argument starts with // a '-' we know it is a flag. We cannot use isFlagArg() here as it requires // the flag name to be complete - if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") { + if flag == nil && len(toComplete) > 0 && toComplete[0] == '-' && !strings.Contains(toComplete, "=") && flagCompletion { var completions []string // First check for required flags @@ -351,7 +365,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi // Find the completion function for the flag or command var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) - if flag != nil { + if flag != nil && flagCompletion { completionFn = flagCompletionFunctions[flag] } else { completionFn = finalCmd.ValidArgsFunction diff --git a/custom_completions_test.go b/custom_completions_test.go index 14ec5a9eb..e5487e929 100644 --- a/custom_completions_test.go +++ b/custom_completions_test.go @@ -1697,6 +1697,159 @@ func TestValidArgsFuncChildCmdsWithDesc(t *testing.T) { } } +func TestFlagCompletionWithNotInterspersedArgs(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + childCmd := &Command{ + Use: "child", + Run: emptyRun, + ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"--validarg", "test"}, ShellCompDirectiveDefault + }, + } + childCmd2 := &Command{ + Use: "child2", + Run: emptyRun, + ValidArgs: []string{"arg1", "arg2"}, + } + rootCmd.AddCommand(childCmd, childCmd2) + childCmd.Flags().Bool("bool", false, "test bool flag") + childCmd.Flags().String("string", "", "test string flag") + childCmd.RegisterFlagCompletionFunc("string", func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"myval"}, ShellCompDirectiveDefault + }) + + // Test flag completion with no argument + output, err := executeCommand(rootCmd, ShellCompRequestCmd, "child", "--") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + "--bool\ttest bool flag", + "--string\ttest string flag", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that no flags are completed after the -- arg + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "-") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "--validarg", + "test", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that no flags are completed after the -- arg with a flag set + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--bool", "--", "-") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "--validarg", + "test", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // set Interspersed to false which means that no flags should be completed after the first arg + childCmd.Flags().SetInterspersed(false) + + // Test that no flags are completed after the first arg + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "arg", "--") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "--validarg", + "test", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that no flags are completed after the fist arg with a flag set + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--string", "t", "arg", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "--validarg", + "test", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Check that args are still completed after -- + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "--validarg", + "test", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Check that args are still completed even if flagname with ValidArgsFunction exists + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--", "--string", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "--validarg", + "test", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Check that args are still completed even if flagname with ValidArgsFunction exists + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child2", "--", "a") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "arg1", + "arg2", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + func TestFlagCompletionInGoWithDesc(t *testing.T) { rootCmd := &Command{ Use: "root",