From 9867fb647f9889297af5beef8b5a2c9b449b5093 Mon Sep 17 00:00:00 2001 From: Lisa Ugray Date: Thu, 1 Feb 2024 12:08:03 -0500 Subject: [PATCH] Show the actual available flags in useline Use common useline notation for available flags: - `[-f foo]` for (standard) optional flags - `[-f foo | -b bar]` for mutually exclusive flags - `-f foo` for required flags - `{-f foo | -b bar}` for mutually exclusive flags where one is required - `[-f foo -b bar]` for flags required as a group For a boolean flag `f`, ` foo` is dropped from the above. The foo/bar comes from `flag.UnquoteUsage`. --- command.go | 101 ++++++++++++++++++++++++++++++++++++++++++++++-- command_test.go | 80 ++++++++++++++++++++++++++++++++++---- 2 files changed, 170 insertions(+), 11 deletions(-) diff --git a/command.go b/command.go index b6f8f4b1..8c2d9b1c 100644 --- a/command.go +++ b/command.go @@ -247,6 +247,11 @@ type Command struct { // line of a command when printing help or generating docs DisableFlagsInUseLine bool + // DisableVerboseFlagsInUseLine will add only [flags] to the usage line of + // a command when printing help or generating docs instead of a more verbose + // form showing the actual available flags + DisableVerboseFlagsInUseLine bool + // DisableSuggestions disables the suggestions based on Levenshtein distance // that go along with 'unknown command' messages. DisableSuggestions bool @@ -1449,13 +1454,101 @@ func (c *Command) UseLine() string { } else { useline = use } + return useline + c.uselineFlags(useline) +} + +func (c *Command) uselineFlags(useline string) string { if c.DisableFlagsInUseLine { - return useline + return "" } - if c.HasAvailableFlags() && !strings.Contains(useline, "[flags]") { - useline += " [flags]" + if !c.HasAvailableFlags() { + return "" } - return useline + if strings.Contains(useline, "[flags]") { + return "" + } + if c.DisableVerboseFlagsInUseLine { + return " [flags]" + } + + included := map[*flag.Flag]struct{}{} + flagsLine := "" + + c.flags.VisitAll(func(f *flag.Flag) { + if _, ok := included[f]; ok || f.Hidden { + return + } + included[f] = struct{}{} + + rag := flagsFromAnnotation(c, f, requiredAsGroup) + me := flagsFromAnnotation(c, f, mutuallyExclusive) + or := flagsFromAnnotation(c, f, oneRequired) + + if len(rag) > 0 { + gr := []string{} + for _, fl := range rag { + included[fl] = struct{}{} + gr = append(gr, shortUsage(fl)) + } + flagsLine += " [" + strings.Join(gr, " ") + "]" + } else if len(me) > 0 { + gr := []string{} + for _, fl := range me { + included[fl] = struct{}{} + gr = append(gr, shortUsage(fl)) + } + if sameFlags(me, or) { + flagsLine += " {" + strings.Join(gr, " | ") + "}" + } else { + flagsLine += " [" + strings.Join(gr, " | ") + "]" + } + } else if req, found := f.Annotations[BashCompOneRequiredFlag]; found && req[0] == "true" { + flagsLine += " " + shortUsage(f) + } else { + flagsLine += " [" + shortUsage(f) + "]" + } + }) + return flagsLine +} + +func shortUsage(f *flag.Flag) (usage string) { + if f.Shorthand != "" { + usage = "-" + f.Shorthand + } else { + usage = "--" + f.Name + } + + varname, _ := flag.UnquoteUsage(f) + if varname != "" { + usage += " " + varname + } + return +} + +func flagsFromAnnotation(c *Command, f *flag.Flag, name string) map[string]*flag.Flag { + flags := map[string]*flag.Flag{} + a := f.Annotations[name] + for _, s := range a { + for _, name := range strings.Split(s, " ") { + fl := c.flags.Lookup(name) + if fl != nil { + flags[name] = fl + } + } + } + return flags +} + +func sameFlags(a, b map[string]*flag.Flag) bool { + if len(a) != len(b) { + return false + } + for k, v := range a { + if b[k] != v { + return false + } + } + return true } // DebugFlags used to determine which flags have been assigned to which commands diff --git a/command_test.go b/command_test.go index b7d88e4d..bef2553b 100644 --- a/command_test.go +++ b/command_test.go @@ -384,7 +384,7 @@ func TestPlugin(t *testing.T) { t.Errorf("Unexpected error: %v", err) } - checkStringContains(t, cmdHelp, "kubectl plugin [flags]") + checkStringContains(t, cmdHelp, "kubectl plugin [-h]") checkStringContains(t, cmdHelp, "help for kubectl plugin") } @@ -398,7 +398,7 @@ func TestPluginWithSubCommands(t *testing.T) { }, } - subCmd := &Command{Use: "sub [flags]", Args: NoArgs, Run: emptyRun} + subCmd := &Command{Use: "sub [-h]", Args: NoArgs, Run: emptyRun} rootCmd.AddCommand(subCmd) rootHelp, err := executeCommand(rootCmd, "-h") @@ -415,7 +415,7 @@ func TestPluginWithSubCommands(t *testing.T) { t.Errorf("Unexpected error: %v", err) } - checkStringContains(t, childHelp, "kubectl plugin sub [flags]") + checkStringContains(t, childHelp, "kubectl plugin sub [-h]") checkStringContains(t, childHelp, "help for sub") helpHelp, err := executeCommand(rootCmd, "help", "-h") @@ -975,7 +975,7 @@ func TestHelpCommandExecutedOnChildWithFlagThatShadowsParentFlag(t *testing.T) { } expected := `Usage: - parent child [flags] + parent child [--bar] [--baz] [--foo] [-h] Flags: --baz child baz usage @@ -1053,11 +1053,11 @@ func TestHelpFlagInHelp(t *testing.T) { t.Errorf("Unexpected error: %v", err) } - checkStringContains(t, output, "[flags]") + checkStringContains(t, output, "[-h]") } func TestFlagsInUsage(t *testing.T) { - rootCmd := &Command{Use: "root", Args: NoArgs, Run: func(*Command, []string) {}} + rootCmd := &Command{Use: "root", Args: NoArgs, Run: func(*Command, []string) {}, DisableVerboseFlagsInUseLine: true} output, err := executeCommand(rootCmd, "--help") if err != nil { t.Errorf("Unexpected error: %v", err) @@ -1066,6 +1066,72 @@ func TestFlagsInUsage(t *testing.T) { checkStringContains(t, output, "[flags]") } +func TestMutuallyExclusiveFlagsInUsage(t *testing.T) { + rootCmd := &Command{Use: "root", Args: NoArgs, Run: func(*Command, []string) {}} + rootCmd.Flags().SortFlags = false + rootCmd.Flags().Bool("foo", false, "foo desc") + rootCmd.Flags().Bool("bar", false, "bar desc") + rootCmd.MarkFlagsMutuallyExclusive("foo", "bar") + output, err := executeCommand(rootCmd, "--help") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, "[--foo | --bar]") +} + +func TestMutuallyExclusiveRequiredFlagsInUsage(t *testing.T) { + rootCmd := &Command{Use: "root", Args: NoArgs, Run: func(*Command, []string) {}} + rootCmd.Flags().SortFlags = false + rootCmd.Flags().Bool("foo", false, "foo desc") + rootCmd.Flags().Bool("bar", false, "bar desc") + rootCmd.MarkFlagsMutuallyExclusive("foo", "bar") + rootCmd.MarkFlagsOneRequired("foo", "bar") + output, err := executeCommand(rootCmd, "--help") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, "{--foo | --bar}") +} + +func TestRequiredFlagInUsage(t *testing.T) { + rootCmd := &Command{Use: "root", Args: NoArgs, Run: func(*Command, []string) {}} + rootCmd.Flags().Bool("foo", false, "foo desc") + rootCmd.MarkFlagRequired("foo") + output, err := executeCommand(rootCmd, "--help") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, " --foo ") +} + +func TestRequiredTogetherFlagsInUsage(t *testing.T) { + rootCmd := &Command{Use: "root", Args: NoArgs, Run: func(*Command, []string) {}} + rootCmd.Flags().SortFlags = false + rootCmd.Flags().Bool("foo", false, "foo desc") + rootCmd.Flags().Bool("bar", false, "bar desc") + rootCmd.MarkFlagsRequiredTogether("foo", "bar") + output, err := executeCommand(rootCmd, "--help") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, "[--foo --bar]") +} + +func TestFlagWithArgInUsage(t *testing.T) { + rootCmd := &Command{Use: "root", Args: NoArgs, Run: func(*Command, []string) {}} + rootCmd.Flags().String("output", "", "the output `file`") + output, err := executeCommand(rootCmd, "--help") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, output, "[--output file]") +} + func TestHelpExecutedOnNonRunnableChild(t *testing.T) { rootCmd := &Command{Use: "root", Run: emptyRun} childCmd := &Command{Use: "child", Long: "Long description"} @@ -2141,7 +2207,7 @@ func TestFlagErrorFuncHelp(t *testing.T) { } expected := `Usage: - c [flags] + c [--help] Flags: --help help for c