Skip to content

Commit de187e8

Browse files
authored
Fix flag completion (#1438)
* Fix flag completion The flag completion functions should not be stored in the root cmd. There is no requirement that the root cmd should be the same when `RegisterFlagCompletionFunc` was called. Storing the flags there does not work when you add the the flags to your cmd struct before you add the cmd to the parent/root cmd. The flags can no longer be found in the rigth place when the completion command is called and thus the flag completion does not work. Also #1423 claims that this would be thread safe but we still have a map which will fail when accessed concurrently. To truly fix this issue use a RWMutex. Fixes #1437 Fixes #1320 Signed-off-by: Paul Holzinger <[email protected]> * Fix trailing whitespaces in fish comp scripts Signed-off-by: Paul Holzinger <[email protected]>
1 parent 07861c8 commit de187e8

File tree

5 files changed

+54
-13
lines changed

5 files changed

+54
-13
lines changed

bash_completions.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,9 @@ func writeLocalNonPersistentFlag(buf io.StringWriter, flag *pflag.Flag) {
512512

513513
// Setup annotations for go completions for registered flags
514514
func prepareCustomAnnotationsForFlags(cmd *Command) {
515-
for flag := range cmd.Root().flagCompletionFunctions {
515+
flagCompletionMutex.RLock()
516+
defer flagCompletionMutex.RUnlock()
517+
for flag := range flagCompletionFunctions {
516518
// Make sure the completion script calls the __*_go_custom_completion function for
517519
// every registered flag. We need to do this here (and not when the flag was registered
518520
// for completion) so that we can know the root command name for the prefix

command.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,6 @@ type Command struct {
142142
// that we can use on every pflag set and children commands
143143
globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName
144144

145-
//flagCompletionFunctions is map of flag completion functions.
146-
flagCompletionFunctions map[*flag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
147-
148145
// usageFunc is usage func defined by user.
149146
usageFunc func(*Command) error
150147
// usageTemplate is usage template defined by user.

completions.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"strings"
7+
"sync"
78

89
"github.com/spf13/pflag"
910
)
@@ -17,6 +18,12 @@ const (
1718
ShellCompNoDescRequestCmd = "__completeNoDesc"
1819
)
1920

21+
// Global map of flag completion functions. Make sure to use flagCompletionMutex before you try to read and write from it.
22+
var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
23+
24+
// lock for reading and writing from flagCompletionFunctions
25+
var flagCompletionMutex = &sync.RWMutex{}
26+
2027
// ShellCompDirective is a bit map representing the different behaviors the shell
2128
// can be instructed to have once completions have been provided.
2229
type ShellCompDirective int
@@ -100,15 +107,13 @@ func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Comman
100107
if flag == nil {
101108
return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
102109
}
110+
flagCompletionMutex.Lock()
111+
defer flagCompletionMutex.Unlock()
103112

104-
root := c.Root()
105-
if _, exists := root.flagCompletionFunctions[flag]; exists {
113+
if _, exists := flagCompletionFunctions[flag]; exists {
106114
return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
107115
}
108-
if root.flagCompletionFunctions == nil {
109-
root.flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
110-
}
111-
root.flagCompletionFunctions[flag] = f
116+
flagCompletionFunctions[flag] = f
112117
return nil
113118
}
114119

@@ -402,7 +407,9 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
402407
// Find the completion function for the flag or command
403408
var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
404409
if flag != nil && flagCompletion {
405-
completionFn = c.Root().flagCompletionFunctions[flag]
410+
flagCompletionMutex.RLock()
411+
completionFn = flagCompletionFunctions[flag]
412+
flagCompletionMutex.RUnlock()
406413
} else {
407414
completionFn = finalCmd.ValidArgsFunction
408415
}

completions_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1971,6 +1971,41 @@ func TestFlagCompletionWithNotInterspersedArgs(t *testing.T) {
19711971
}
19721972
}
19731973

1974+
func TestFlagCompletionWorksRootCommandAddedAfterFlags(t *testing.T) {
1975+
rootCmd := &Command{Use: "root", Run: emptyRun}
1976+
childCmd := &Command{
1977+
Use: "child",
1978+
Run: emptyRun,
1979+
ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
1980+
return []string{"--validarg", "test"}, ShellCompDirectiveDefault
1981+
},
1982+
}
1983+
childCmd.Flags().Bool("bool", false, "test bool flag")
1984+
childCmd.Flags().String("string", "", "test string flag")
1985+
_ = childCmd.RegisterFlagCompletionFunc("string", func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
1986+
return []string{"myval"}, ShellCompDirectiveDefault
1987+
})
1988+
1989+
// Important: This is a test for https://github.com/spf13/cobra/issues/1437
1990+
// Only add the subcommand after RegisterFlagCompletionFunc was called, do not change this order!
1991+
rootCmd.AddCommand(childCmd)
1992+
1993+
// Test that flag completion works for the subcmd
1994+
output, err := executeCommand(rootCmd, ShellCompRequestCmd, "child", "--string", "")
1995+
if err != nil {
1996+
t.Errorf("Unexpected error: %v", err)
1997+
}
1998+
1999+
expected := strings.Join([]string{
2000+
"myval",
2001+
":0",
2002+
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
2003+
2004+
if output != expected {
2005+
t.Errorf("expected: %q, got: %q", expected, output)
2006+
}
2007+
}
2008+
19742009
func TestFlagCompletionInGoWithDesc(t *testing.T) {
19752010
rootCmd := &Command{
19762011
Use: "root",

fish_completions.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,11 @@ function __%[1]s_prepare_completions
152152
# We don't need descriptions anyway since there is only a single
153153
# real completion which the shell will expand immediately.
154154
set -l split (string split --max 1 \t $__%[1]s_comp_results[1])
155-
155+
156156
# Fish won't add a space if the completion ends with any
157157
# of the following characters: @=/:.,
158158
set -l lastChar (string sub -s -1 -- $split)
159-
if not string match -r -q "[@=/:.,]" -- "$lastChar"
159+
if not string match -r -q "[@=/:.,]" -- "$lastChar"
160160
# In other cases, to support the "nospace" directive we trick the shell
161161
# by outputting an extra, longer completion.
162162
__%[1]s_debug "Adding second completion to perform nospace directive"

0 commit comments

Comments
 (0)