From ae890af2dd3d7c60495b8a65c265a7dbc966565b Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Sat, 2 Jan 2021 15:35:07 +0100 Subject: [PATCH] custom comp: do not complete flags after args when interspersed is false If the interspersed option is set false and one arg is already set all following arguments are counted as arg and not parsed as flags. Because of that we should not offer flag completion. The same applies to arguments followed after `--`. Signed-off-by: Paul Holzinger --- custom_completions.go | 20 ++++++-- custom_completions_test.go | 101 +++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/custom_completions.go b/custom_completions.go index f9e88e081f..51186902fe 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 than + // 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 @@ -284,7 +298,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi var completions []string directive := ShellCompDirectiveDefault - if flag == nil { + if flag == nil && flagCompletion { foundLocalNonPersistentFlag := false // If TraverseChildren is true on the root command we don't check for // local flags because we can use a local flag on a parent command diff --git a/custom_completions_test.go b/custom_completions_test.go index 14ec5a9eb6..202f08749d 100644 --- a/custom_completions_test.go +++ b/custom_completions_test.go @@ -1697,6 +1697,107 @@ func TestValidArgsFuncChildCmdsWithDesc(t *testing.T) { } } +func TestFlagCompletionWithNotInterspersedArgs(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + ValidArgsFunction: validArgsFunc, + } + rootCmd.Flags().Bool("bool", false, "test bool flag") + rootCmd.Flags().String("string", "", "test string flag") + + // Test flag completion with no argument + output, err := executeCommand(rootCmd, ShellCompRequestCmd, "--") + 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, "--", "-") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + ":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, "--bool", "--", "-") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + ":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 fist arg + rootCmd.Flags().SetInterspersed(false) + + // Test that no flags are completed after the fist arg + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "arg", "--") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + ":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, "--string", "t", "arg", "--") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + ":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, "--", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "one\tThe first", + "two\tThe second", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + func TestFlagCompletionInGoWithDesc(t *testing.T) { rootCmd := &Command{ Use: "root",