Skip to content

Commit

Permalink
Update shell completion to respect flag groups
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
  • Loading branch information
marckhouzam committed Apr 14, 2022
1 parent a2600fe commit a7b9a8c
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 0 deletions.
3 changes: 3 additions & 0 deletions completions.go
Expand Up @@ -319,6 +319,9 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
var completions []string
var directive ShellCompDirective

// Enforce flag groups before doing flag completions
finalCmd.enforceFlagGroupsForCompletion()

// Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true;
// doing this allows for completion of persistent flag names even for commands that disable flag parsing.
//
Expand Down
168 changes: 168 additions & 0 deletions completions_test.go
Expand Up @@ -2691,3 +2691,171 @@ func TestFixedCompletions(t *testing.T) {
t.Errorf("expected: %q, got: %q", expected, output)
}
}

func TestCompletionForGroupedFlags(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
childCmd := &Command{
Use: "child",
ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
return []string{"subArg"}, ShellCompDirectiveNoFileComp
},
Run: emptyRun,
}
rootCmd.AddCommand(childCmd)

rootCmd.PersistentFlags().Int("group1-1", -1, "group1-1")
rootCmd.PersistentFlags().String("group1-2", "", "group1-2")

childCmd.Flags().Bool("group1-3", false, "group1-3")
childCmd.Flags().Bool("flag2", false, "flag2")

// Add flags to a group
childCmd.MarkFlagsRequiredTogether("group1-1", "group1-2", "group1-3")

// Test that flags in a group are not suggested without the - prefix
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected := strings.Join([]string{
"subArg",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}

// Test that flags in a group are suggested with the - prefix
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "-")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected = strings.Join([]string{
"--group1-1",
"--group1-2",
"--flag2",
"--group1-3",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}

// Test that when a flag in a group is present, the other flags in the group are suggested
// even without the - prefix
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "--group1-2", "value", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected = strings.Join([]string{
"--group1-1",
"--group1-3",
"subArg",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}

// Test that when all flags in a group are present, flags are not suggested without the - prefix
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child",
"--group1-1", "8",
"--group1-2", "value2",
"--group1-3",
"")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected = strings.Join([]string{
"subArg",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}

func TestCompletionForMutuallyExclusiveFlags(t *testing.T) {
rootCmd := &Command{
Use: "root",
Run: emptyRun,
}
childCmd := &Command{
Use: "child",
ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
return []string{"subArg"}, ShellCompDirectiveNoFileComp
},
Run: emptyRun,
}
rootCmd.AddCommand(childCmd)

rootCmd.PersistentFlags().IntSlice("group1-1", []int{1}, "group1-1")
rootCmd.PersistentFlags().String("group1-2", "", "group1-2")

childCmd.Flags().Bool("group1-3", false, "group1-3")
childCmd.Flags().Bool("flag2", false, "flag2")

// Add flags to a group
childCmd.MarkFlagsMutuallyExclusive("group1-1", "group1-2", "group1-3")

// Test that flags in a mutually exclusive group are not suggested without the - prefix
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected := strings.Join([]string{
"subArg",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}

// Test that flags in a mutually exclusive group are suggested with the - prefix
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "-")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected = strings.Join([]string{
"--group1-1",
"--group1-2",
"--flag2",
"--group1-3",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}

// Test that when a flag in a mutually exclusive group is present, the other flags in the group are
// not suggested even with the - prefix
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "--group1-1", "8", "-")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

expected = strings.Join([]string{
"--group1-1", // Should be repeated since it is a slice
"--flag2",
":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")

if output != expected {
t.Errorf("expected: %q, got: %q", expected, output)
}
}
49 changes: 49 additions & 0 deletions flag_groups.go
Expand Up @@ -172,3 +172,52 @@ func sortedKeys(m map[string]map[string]bool) []string {
sort.Strings(keys)
return keys
}

// enforceFlagGroupsForCompletion will do the following:
// - when a flag in a group is present, other flags in the group will be marked required
// - when a flag in a mutually exclusive group is present, other flags in the group will be marked as hidden
// This allows the standard completion logic to behave appropriately for flag groups
func (c *Command) enforceFlagGroupsForCompletion() {
if c.DisableFlagParsing {
return
}

flags := c.Flags()
groupStatus := map[string]map[string]bool{}
mutuallyExclusiveGroupStatus := map[string]map[string]bool{}
c.Flags().VisitAll(func(pflag *flag.Flag) {
processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus)
processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus)
})

// If a flag that is part of a group is present, we make all the other flags
// of that group required so that the shell completion suggests them automatically
for flagList, flagnameAndStatus := range groupStatus {
for _, isSet := range flagnameAndStatus {
if isSet {
// One of the flags of the group is set, mark the other ones as required
for _, fName := range strings.Split(flagList, " ") {
_ = c.MarkFlagRequired(fName)
}
}
}
}

// If a flag that is mutually exclusive to others is present, we hide the other
// flags of that group so the shell completion does not suggest them
for flagList, flagnameAndStatus := range mutuallyExclusiveGroupStatus {
for flagName, isSet := range flagnameAndStatus {
if isSet {
// One of the flags of the mutually exclusive group is set, mark the other ones as hidden
// Don't mark the flag that is already set as hidden because it may be an
// array or slice flag and therefore must continue being suggested
for _, fName := range strings.Split(flagList, " ") {
if fName != flagName {
flag := c.Flags().Lookup(fName)
flag.Hidden = true
}
}
}
}
}
}

0 comments on commit a7b9a8c

Please sign in to comment.