From 081ad40dd618b96b6e184b6f6d88c19fde1070ba Mon Sep 17 00:00:00 2001 From: "daniel.schroeder" Date: Thu, 3 Oct 2019 19:33:40 +0200 Subject: [PATCH 1/5] aligns flag description --- README.md | 37 ++++++++++++-------------- help.go | 6 ++--- helpValues.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 77fa20c..63115ac 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@

-Sensible and _fast_ command-line flag parsing with excellent support for **subcommands** and **positional values**. Flags can be at any position. Flaggy has no required project or package layout like [Cobra requires](https://github.com/spf13/cobra/issues/641), and **no external dependencies**! +Sensible and _fast_ command-line flag parsing with excellent support for **subcommands** and **positional values**. Flags can be at any position. Flaggy has no required project or package layout like [Cobra requires](https://github.com/spf13/cobra/issues/641), and **no external dependencies**! -Check out the [godoc](http://godoc.org/github.com/integrii/flaggy), [examples directory](https://github.com/integrii/flaggy/tree/master/examples), and [examples in this readme](https://github.com/integrii/flaggy#super-simple-example) to get started quickly. You can also read the Flaggy introduction post with helpful examples [on my weblog](https://ericgreer.info/post/a-better-flags-package-for-go/). +Check out the [godoc](http://godoc.org/github.com/integrii/flaggy), [examples directory](https://github.com/integrii/flaggy/tree/master/examples), and [examples in this readme](https://github.com/integrii/flaggy#super-simple-example) to get started quickly. You can also read the Flaggy introduction post with helpful examples [on my weblog](https://ericgreer.info/post/a-better-flags-package-for-go/). # Installation @@ -44,7 +44,6 @@ Check out the [godoc](http://godoc.org/github.com/integrii/flaggy), [examples di - Optional but default help output when any invalid or unknown parameter is passed - It's _fast_. All flag and subcommand parsing takes less than `1ms` in most programs. - # Example Help Output ``` @@ -55,21 +54,21 @@ This is a prepend for help testCommand [subcommandA|subcommandB|subcommandC] [testPositionalA] [testPositionalB] Positional Variables: - testPositionalA - (Required) Test positional A does some things with a positional value. (default: defaultValue) - testPositionalB - Test positional B does some less than serious things with a positional value. + testPositionalA Test positional A does some things with a positional value. (Required) + testPositionalB Test positional B does some less than serious things with a positional value. Subcommands: - subcommandA (a) - Subcommand A is a command that does stuff - subcommandB (b) - Subcommand B is a command that does other stuff - subcommandC (c) - Subcommand C is a command that does SERIOUS stuff + subcommandA (a) Subcommand A is a command that does stuff + subcommandB (b) Subcommand B is a command that does other stuff + subcommandC (c) Subcommand C is a command that does SERIOUS stuff Flags: - --version Displays the program version string. - -h --help Displays help with available flag, subcommand, and positional value parameters. - -s --stringFlag This is a test string flag that does some stringy string stuff. - -i --intFlg This is a test int flag that does some interesting int stuff. (default: 5) - -b --boolFlag This is a test bool flag that does some booly bool stuff. (default: true) - -d --durationFlag This is a test duration flag that does some untimely stuff. (default: 1h23s) + --version Displays the program version string. + -h --help Displays help with available flag, subcommand, and positional value parameters. + -s --stringFlag This is a test string flag that does some stringy string stuff. + -i --intFlg This is a test int flag that does some interesting int stuff. (default: 5) + -b --boolFlag This is a test bool flag that does some booly bool stuff. (default: true) + -d --durationFlag This is a test duration flag that does some untimely stuff. (default: 1h23s) This is an append for help This is a help add-on message @@ -152,7 +151,7 @@ print(boolFlagB) print(flaggy.TrailingArguments[0]) ``` -# Supported Flag Types: +# Supported Flag Types - string - []string @@ -191,7 +190,6 @@ print(flaggy.TrailingArguments[0]) - net.IPMask - []net.IPMask - # Recommended Program Structure Best practice when using flaggy includes setting your program's name, description, and version (at build time). @@ -212,13 +210,13 @@ func init() { flaggy.SetName("Test Program") flaggy.SetDescription("A little example program") - // you can disable various things by changing bools on the default parser + // you can disable various things by changing bools on the default parser // (or your own parser if you have created one) flaggy.DefaultParser.ShowHelpOnUnexpected = false // you can set a help prepend or append on the default parser flaggy.DefaultParser.AdditionalHelpPrepend = "http://github.com/integrii/flaggy" - + // create any subcommands and set their parameters mySubcommand = flaggy.NewSubcommand("mySubcommand") mySubcommand.Description = "My great subcommand!" @@ -246,7 +244,6 @@ Test Program - A little example program http://github.com/integrii/flaggy ``` - # Contributions -Please feel free to open an issue if you find any bugs or see any features that make sense. Pull requests will be reviewed and accepted if they make sense, but it is always wise to submit a proposal issue before any major changes. +Please feel free to open an issue if you find any bugs or see any features that make sense. Pull requests will be reviewed and accepted if they make sense, but it is always wise to submit a proposal issue before any major changes. diff --git a/help.go b/help.go index cdbb02b..c5c49e9 100644 --- a/help.go +++ b/help.go @@ -10,13 +10,13 @@ const defaultHelpTemplate = `{{.CommandName}}{{if .Description}} - {{.Descriptio {{.UsageString}}{{end}}{{if .Positionals}} Positional Variables: {{range .Positionals}} - {{.Name}}{{if .Required}} (Required){{end}}{{if .Description}} - {{.Description}}{{end}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{end}}{{end}}{{end}}{{if .Subcommands}} + {{.Name}} {{.Spacer}}{{if .Description}} {{.Description}}{{end}}{{if .Required}} (Required){{end}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{end}}{{end}}{{end}}{{if .Subcommands}} Subcommands: {{range .Subcommands}} - {{.LongName}}{{if .ShortName}} ({{.ShortName}}){{end}}{{if .Position}}{{if gt .Position 1}} (position {{.Position}}){{end}}{{end}}{{if .Description}} - {{.Description}}{{end}}{{end}} + {{.LongName}}{{if .ShortName}} ({{.ShortName}}){{end}}{{if .Position}}{{if gt .Position 1}} (position {{.Position}}){{end}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{end}}{{end}} {{end}}{{if (gt (len .Flags) 0)}} Flags: {{if .Flags}}{{range .Flags}} - {{if .ShortName}}-{{.ShortName}} {{else}} {{end}}{{if .LongName}}--{{.LongName}} {{end}}{{if .Description}} {{.Description}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{end}}{{end}}{{end}}{{end}} + {{if .ShortName}}-{{.ShortName}} {{else}} {{end}}{{if .LongName}}--{{.LongName}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{end}}{{end}}{{end}}{{end}} {{end}}{{if .AppendMessage}}{{.AppendMessage}} {{end}}{{if .Message}} {{.Message}}{{end}} diff --git a/helpValues.go b/helpValues.go index 1db46a6..a36def0 100644 --- a/helpValues.go +++ b/helpValues.go @@ -1,5 +1,12 @@ package flaggy +import ( + "log" + "reflect" + "strings" + "unicode/utf8" +) + // Help represents the values needed to render a Help page type Help struct { Subcommands []HelpSubcommand @@ -19,6 +26,7 @@ type HelpSubcommand struct { LongName string Description string Position int + Spacer string } // HelpPositional is used to template positional Help output @@ -28,6 +36,7 @@ type HelpPositional struct { Required bool Position int DefaultValue string + Spacer string } // HelpFlag is used to template string flag Help output @@ -36,11 +45,12 @@ type HelpFlag struct { LongName string Description string DefaultValue string + Spacer string } // ExtractValues extracts Help template values from a subcommand and its parent -// parser. The parser is required in order to detect default flag settings -// for help and version outut. +// parser. The parser is required in order to detect default flag settings +// for help and version output. func (h *Help) ExtractValues(p *Parser, message string) { // accept message string for output @@ -56,6 +66,8 @@ func (h *Help) ExtractValues(p *Parser, message string) { // description h.Description = p.subcommandContext.Description + maxLength := getLongestNameLength(p.subcommandContext.Subcommands, 0) + // subcommands []HelpSubcommand for _, cmd := range p.subcommandContext.Subcommands { if cmd.Hidden { @@ -66,10 +78,13 @@ func (h *Help) ExtractValues(p *Parser, message string) { LongName: cmd.Name, Description: cmd.Description, Position: cmd.Position, + Spacer: makeSpacer(cmd.Name, maxLength), } h.Subcommands = append(h.Subcommands, newHelpSubcommand) } + maxLength = getLongestNameLength(p.subcommandContext.PositionalFlags, 0) + // parse positional flags into help output structs for _, pos := range p.subcommandContext.PositionalFlags { if pos.Hidden { @@ -81,10 +96,18 @@ func (h *Help) ExtractValues(p *Parser, message string) { Description: pos.Description, Required: pos.Required, DefaultValue: pos.defaultValue, + Spacer: makeSpacer(pos.Name, maxLength), } h.Positionals = append(h.Positionals, newHelpPositional) } + maxLength = len(versionFlagLongName) + if len(helpFlagLongName) > maxLength { + maxLength = len(helpFlagLongName) + } + maxLength = getLongestNameLength(p.subcommandContext.Flags, maxLength) + maxLength = getLongestNameLength(p.Flags, maxLength) + // if the built-in version flag is enabled, then add it as a help flag if p.ShowVersionWithVersionFlag { defaultVersionFlag := HelpFlag{ @@ -92,6 +115,7 @@ func (h *Help) ExtractValues(p *Parser, message string) { LongName: versionFlagLongName, Description: "Displays the program version string.", DefaultValue: "", + Spacer: makeSpacer(versionFlagLongName, maxLength), } h.Flags = append(h.Flags, defaultVersionFlag) } @@ -103,15 +127,16 @@ func (h *Help) ExtractValues(p *Parser, message string) { LongName: helpFlagLongName, Description: "Displays help with available flag, subcommand, and positional value parameters.", DefaultValue: "", + Spacer: makeSpacer(helpFlagLongName, maxLength), } h.Flags = append(h.Flags, defaultHelpFlag) } // go through every flag in the subcommand and add it to help output - h.parseFlagsToHelpFlags(p.subcommandContext.Flags) + h.parseFlagsToHelpFlags(p.subcommandContext.Flags, maxLength) // go through every flag in the parent parser and add it to help output - h.parseFlagsToHelpFlags(p.Flags) + h.parseFlagsToHelpFlags(p.Flags, maxLength) // formulate the usage string // first, we capture all the command and positional names by position @@ -167,7 +192,8 @@ func (h *Help) ExtractValues(p *Parser, message string) { // parseFlagsToHelpFlags parses the specified slice of flags into // help flags on the the calling help command -func (h *Help) parseFlagsToHelpFlags(flags []*Flag) { +func (h *Help) parseFlagsToHelpFlags(flags []*Flag, maxLength int) { + for _, f := range flags { if f.Hidden { continue @@ -202,6 +228,7 @@ func (h *Help) parseFlagsToHelpFlags(flags []*Flag) { LongName: f.LongName, Description: f.Description, DefaultValue: defaultValue, + Spacer: makeSpacer(f.LongName, maxLength), } h.AddFlagToHelp(newHelpFlag) } @@ -219,3 +246,39 @@ func (h *Help) AddFlagToHelp(f HelpFlag) { } h.Flags = append(h.Flags, f) } + +// getLongestNameLength takes a slice of any supported flag and returns the length of the longest of their names +func getLongestNameLength(slice interface{}, min int) int { + var maxLength = min + + s := reflect.ValueOf(slice) + if s.Kind() != reflect.Slice { + log.Panicf("Paremeter given to getLongestNameLength() is of type %s. Expected slice", s.Kind()) + } + + for i := 0; i < s.Len(); i++ { + option := s.Index(i).Interface() + var name string + switch t := option.(type) { + case *Subcommand: + name = t.Name + case *Flag: + name = t.LongName + case *PositionalValue: + name = t.Name + default: + log.Panicf("Unexpected type %T found in slice passed to getLongestNameLength(). Possible types: *Subcommand, *Flag, *PositionalValue", t) + } + length := len(name) + if length > maxLength { + maxLength = length + } + } + + return maxLength +} + +func makeSpacer(name string, maxLength int) string { + length := maxLength - utf8.RuneCountInString(name) + return strings.Repeat(" ", length) +} From 8b277dcfc234b04735f82db95b68ae0aa60b8e95 Mon Sep 17 00:00:00 2001 From: "daniel.schroeder" Date: Sat, 5 Oct 2019 17:55:36 +0200 Subject: [PATCH 2/5] ensures we don't attempt to create string with negative length --- helpValues.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpValues.go b/helpValues.go index a36def0..df2f679 100644 --- a/helpValues.go +++ b/helpValues.go @@ -278,7 +278,12 @@ func getLongestNameLength(slice interface{}, min int) int { return maxLength } +// makeSpacer creates a string of whitespaces, with a length of the given +// maxLength minus the length of the given name func makeSpacer(name string, maxLength int) string { length := maxLength - utf8.RuneCountInString(name) + if length < 0 { + length = 0 + } return strings.Repeat(" ", length) } From 468a13e52eb098829b131a60ef47542bec0e4227 Mon Sep 17 00:00:00 2001 From: "daniel.schroeder" Date: Sat, 5 Oct 2019 17:58:03 +0200 Subject: [PATCH 3/5] adds test for new functionality --- ...ues_test.go => helpValues_blackbox_test.go | 0 helpValues_whitebox_test.go | 56 +++++++++++++++++++ 2 files changed, 56 insertions(+) rename helpValues_test.go => helpValues_blackbox_test.go (100%) create mode 100644 helpValues_whitebox_test.go diff --git a/helpValues_test.go b/helpValues_blackbox_test.go similarity index 100% rename from helpValues_test.go rename to helpValues_blackbox_test.go diff --git a/helpValues_whitebox_test.go b/helpValues_whitebox_test.go new file mode 100644 index 0000000..9513cef --- /dev/null +++ b/helpValues_whitebox_test.go @@ -0,0 +1,56 @@ +package flaggy + +import ( + "testing" +) + +func TestMakeSpacer(t *testing.T) { + if spacer := makeSpacer("short", 20); len(spacer) != 15 { + t.Errorf("spacer length expected to be 15, got %d.", len(spacer)) + } + + if spacer := makeSpacer("very long", 20); len(spacer) != 11 { + t.Errorf("spacer length expected to be 11, got %d.", len(spacer)) + } + + if spacer := makeSpacer("very long", 0); len(spacer) != 0 { + t.Errorf("spacer length expected to be 0, got %d.", len(spacer)) + } +} + +func TestGetLongestNameLength(t *testing.T) { + input := []string{"short", "longer", "very-long"} + var subcommands []*Subcommand + var flags []*Flag + var positionalValues []*PositionalValue + + for _, name := range input { + subcommands = append(subcommands, NewSubcommand(name)) + flags = append(flags, &Flag{LongName: name}) + positionalValues = append(positionalValues, &PositionalValue{Name: name}) + } + + if l := getLongestNameLength(subcommands, 0); l != 9 { + t.Errorf("should have returned 9, got %d.", l) + } + + if l := getLongestNameLength(subcommands, 15); l != 15 { + t.Errorf("should have returned 15, got %d.", l) + } + + if l := getLongestNameLength(flags, 0); l != 9 { + t.Errorf("should have returned 9, got %d.", l) + } + + if l := getLongestNameLength(flags, 15); l != 15 { + t.Errorf("should have returned 15, got %d.", l) + } + + if l := getLongestNameLength(positionalValues, 0); l != 9 { + t.Errorf("should have returned 15, got %d.", l) + } + + if l := getLongestNameLength(positionalValues, 15); l != 15 { + t.Errorf("should have returned 9, got %d.", l) + } +} From bdffc7b71d830501c7fa2cef3904ed6146845b41 Mon Sep 17 00:00:00 2001 From: "daniel.schroeder" Date: Sat, 5 Oct 2019 18:40:27 +0200 Subject: [PATCH 4/5] fixes default value assignment --- subCommand.go | 1 + 1 file changed, 1 insertion(+) diff --git a/subCommand.go b/subCommand.go index 95d8729..c0c1307 100644 --- a/subCommand.go +++ b/subCommand.go @@ -610,6 +610,7 @@ func (sc *Subcommand) AddPositionalValue(assignmentVar *string, name string, rel AssignmentVar: assignmentVar, Required: required, Description: description, + defaultValue: *assignmentVar, } sc.PositionalFlags = append(sc.PositionalFlags, &newPositionalValue) } From 9f1f753d8f33b5068065faef6202f581ae9cfb26 Mon Sep 17 00:00:00 2001 From: "daniel.schroeder" Date: Sat, 5 Oct 2019 18:44:10 +0200 Subject: [PATCH 5/5] corrects positional var description. if it has a default value, it does not need to be shown as required --- help.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/help.go b/help.go index c5c49e9..353be7d 100644 --- a/help.go +++ b/help.go @@ -10,7 +10,7 @@ const defaultHelpTemplate = `{{.CommandName}}{{if .Description}} - {{.Descriptio {{.UsageString}}{{end}}{{if .Positionals}} Positional Variables: {{range .Positionals}} - {{.Name}} {{.Spacer}}{{if .Description}} {{.Description}}{{end}}{{if .Required}} (Required){{end}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{end}}{{end}}{{end}}{{if .Subcommands}} + {{.Name}} {{.Spacer}}{{if .Description}} {{.Description}}{{end}}{{if .DefaultValue}} (default: {{.DefaultValue}}){{else}}{{if .Required}} (Required){{end}}{{end}}{{end}}{{end}}{{if .Subcommands}} Subcommands: {{range .Subcommands}} {{.LongName}}{{if .ShortName}} ({{.ShortName}}){{end}}{{if .Position}}{{if gt .Position 1}} (position {{.Position}}){{end}}{{end}}{{if .Description}} {{.Spacer}}{{.Description}}{{end}}{{end}}