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 <pholzing@redhat.com>

* Fix trailing whitespaces in fish comp scripts

Signed-off-by: Paul Holzinger <pholzing@redhat.com>
This commit is contained in:
Paul Holzinger 2021-07-02 17:25:47 +02:00 committed by GitHub
parent 07861c800d
commit de187e874d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 54 additions and 13 deletions

View file

@ -512,7 +512,9 @@ func writeLocalNonPersistentFlag(buf io.StringWriter, flag *pflag.Flag) {
// Setup annotations for go completions for registered flags // Setup annotations for go completions for registered flags
func prepareCustomAnnotationsForFlags(cmd *Command) { func prepareCustomAnnotationsForFlags(cmd *Command) {
for flag := range cmd.Root().flagCompletionFunctions { flagCompletionMutex.RLock()
defer flagCompletionMutex.RUnlock()
for flag := range flagCompletionFunctions {
// Make sure the completion script calls the __*_go_custom_completion function for // Make sure the completion script calls the __*_go_custom_completion function for
// every registered flag. We need to do this here (and not when the flag was registered // every registered flag. We need to do this here (and not when the flag was registered
// for completion) so that we can know the root command name for the prefix // for completion) so that we can know the root command name for the prefix

View file

@ -142,9 +142,6 @@ type Command struct {
// that we can use on every pflag set and children commands // that we can use on every pflag set and children commands
globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName
//flagCompletionFunctions is map of flag completion functions.
flagCompletionFunctions map[*flag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
// usageFunc is usage func defined by user. // usageFunc is usage func defined by user.
usageFunc func(*Command) error usageFunc func(*Command) error
// usageTemplate is usage template defined by user. // usageTemplate is usage template defined by user.

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"sync"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@ -17,6 +18,12 @@ const (
ShellCompNoDescRequestCmd = "__completeNoDesc" ShellCompNoDescRequestCmd = "__completeNoDesc"
) )
// Global map of flag completion functions. Make sure to use flagCompletionMutex before you try to read and write from it.
var flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
// lock for reading and writing from flagCompletionFunctions
var flagCompletionMutex = &sync.RWMutex{}
// ShellCompDirective is a bit map representing the different behaviors the shell // ShellCompDirective is a bit map representing the different behaviors the shell
// can be instructed to have once completions have been provided. // can be instructed to have once completions have been provided.
type ShellCompDirective int type ShellCompDirective int
@ -100,15 +107,13 @@ func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Comman
if flag == nil { if flag == nil {
return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName) return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' does not exist", flagName)
} }
flagCompletionMutex.Lock()
defer flagCompletionMutex.Unlock()
root := c.Root() if _, exists := flagCompletionFunctions[flag]; exists {
if _, exists := root.flagCompletionFunctions[flag]; exists {
return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName) return fmt.Errorf("RegisterFlagCompletionFunc: flag '%s' already registered", flagName)
} }
if root.flagCompletionFunctions == nil { flagCompletionFunctions[flag] = f
root.flagCompletionFunctions = map[*pflag.Flag]func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective){}
}
root.flagCompletionFunctions[flag] = f
return nil return nil
} }
@ -402,7 +407,9 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
// Find the completion function for the flag or command // Find the completion function for the flag or command
var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) var completionFn func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective)
if flag != nil && flagCompletion { if flag != nil && flagCompletion {
completionFn = c.Root().flagCompletionFunctions[flag] flagCompletionMutex.RLock()
completionFn = flagCompletionFunctions[flag]
flagCompletionMutex.RUnlock()
} else { } else {
completionFn = finalCmd.ValidArgsFunction completionFn = finalCmd.ValidArgsFunction
} }

View file

@ -1971,6 +1971,41 @@ func TestFlagCompletionWithNotInterspersedArgs(t *testing.T) {
} }
} }
func TestFlagCompletionWorksRootCommandAddedAfterFlags(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
},
}
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
})
// Important: This is a test for https://github.com/spf13/cobra/issues/1437
// Only add the subcommand after RegisterFlagCompletionFunc was called, do not change this order!
rootCmd.AddCommand(childCmd)
// Test that flag completion works for the subcmd
output, err := executeCommand(rootCmd, ShellCompRequestCmd, "child", "--string", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
expected := strings.Join([]string{
"myval",
":0",
"Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n")
if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
func TestFlagCompletionInGoWithDesc(t *testing.T) { func TestFlagCompletionInGoWithDesc(t *testing.T) {
rootCmd := &Command{ rootCmd := &Command{
Use: "root", Use: "root",

View file

@ -152,11 +152,11 @@ function __%[1]s_prepare_completions
# We don't need descriptions anyway since there is only a single # We don't need descriptions anyway since there is only a single
# real completion which the shell will expand immediately. # real completion which the shell will expand immediately.
set -l split (string split --max 1 \t $__%[1]s_comp_results[1]) set -l split (string split --max 1 \t $__%[1]s_comp_results[1])
# Fish won't add a space if the completion ends with any # Fish won't add a space if the completion ends with any
# of the following characters: @=/:., # of the following characters: @=/:.,
set -l lastChar (string sub -s -1 -- $split) set -l lastChar (string sub -s -1 -- $split)
if not string match -r -q "[@=/:.,]" -- "$lastChar" if not string match -r -q "[@=/:.,]" -- "$lastChar"
# In other cases, to support the "nospace" directive we trick the shell # In other cases, to support the "nospace" directive we trick the shell
# by outputting an extra, longer completion. # by outputting an extra, longer completion.
__%[1]s_debug "Adding second completion to perform nospace directive" __%[1]s_debug "Adding second completion to perform nospace directive"