Skip to content

Commit 28e6e86

Browse files
committed
Update shell completion to respect flag groups
Signed-off-by: Marc Khouzam <[email protected]>
1 parent 421ddbc commit 28e6e86

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

Diff for: completions.go

+3
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,9 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
319319
var completions []string
320320
var directive ShellCompDirective
321321

322+
// Enforce flag groups before doing flag completions
323+
finalCmd.enforceFlagGroupsForCompletion()
324+
322325
// Note that we want to perform flagname completion even if finalCmd.DisableFlagParsing==true;
323326
// doing this allows for completion of persistent flag names even for commands that disable flag parsing.
324327
//

Diff for: completions_test.go

+168
Original file line numberDiff line numberDiff line change
@@ -2691,3 +2691,171 @@ func TestFixedCompletions(t *testing.T) {
26912691
t.Errorf("expected: %q, got: %q", expected, output)
26922692
}
26932693
}
2694+
2695+
func TestCompletionForGroupedFlags(t *testing.T) {
2696+
rootCmd := &Command{
2697+
Use: "root",
2698+
Run: emptyRun,
2699+
}
2700+
childCmd := &Command{
2701+
Use: "child",
2702+
ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
2703+
return []string{"subArg"}, ShellCompDirectiveNoFileComp
2704+
},
2705+
Run: emptyRun,
2706+
}
2707+
rootCmd.AddCommand(childCmd)
2708+
2709+
rootCmd.PersistentFlags().Int("group1-1", -1, "group1-1")
2710+
rootCmd.PersistentFlags().String("group1-2", "", "group1-2")
2711+
2712+
childCmd.Flags().Bool("group1-3", false, "group1-3")
2713+
childCmd.Flags().Bool("flag2", false, "flag2")
2714+
2715+
// Add flags to a group
2716+
childCmd.MarkFlagsRequiredTogether("group1-1", "group1-2", "group1-3")
2717+
2718+
// Test that flags in a group are not suggested without the - prefix
2719+
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "")
2720+
if err != nil {
2721+
t.Errorf("Unexpected error: %v", err)
2722+
}
2723+
2724+
expected := strings.Join([]string{
2725+
"subArg",
2726+
":4",
2727+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
2728+
2729+
if output != expected {
2730+
t.Errorf("expected: %q, got: %q", expected, output)
2731+
}
2732+
2733+
// Test that flags in a group are suggested with the - prefix
2734+
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "-")
2735+
if err != nil {
2736+
t.Errorf("Unexpected error: %v", err)
2737+
}
2738+
2739+
expected = strings.Join([]string{
2740+
"--group1-1",
2741+
"--group1-2",
2742+
"--flag2",
2743+
"--group1-3",
2744+
":4",
2745+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
2746+
2747+
if output != expected {
2748+
t.Errorf("expected: %q, got: %q", expected, output)
2749+
}
2750+
2751+
// Test that when a flag in a group is present, the other flags in the group are suggested
2752+
// even without the - prefix
2753+
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "--group1-2", "value", "")
2754+
if err != nil {
2755+
t.Errorf("Unexpected error: %v", err)
2756+
}
2757+
2758+
expected = strings.Join([]string{
2759+
"--group1-1",
2760+
"--group1-3",
2761+
"subArg",
2762+
":4",
2763+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
2764+
2765+
if output != expected {
2766+
t.Errorf("expected: %q, got: %q", expected, output)
2767+
}
2768+
2769+
// Test that when all flags in a group are present, flags are not suggested without the - prefix
2770+
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child",
2771+
"--group1-1", "8",
2772+
"--group1-2", "value2",
2773+
"--group1-3",
2774+
"")
2775+
if err != nil {
2776+
t.Errorf("Unexpected error: %v", err)
2777+
}
2778+
2779+
expected = strings.Join([]string{
2780+
"subArg",
2781+
":4",
2782+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
2783+
2784+
if output != expected {
2785+
t.Errorf("expected: %q, got: %q", expected, output)
2786+
}
2787+
}
2788+
2789+
func TestCompletionForMutuallyExclusiveFlags(t *testing.T) {
2790+
rootCmd := &Command{
2791+
Use: "root",
2792+
Run: emptyRun,
2793+
}
2794+
childCmd := &Command{
2795+
Use: "child",
2796+
ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
2797+
return []string{"subArg"}, ShellCompDirectiveNoFileComp
2798+
},
2799+
Run: emptyRun,
2800+
}
2801+
rootCmd.AddCommand(childCmd)
2802+
2803+
rootCmd.PersistentFlags().IntSlice("group1-1", []int{1}, "group1-1")
2804+
rootCmd.PersistentFlags().String("group1-2", "", "group1-2")
2805+
2806+
childCmd.Flags().Bool("group1-3", false, "group1-3")
2807+
childCmd.Flags().Bool("flag2", false, "flag2")
2808+
2809+
// Add flags to a group
2810+
childCmd.MarkFlagsMutuallyExclusive("group1-1", "group1-2", "group1-3")
2811+
2812+
// Test that flags in a mutually exclusive group are not suggested without the - prefix
2813+
output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "")
2814+
if err != nil {
2815+
t.Errorf("Unexpected error: %v", err)
2816+
}
2817+
2818+
expected := strings.Join([]string{
2819+
"subArg",
2820+
":4",
2821+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
2822+
2823+
if output != expected {
2824+
t.Errorf("expected: %q, got: %q", expected, output)
2825+
}
2826+
2827+
// Test that flags in a mutually exclusive group are suggested with the - prefix
2828+
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "-")
2829+
if err != nil {
2830+
t.Errorf("Unexpected error: %v", err)
2831+
}
2832+
2833+
expected = strings.Join([]string{
2834+
"--group1-1",
2835+
"--group1-2",
2836+
"--flag2",
2837+
"--group1-3",
2838+
":4",
2839+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
2840+
2841+
if output != expected {
2842+
t.Errorf("expected: %q, got: %q", expected, output)
2843+
}
2844+
2845+
// Test that when a flag in a mutually exclusive group is present, the other flags in the group are
2846+
// not suggested even with the - prefix
2847+
output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child", "--group1-1", "8", "-")
2848+
if err != nil {
2849+
t.Errorf("Unexpected error: %v", err)
2850+
}
2851+
2852+
expected = strings.Join([]string{
2853+
"--group1-1", // Should be repeated since it is a slice
2854+
"--flag2",
2855+
":4",
2856+
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n")
2857+
2858+
if output != expected {
2859+
t.Errorf("expected: %q, got: %q", expected, output)
2860+
}
2861+
}

Diff for: flag_groups.go

+48
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,51 @@ func sortedKeys(m map[string]map[string]bool) []string {
157157
sort.Strings(keys)
158158
return keys
159159
}
160+
161+
// enforceFlagGroupsForCompletion will do the following:
162+
// - when a flag in a group is present, other flags in the group will be marked required
163+
// - when a flag in a mutually exclusive group is present, other flags in the group will be marked as hidden
164+
// This allows the standard completion logic to behave appropriately for flag groups
165+
func (c *Command) enforceFlagGroupsForCompletion() {
166+
if c.DisableFlagParsing {
167+
return
168+
}
169+
170+
groupStatus := map[string]map[string]bool{}
171+
mutuallyExclusiveGroupStatus := map[string]map[string]bool{}
172+
c.Flags().VisitAll(func(pflag *flag.Flag) {
173+
processFlagForGroupAnnotation(pflag, requiredAsGroup, groupStatus)
174+
processFlagForGroupAnnotation(pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus)
175+
})
176+
177+
// If a flag that is part of a group is present, we make all the other flags
178+
// of that group required so that the shell completion suggests them automatically
179+
for flagList, flagnameAndStatus := range groupStatus {
180+
for _, isSet := range flagnameAndStatus {
181+
if isSet {
182+
// One of the flags of the group is set, mark the other ones as required
183+
for _, fName := range strings.Split(flagList, " ") {
184+
_ = c.MarkFlagRequired(fName)
185+
}
186+
}
187+
}
188+
}
189+
190+
// If a flag that is mutually exclusive to others is present, we hide the other
191+
// flags of that group so the shell completion does not suggest them
192+
for flagList, flagnameAndStatus := range mutuallyExclusiveGroupStatus {
193+
for flagName, isSet := range flagnameAndStatus {
194+
if isSet {
195+
// One of the flags of the mutually exclusive group is set, mark the other ones as hidden
196+
// Don't mark the flag that is already set as hidden because it may be an
197+
// array or slice flag and therefore must continue being suggested
198+
for _, fName := range strings.Split(flagList, " ") {
199+
if fName != flagName {
200+
flag := c.Flags().Lookup(fName)
201+
flag.Hidden = true
202+
}
203+
}
204+
}
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)