Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add flag category support (#796) #1368

Merged
merged 17 commits into from May 22, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions app.go
Expand Up @@ -52,6 +52,8 @@ type App struct {
HideVersion bool
// categories contains the categorized commands and is populated on app startup
categories CommandCategories
// flagCategories contains the categorized flags and is populated on app startup
flagCategories FlagCategories
// An action to execute when the shell completion flag is set
BashComplete BashCompleteFunc
// An action to execute before any subcommands are run, but after the context is ready
Expand Down Expand Up @@ -181,6 +183,15 @@ func (a *App) Setup() {
if c.HelpName == "" {
c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name)
}

fc := newFlagCategories()
meatballhat marked this conversation as resolved.
Show resolved Hide resolved
for _, fl := range c.Flags {
if cf, ok := fl.(CategorizableFlag); ok {
fc.AddFlag(cf.GetCategory(), cf)
}
}

c.flagCategories = fc
newCommands = append(newCommands, c)
}
a.Commands = newCommands
Expand All @@ -205,6 +216,13 @@ func (a *App) Setup() {
}
sort.Sort(a.categories.(*commandCategories))

a.flagCategories = newFlagCategories()
for _, fl := range a.Flags {
if cf, ok := fl.(CategorizableFlag); ok {
a.flagCategories.AddFlag(cf.GetCategory(), cf)
}
}

if a.Metadata == nil {
a.Metadata = make(map[string]interface{})
}
Expand Down Expand Up @@ -481,6 +499,14 @@ func (a *App) VisibleCommands() []*Command {
return ret
}

// VisibleFlagCategories returns a slice containing all the categories with the flags they contain
func (a *App) VisibleFlagCategories() []VisibleFlagCategory {
if a.flagCategories == nil {
a.flagCategories = newFlagCategories()
meatballhat marked this conversation as resolved.
Show resolved Hide resolved
}
return a.flagCategories.VisibleCategories()
}

// VisibleFlags returns a slice of the Flags with Hidden=false
func (a *App) VisibleFlags() []Flag {
return visibleFlags(a.Flags)
Expand Down
10 changes: 9 additions & 1 deletion app_test.go
Expand Up @@ -142,8 +142,8 @@ func ExampleApp_Run_appHelp() {
// help, h Shows a list of commands or help for one command
//
// GLOBAL OPTIONS:
// --name value a name to say (default: "bob")
// --help, -h show help (default: false)
// --name value a name to say (default: "bob")
// --version, -v print the version (default: false)
}

Expand Down Expand Up @@ -1924,6 +1924,14 @@ func TestApp_VisibleCategories(t *testing.T) {
expect(t, []CommandCategory{}, app.VisibleCategories())
}

func TestApp_VisibleFlagCategories(t *testing.T) {
app := &App{}
vfc := app.VisibleFlagCategories()
if len(vfc) != 0 {
t.Errorf("unexpected visible flag categories %+v", vfc)
}
}

func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) {
app := &App{
Action: func(c *Context) error { return nil },
Expand Down
83 changes: 82 additions & 1 deletion category.go
@@ -1,10 +1,12 @@
package cli

import "sort"

// CommandCategories interface allows for category manipulation
type CommandCategories interface {
// AddCommand adds a command to a category, creating a new category if necessary.
AddCommand(category string, command *Command)
// categories returns a copy of the category slice
// Categories returns a slice of categories sorted by name
Categories() []CommandCategory
}

Expand Down Expand Up @@ -77,3 +79,82 @@ func (c *commandCategory) VisibleCommands() []*Command {
}
return ret
}

// FlagCategories interface allows for category manipulation
type FlagCategories interface {
// AddFlags adds a flag to a category, creating a new category if necessary.
AddFlag(category string, fl Flag)
// VisibleCategories returns a slice of visible flag categories sorted by name
VisibleCategories() []VisibleFlagCategory
}

type defaultFlagCategories struct {
m map[string]*defaultVisibleFlagCategory
}

func newFlagCategories() FlagCategories {
return &defaultFlagCategories{
m: map[string]*defaultVisibleFlagCategory{},
}
}

func (f *defaultFlagCategories) AddFlag(category string, fl Flag) {
if _, ok := f.m[category]; !ok {
f.m[category] = &defaultVisibleFlagCategory{name: category, m: map[string]Flag{}}
}

f.m[category].m[fl.String()] = fl
}

func (f *defaultFlagCategories) VisibleCategories() []VisibleFlagCategory {
catNames := []string{}
for name := range f.m {
catNames = append(catNames, name)
}

sort.Strings(catNames)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt f.m already sorted ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC ranging over a map has undefined sort order: https://go.dev/play/p/aR7JKg4ae8r Or did you mean something else? 🤔


ret := make([]VisibleFlagCategory, len(catNames))
for i, name := range catNames {
ret[i] = f.m[name]
}

return ret
}

// VisibleFlagCategory is a category containing flags.
type VisibleFlagCategory interface {
// Name returns the category name string
Name() string
// Flags returns a slice of VisibleFlag sorted by name
Flags() []VisibleFlag
}

type defaultVisibleFlagCategory struct {
name string
m map[string]Flag
}

func (fc *defaultVisibleFlagCategory) Name() string {
return fc.name
}

func (fc *defaultVisibleFlagCategory) Flags() []VisibleFlag {
vfNames := []string{}
for flName, fl := range fc.m {
if vf, ok := fl.(VisibleFlag); ok {
if vf.IsVisible() {
vfNames = append(vfNames, flName)
}
}
}

sort.Strings(vfNames)

ret := make([]VisibleFlag, len(vfNames))
for i, flName := range vfNames {
ret[i] = fc.m[flName].(VisibleFlag)
}

return ret
}
11 changes: 10 additions & 1 deletion command.go
Expand Up @@ -38,7 +38,8 @@ type Command struct {
// List of child commands
Subcommands []*Command
// List of flags to parse
Flags []Flag
Flags []Flag
flagCategories FlagCategories
// Treat all flags as normal arguments if true
SkipFlagParsing bool
// Boolean to hide built-in help command and help flag
Expand Down Expand Up @@ -280,6 +281,14 @@ func (c *Command) startApp(ctx *Context) error {
return app.RunAsSubcommand(ctx)
}

// VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain
func (c *Command) VisibleFlagCategories() []VisibleFlagCategory {
if c.flagCategories == nil {
meatballhat marked this conversation as resolved.
Show resolved Hide resolved
c.flagCategories = newFlagCategories()
meatballhat marked this conversation as resolved.
Show resolved Hide resolved
}
return c.flagCategories.VisibleCategories()
}

// VisibleFlags returns a slice of the Flags with Hidden=false
func (c *Command) VisibleFlags() []Flag {
return visibleFlags(c.Flags)
Expand Down
8 changes: 8 additions & 0 deletions flag.go
Expand Up @@ -132,6 +132,14 @@ type VisibleFlag interface {
IsVisible() bool
}

// CategorizableFlag is an interface that allows us to potentially
// use a flag in a categorized representation.
type CategorizableFlag interface {
VisibleFlag

GetCategory() string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to just Category(). golang doesnt recommend using GetXXX

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the conflict with the field name Category? 🤔 I agree that GetXXX is generally bad style, but this is a language-level wart we're working around.

}

func flagSet(name string, flags []Flag) (*flag.FlagSet, error) {
set := flag.NewFlagSet(name, flag.ContinueOnError)

Expand Down
6 changes: 6 additions & 0 deletions flag_bool.go
Expand Up @@ -19,6 +19,7 @@ type BoolFlag struct {
DefaultText string
Destination *bool
HasBeenSet bool
Category string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meatballhat How would this tie-in with your generator code ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all the flag types defined in this repo have Category fields, then it'd be part of the struct template. Or did you mean the potential merge conflict? (or something else?)

}

// IsSet returns whether or not the flag has been set through env or file
Expand Down Expand Up @@ -52,6 +53,11 @@ func (f *BoolFlag) GetUsage() string {
return f.Usage
}

// GetCategory returns the category for the flag
func (f *BoolFlag) GetCategory() string {
return f.Category
}

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *BoolFlag) GetValue() string {
Expand Down
6 changes: 6 additions & 0 deletions flag_duration.go
Expand Up @@ -19,6 +19,7 @@ type DurationFlag struct {
DefaultText string
Destination *time.Duration
HasBeenSet bool
Category string
}

// IsSet returns whether or not the flag has been set through env or file
Expand Down Expand Up @@ -52,6 +53,11 @@ func (f *DurationFlag) GetUsage() string {
return f.Usage
}

// GetCategory returns the category for the flag
func (f *DurationFlag) GetCategory() string {
return f.Category
}

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *DurationFlag) GetValue() string {
Expand Down
6 changes: 6 additions & 0 deletions flag_float64.go
Expand Up @@ -19,6 +19,7 @@ type Float64Flag struct {
DefaultText string
Destination *float64
HasBeenSet bool
Category string
}

// IsSet returns whether or not the flag has been set through env or file
Expand Down Expand Up @@ -52,6 +53,11 @@ func (f *Float64Flag) GetUsage() string {
return f.Usage
}

// GetCategory returns the category for the flag
func (f *Float64Flag) GetCategory() string {
return f.Category
}

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *Float64Flag) GetValue() string {
Expand Down
6 changes: 6 additions & 0 deletions flag_float64_slice.go
Expand Up @@ -87,6 +87,7 @@ type Float64SliceFlag struct {
Value *Float64Slice
DefaultText string
HasBeenSet bool
Category string
}

// IsSet returns whether or not the flag has been set through env or file
Expand Down Expand Up @@ -120,6 +121,11 @@ func (f *Float64SliceFlag) GetUsage() string {
return f.Usage
}

// GetCategory returns the category for the flag
func (f *Float64SliceFlag) GetCategory() string {
return f.Category
}

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *Float64SliceFlag) GetValue() string {
Expand Down
6 changes: 6 additions & 0 deletions flag_generic.go
Expand Up @@ -24,6 +24,7 @@ type GenericFlag struct {
Value Generic
DefaultText string
HasBeenSet bool
Category string
}

// IsSet returns whether or not the flag has been set through env or file
Expand Down Expand Up @@ -57,6 +58,11 @@ func (f *GenericFlag) GetUsage() string {
return f.Usage
}

// GetCategory returns the category for the flag
func (f *GenericFlag) GetCategory() string {
return f.Category
}

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *GenericFlag) GetValue() string {
Expand Down
6 changes: 6 additions & 0 deletions flag_int.go
Expand Up @@ -19,6 +19,7 @@ type IntFlag struct {
DefaultText string
Destination *int
HasBeenSet bool
Category string
}

// IsSet returns whether or not the flag has been set through env or file
Expand Down Expand Up @@ -52,6 +53,11 @@ func (f *IntFlag) GetUsage() string {
return f.Usage
}

// GetCategory returns the category for the flag
func (f *IntFlag) GetCategory() string {
return f.Category
}

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *IntFlag) GetValue() string {
Expand Down
6 changes: 6 additions & 0 deletions flag_int64.go
Expand Up @@ -19,6 +19,7 @@ type Int64Flag struct {
DefaultText string
Destination *int64
HasBeenSet bool
Category string
}

// IsSet returns whether or not the flag has been set through env or file
Expand Down Expand Up @@ -52,6 +53,11 @@ func (f *Int64Flag) GetUsage() string {
return f.Usage
}

// GetCategory returns the category for the flag
func (f *Int64Flag) GetCategory() string {
return f.Category
}

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *Int64Flag) GetValue() string {
Expand Down
8 changes: 7 additions & 1 deletion flag_int64_slice.go
Expand Up @@ -88,6 +88,7 @@ type Int64SliceFlag struct {
Value *Int64Slice
DefaultText string
HasBeenSet bool
Category string
}

// IsSet returns whether or not the flag has been set through env or file
Expand Down Expand Up @@ -117,10 +118,15 @@ func (f *Int64SliceFlag) TakesValue() bool {
}

// GetUsage returns the usage string for the flag
func (f Int64SliceFlag) GetUsage() string {
func (f *Int64SliceFlag) GetUsage() string {
return f.Usage
}

// GetCategory returns the category for the flag
func (f *Int64SliceFlag) GetCategory() string {
return f.Category
}

// GetValue returns the flags value as string representation and an empty
// string if the flag takes no value at all.
func (f *Int64SliceFlag) GetValue() string {
Expand Down