Skip to content

Commit

Permalink
Show the actual available flags in useline
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
lugray committed Feb 1, 2024
1 parent bcfcff7 commit 9867fb6
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 11 deletions.
101 changes: 97 additions & 4 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
80 changes: 73 additions & 7 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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"}
Expand Down Expand Up @@ -2141,7 +2207,7 @@ func TestFlagErrorFuncHelp(t *testing.T) {
}

expected := `Usage:
c [flags]
c [--help]
Flags:
--help help for c
Expand Down

0 comments on commit 9867fb6

Please sign in to comment.