Skip to content

Commit 0745e55

Browse files
completion: improve detection for flags that accept multiple values (#2210)
The completion code attempts to detect whether a flag can be specified more than once, and therefore should provide completion even if already set. Currently, this code depends on conventions used in the pflag package, which uses an "Array" or "Slice" suffix or for some types a "stringTo" prefix. Cobra allows custom value types to be used, which may not use the same convention for naming, and therefore currently aren't detected to allow multiple values. The pflag module defines a [SliceValue] interface, which is implemented by the Slice and Array value types it provides (unfortunately, it's not currently implemented by the "stringTo" values). This patch adds a reduced interface based on the [SliceValue] interface mentioned above to allow detecting Value-types that accept multiple values. Custom types can implement this interface to make completion work for those values. I deliberately used a reduced interface to keep the requirements for this detection as low as possible, without enforcing the other methods defined in the interface (Append, Replace) which may not apply to all custom types. Future improvements can likely still be made, considering either implementing the SliceValue interface for the "stringTo" values or defining a separate "MapValue" interface for those types. Possibly providing the reduced interface as part of the pflag module and to export it. [SliceValue]: https://github.com/spf13/pflag/blob/d5e0c0615acee7028e1e2740a11102313be88de1/flag.go#L193-L203 Signed-off-by: Sebastiaan van Stijn <github@gone.nl> Signed-off-by: Marc Khouzam <marc.khouzam@gmail.com> Co-authored-by: Marc Khouzam <marc.khouzam@gmail.com>
1 parent d1e9d85 commit 0745e55

File tree

2 files changed

+47
-3
lines changed

2 files changed

+47
-3
lines changed

completions.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,14 @@ func (c *Command) initCompleteCmd(args []string) {
270270
}
271271
}
272272

273+
// SliceValue is a reduced version of [pflag.SliceValue]. It is used to detect
274+
// flags that accept multiple values and therefore can provide completion
275+
// multiple times.
276+
type SliceValue interface {
277+
// GetSlice returns the flag value list as an array of strings.
278+
GetSlice() []string
279+
}
280+
273281
func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDirective, error) {
274282
// The last argument, which is not completely typed by the user,
275283
// should not be part of the list of arguments
@@ -399,10 +407,13 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
399407
// If we have not found any required flags, only then can we show regular flags
400408
if len(completions) == 0 {
401409
doCompleteFlags := func(flag *pflag.Flag) {
402-
if !flag.Changed ||
410+
_, acceptsMultiple := flag.Value.(SliceValue)
411+
acceptsMultiple = acceptsMultiple ||
403412
strings.Contains(flag.Value.Type(), "Slice") ||
404413
strings.Contains(flag.Value.Type(), "Array") ||
405-
strings.HasPrefix(flag.Value.Type(), "stringTo") {
414+
strings.HasPrefix(flag.Value.Type(), "stringTo")
415+
416+
if !flag.Changed || acceptsMultiple {
406417
// If the flag is not already present, or if it can be specified multiple times (Array, Slice, or stringTo)
407418
// we suggest it as a completion
408419
completions = append(completions, getFlagNameCompletions(flag, toComplete)...)

completions_test.go

+34-1
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,29 @@ func TestFlagNameCompletionInGoWithDesc(t *testing.T) {
671671
}
672672
}
673673

674+
// customMultiString is a custom Value type that accepts multiple values,
675+
// but does not include "Slice" or "Array" in its "Type" string.
676+
type customMultiString []string
677+
678+
var _ SliceValue = (*customMultiString)(nil)
679+
680+
func (s *customMultiString) String() string {
681+
return fmt.Sprintf("%v", *s)
682+
}
683+
684+
func (s *customMultiString) Set(v string) error {
685+
*s = append(*s, v)
686+
return nil
687+
}
688+
689+
func (s *customMultiString) Type() string {
690+
return "multi string"
691+
}
692+
693+
func (s *customMultiString) GetSlice() []string {
694+
return *s
695+
}
696+
674697
func TestFlagNameCompletionRepeat(t *testing.T) {
675698
rootCmd := &Command{
676699
Use: "root",
@@ -693,6 +716,8 @@ func TestFlagNameCompletionRepeat(t *testing.T) {
693716
sliceFlag := rootCmd.Flags().Lookup("slice")
694717
rootCmd.Flags().BoolSliceP("bslice", "b", nil, "bool slice flag")
695718
bsliceFlag := rootCmd.Flags().Lookup("bslice")
719+
rootCmd.Flags().VarP(&customMultiString{}, "multi", "m", "multi string flag")
720+
multiFlag := rootCmd.Flags().Lookup("multi")
696721

697722
// Test that flag names are not repeated unless they are an array or slice
698723
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--first", "1", "--")
@@ -706,6 +731,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) {
706731
"--array",
707732
"--bslice",
708733
"--help",
734+
"--multi",
709735
"--second",
710736
"--slice",
711737
":4",
@@ -728,6 +754,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) {
728754
"--array",
729755
"--bslice",
730756
"--help",
757+
"--multi",
731758
"--slice",
732759
":4",
733760
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
@@ -737,20 +764,22 @@ func TestFlagNameCompletionRepeat(t *testing.T) {
737764
}
738765

739766
// Test that flag names are not repeated unless they are an array or slice
740-
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--slice", "1", "--slice=2", "--array", "val", "--bslice", "true", "--")
767+
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--slice", "1", "--slice=2", "--array", "val", "--bslice", "true", "--multi", "val", "--")
741768
if err != nil {
742769
t.Errorf("Unexpected error: %v", err)
743770
}
744771
// Reset the flag for the next command
745772
sliceFlag.Changed = false
746773
arrayFlag.Changed = false
747774
bsliceFlag.Changed = false
775+
multiFlag.Changed = false
748776

749777
expected = strings.Join([]string{
750778
"--array",
751779
"--bslice",
752780
"--first",
753781
"--help",
782+
"--multi",
754783
"--second",
755784
"--slice",
756785
":4",
@@ -768,6 +797,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) {
768797
// Reset the flag for the next command
769798
sliceFlag.Changed = false
770799
arrayFlag.Changed = false
800+
multiFlag.Changed = false
771801

772802
expected = strings.Join([]string{
773803
"--array",
@@ -778,6 +808,8 @@ func TestFlagNameCompletionRepeat(t *testing.T) {
778808
"-f",
779809
"--help",
780810
"-h",
811+
"--multi",
812+
"-m",
781813
"--second",
782814
"-s",
783815
"--slice",
@@ -797,6 +829,7 @@ func TestFlagNameCompletionRepeat(t *testing.T) {
797829
// Reset the flag for the next command
798830
sliceFlag.Changed = false
799831
arrayFlag.Changed = false
832+
multiFlag.Changed = false
800833

801834
expected = strings.Join([]string{
802835
"-a",

0 commit comments

Comments
 (0)