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`.
This commit is contained in:
Lisa Ugray 2024-02-01 12:08:03 -05:00
parent bcfcff729e
commit 9867fb647f
2 changed files with 170 additions and 11 deletions

View file

@ -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

View file

@ -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