From 4740c2691b7dc8b6030a9bc40557e49c788ebd5c 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 | 153 +++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 3 deletions(-) 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",