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

Aligns flag description #50

Merged
merged 5 commits into from
Oct 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 17 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<a href="https://github.com/avelino/awesome-go"><img src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg"></a>
</p>

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

Expand Down Expand Up @@ -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

```
Expand All @@ -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)
integrii marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down Expand Up @@ -152,7 +151,7 @@ print(boolFlagB)
print(flaggy.TrailingArguments[0])
```

# Supported Flag Types:
# Supported Flag Types

- string
- []string
Expand Down Expand Up @@ -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).
Expand All @@ -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!"
Expand Down Expand Up @@ -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.
6 changes: 3 additions & 3 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 .DefaultValue}} (default: {{.DefaultValue}}){{else}}{{if .Required}} (Required){{end}}{{end}}{{end}}{{end}}{{if .Subcommands}}
integrii marked this conversation as resolved.
Show resolved Hide resolved

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}}
Expand Down
78 changes: 73 additions & 5 deletions helpValues.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,6 +26,7 @@ type HelpSubcommand struct {
LongName string
Description string
Position int
Spacer string
}

// HelpPositional is used to template positional Help output
Expand All @@ -28,6 +36,7 @@ type HelpPositional struct {
Required bool
Position int
DefaultValue string
Spacer string
}

// HelpFlag is used to template string flag Help output
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -81,17 +96,26 @@ 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)
integrii marked this conversation as resolved.
Show resolved Hide resolved

// if the built-in version flag is enabled, then add it as a help flag
if p.ShowVersionWithVersionFlag {
defaultVersionFlag := HelpFlag{
ShortName: "",
LongName: versionFlagLongName,
Description: "Displays the program version string.",
DefaultValue: "",
Spacer: makeSpacer(versionFlagLongName, maxLength),
}
h.Flags = append(h.Flags, defaultVersionFlag)
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -219,3 +246,44 @@ 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
}

// 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)
}
File renamed without changes.
56 changes: 56 additions & 0 deletions helpValues_whitebox_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
1 change: 1 addition & 0 deletions subCommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ func (sc *Subcommand) AddPositionalValue(assignmentVar *string, name string, rel
AssignmentVar: assignmentVar,
Required: required,
Description: description,
defaultValue: *assignmentVar,
integrii marked this conversation as resolved.
Show resolved Hide resolved
}
sc.PositionalFlags = append(sc.PositionalFlags, &newPositionalValue)
}
Expand Down