summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md22
-rw-r--r--example_test.go7
-rw-r--r--parse.go52
-rw-r--r--parse_test.go33
-rw-r--r--subcommand.go42
-rw-r--r--subcommand_test.go95
-rw-r--r--usage.go191
-rw-r--r--usage_test.go334
8 files changed, 630 insertions, 146 deletions
diff --git a/README.md b/README.md
index f105b17..4d23034 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,23 @@ $ WORKERS='1,99' ./example
Workers: [1 99]
```
+You can also have an environment variable that doesn't match the arg name:
+
+```go
+var args struct {
+ Workers int `arg:"--count,env:NUM_WORKERS"`
+}
+arg.MustParse(&args)
+fmt.Println("Workers:", args.Workers)
+```
+
+```
+$ NUM_WORKERS=6 ./example
+Workers: 6
+$ NUM_WORKERS=6 ./example --count 4
+Workers: 4
+```
+
### Usage strings
```go
var args struct {
@@ -284,6 +301,9 @@ $ ./example --version
someprogram 4.3.0
```
+> **Note**
+> If a `--version` flag is defined in `args` or any subcommand, it overrides the built-in versioning.
+
### Overriding option names
```go
@@ -591,7 +611,7 @@ https://godoc.org/github.com/alexflint/go-arg
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
-The `flag` library that ships in the standard library seems awkward to me. Positional arguments must preceed options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
+The `flag` library that ships in the standard library seems awkward to me. Positional arguments must precede options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
Many third-party argument parsing libraries are great for writing sophisticated command line interfaces, but feel to me like overkill for a simple script with a few flags.
diff --git a/example_test.go b/example_test.go
index 4bd7632..d3622bf 100644
--- a/example_test.go
+++ b/example_test.go
@@ -163,6 +163,7 @@ func Example_helpText() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
+ mustParseOut = os.Stdout
MustParse(&args)
@@ -195,6 +196,7 @@ func Example_helpPlaceholder() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
+ mustParseOut = os.Stdout
MustParse(&args)
@@ -235,6 +237,7 @@ func Example_helpTextWithSubcommand() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
+ mustParseOut = os.Stdout
MustParse(&args)
@@ -272,6 +275,7 @@ func Example_helpTextWhenUsingSubcommand() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
+ mustParseOut = os.Stdout
MustParse(&args)
@@ -392,6 +396,7 @@ func Example_errorText() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
+ mustParseOut = os.Stdout
MustParse(&args)
@@ -415,6 +420,7 @@ func Example_errorTextForSubcommand() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
+ mustParseOut = os.Stdout
MustParse(&args)
@@ -450,6 +456,7 @@ func Example_subcommand() {
// This is only necessary when running inside golang's runnable example harness
mustParseExit = func(int) {}
+ mustParseOut = os.Stdout
MustParse(&args)
diff --git a/parse.go b/parse.go
index 0bdddc7..2bed8bf 100644
--- a/parse.go
+++ b/parse.go
@@ -56,12 +56,13 @@ type spec struct {
env string // the name of the environment variable for this option, or empty for none
defaultValue reflect.Value // default value for this option
defaultString string // default value for this option, in string form to be displayed in help text
- placeholder string // name of the data in help
+ placeholder string // placeholder string in help
}
// command represents a named subcommand, or the top-level command
type command struct {
name string
+ aliases []string
help string
dest path
specs []*spec
@@ -75,27 +76,21 @@ var ErrHelp = errors.New("help requested by user")
// ErrVersion indicates that the builtin --version was provided
var ErrVersion = errors.New("version requested by user")
-// for monkey patching in example code
+// for monkey patching in example and test code
var mustParseExit = os.Exit
+var mustParseOut io.Writer = os.Stdout
// MustParse processes command line arguments and exits upon failure
func MustParse(dest ...interface{}) *Parser {
- return mustParse(Config{Exit: mustParseExit}, dest...)
+ return mustParse(Config{Exit: mustParseExit, Out: mustParseOut}, dest...)
}
// mustParse is a helper that facilitates testing
func mustParse(config Config, dest ...interface{}) *Parser {
- if config.Exit == nil {
- config.Exit = os.Exit
- }
- if config.Out == nil {
- config.Out = os.Stdout
- }
-
p, err := NewParser(config, dest...)
if err != nil {
fmt.Fprintln(config.Out, err)
- config.Exit(-1)
+ config.Exit(2)
return nil
}
@@ -153,7 +148,7 @@ type Parser struct {
epilogue string
// the following field changes during processing of command line arguments
- lastCmd *command
+ subcommand []string
}
// Versioned is the interface that the destination struct should implement to
@@ -341,9 +336,8 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
spec.help = help
}
- // Look at the tag
- var isSubcommand bool // tracks whether this field is a subcommand
-
+ // process each comma-separated part of the tag
+ var isSubcommand bool
for _, key := range strings.Split(tag, ",") {
if key == "" {
continue
@@ -384,18 +378,24 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
}
case key == "subcommand":
// decide on a name for the subcommand
- cmdname := value
- if cmdname == "" {
- cmdname = strings.ToLower(field.Name)
+ var cmdnames []string
+ if value == "" {
+ cmdnames = []string{strings.ToLower(field.Name)}
+ } else {
+ cmdnames = strings.Split(value, "|")
+ }
+ for i := range cmdnames {
+ cmdnames[i] = strings.TrimSpace(cmdnames[i])
}
// parse the subcommand recursively
- subcmd, err := cmdFromStruct(cmdname, subdest, field.Type)
+ subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type)
if err != nil {
errs = append(errs, err.Error())
return false
}
+ subcmd.aliases = cmdnames[1:]
subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help")
@@ -407,6 +407,7 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
}
}
+ // placeholder is the string used in the help text like this: "--somearg PLACEHOLDER"
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
if hasPlaceholder {
spec.placeholder = placeholder
@@ -514,13 +515,13 @@ func (p *Parser) MustParse(args []string) {
err := p.Parse(args)
switch {
case err == ErrHelp:
- p.writeHelpForSubcommand(p.config.Out, p.lastCmd)
+ p.WriteHelpForSubcommand(p.config.Out, p.subcommand...)
p.config.Exit(0)
case err == ErrVersion:
fmt.Fprintln(p.config.Out, p.version)
p.config.Exit(0)
case err != nil:
- p.failWithSubcommand(err.Error(), p.lastCmd)
+ p.FailSubcommand(err.Error(), p.subcommand...)
}
}
@@ -577,7 +578,7 @@ func (p *Parser) process(args []string) error {
// union of specs for the chain of subcommands encountered so far
curCmd := p.cmd
- p.lastCmd = curCmd
+ p.subcommand = nil
// make a copy of the specs because we will add to this list each time we expand a subcommand
specs := make([]*spec, len(curCmd.specs))
@@ -648,7 +649,7 @@ func (p *Parser) process(args []string) error {
}
curCmd = subcmd
- p.lastCmd = curCmd
+ p.subcommand = append(p.subcommand, arg)
continue
}
@@ -842,6 +843,11 @@ func findSubcommand(cmds []*command, name string) *command {
if cmd.name == name {
return cmd
}
+ for _, alias := range cmd.aliases {
+ if alias == name {
+ return cmd
+ }
+ }
}
return nil
}
diff --git a/parse_test.go b/parse_test.go
index 06e7a76..5bc781c 100644
--- a/parse_test.go
+++ b/parse_test.go
@@ -692,6 +692,21 @@ func TestMustParse(t *testing.T) {
assert.NotNil(t, parser)
}
+func TestMustParseError(t *testing.T) {
+ var args struct {
+ Foo []string `default:""`
+ }
+ var exitCode int
+ var stdout bytes.Buffer
+ mustParseExit = func(code int) { exitCode = code }
+ mustParseOut = &stdout
+ os.Args = []string{"example"}
+ parser := MustParse(&args)
+ assert.Nil(t, parser)
+ assert.Equal(t, 2, exitCode)
+ assert.Contains(t, stdout.String(), "default values are not supported for slice or map fields")
+}
+
func TestEnvironmentVariable(t *testing.T) {
var args struct {
Foo string `arg:"env"`
@@ -883,7 +898,8 @@ func TestEnvironmentVariableInSubcommandIgnored(t *testing.T) {
require.NoError(t, err)
err = p.Parse([]string{"sub"})
- assert.NoError(t, err)
+ require.NoError(t, err)
+ require.NotNil(t, args.Sub)
assert.Equal(t, "", args.Sub.Foo)
}
@@ -905,7 +921,7 @@ func TestParserMustParse(t *testing.T) {
}{
{name: "help", args: struct{}{}, cmdLine: []string{"--help"}, code: 0, output: "display this help and exit"},
{name: "version", args: versioned{}, cmdLine: []string{"--version"}, code: 0, output: "example 3.2.1"},
- {name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: -1, output: ""},
+ {name: "invalid", args: struct{}{}, cmdLine: []string{"invalid"}, code: 2, output: ""},
}
for _, tt := range tests {
@@ -1555,7 +1571,7 @@ func TestMustParseInvalidParser(t *testing.T) {
}
parser := mustParse(Config{Out: &stdout, Exit: exit}, &args)
assert.Nil(t, parser)
- assert.Equal(t, -1, exitCode)
+ assert.Equal(t, 2, exitCode)
}
func TestMustParsePrintsHelp(t *testing.T) {
@@ -1731,7 +1747,16 @@ func TestSubcommandGlobalFlag_InCommand_Strict_Inner(t *testing.T) {
require.NoError(t, err)
err = p.Parse([]string{"sub", "-g"})
- assert.NoError(t, err)
+ require.NoError(t, err)
assert.False(t, args.Global)
+ require.NotNil(t, args.Sub)
assert.True(t, args.Sub.Guard)
}
+
+func TestExitFunctionAndOutStreamGetFilledIn(t *testing.T) {
+ var args struct{}
+ p, err := NewParser(Config{}, &args)
+ require.NoError(t, err)
+ assert.NotNil(t, p.config.Exit) // go prohibits function pointer comparison
+ assert.Equal(t, p.config.Out, os.Stdout)
+}
diff --git a/subcommand.go b/subcommand.go
index dff732c..da6ed11 100644
--- a/subcommand.go
+++ b/subcommand.go
@@ -1,5 +1,7 @@
package arg
+import "fmt"
+
// Subcommand returns the user struct for the subcommand selected by
// the command line arguments most recently processed by the parser.
// The return value is always a pointer to a struct. If no subcommand
@@ -7,31 +9,35 @@ package arg
// no command line arguments have been processed by this parser then it
// returns nil.
func (p *Parser) Subcommand() interface{} {
- if p.lastCmd == nil || p.lastCmd.parent == nil {
+ if len(p.subcommand) == 0 {
+ return nil
+ }
+ cmd, err := p.lookupCommand(p.subcommand...)
+ if err != nil {
return nil
}
- return p.val(p.lastCmd.dest).Interface()
+ return p.val(cmd.dest).Interface()
}
// SubcommandNames returns the sequence of subcommands specified by the
// user. If no subcommands were given then it returns an empty slice.
func (p *Parser) SubcommandNames() []string {
- if p.lastCmd == nil {
- return nil
- }
-
- // make a list of ancestor commands
- var ancestors []string
- cur := p.lastCmd
- for cur.parent != nil { // we want to exclude the root
- ancestors = append(ancestors, cur.name)
- cur = cur.parent
- }
+ return p.subcommand
+}
- // reverse the list
- out := make([]string, len(ancestors))
- for i := 0; i < len(ancestors); i++ {
- out[i] = ancestors[len(ancestors)-i-1]
+// lookupCommand finds a subcommand based on a sequence of subcommand names. The
+// first string should be a top-level subcommand, the next should be a child
+// subcommand of that subcommand, and so on. If no strings are given then the
+// root command is returned. If no such subcommand exists then an error is
+// returned.
+func (p *Parser) lookupCommand(path ...string) (*command, error) {
+ cmd := p.cmd
+ for _, name := range path {
+ found := findSubcommand(cmd.subcommands, name)
+ if found == nil {
+ return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
+ }
+ cmd = found
}
- return out
+ return cmd, nil
}
diff --git a/subcommand_test.go b/subcommand_test.go
index 2c61dd3..00efae0 100644
--- a/subcommand_test.go
+++ b/subcommand_test.go
@@ -83,6 +83,19 @@ func TestNamedSubcommand(t *testing.T) {
assert.Equal(t, []string{"ls"}, p.SubcommandNames())
}
+func TestSubcommandAliases(t *testing.T) {
+ type listCmd struct {
+ }
+ var args struct {
+ List *listCmd `arg:"subcommand:list|ls"`
+ }
+ p, err := pparse("ls", &args)
+ require.NoError(t, err)
+ assert.NotNil(t, args.List)
+ assert.Equal(t, args.List, p.Subcommand())
+ assert.Equal(t, []string{"ls"}, p.SubcommandNames())
+}
+
func TestEmptySubcommand(t *testing.T) {
type listCmd struct {
}
@@ -113,6 +126,23 @@ func TestTwoSubcommands(t *testing.T) {
assert.Equal(t, []string{"list"}, p.SubcommandNames())
}
+func TestTwoSubcommandsWithAliases(t *testing.T) {
+ type getCmd struct {
+ }
+ type listCmd struct {
+ }
+ var args struct {
+ Get *getCmd `arg:"subcommand:get|g"`
+ List *listCmd `arg:"subcommand:list|ls"`
+ }
+ p, err := pparse("ls", &args)
+ require.NoError(t, err)
+ assert.Nil(t, args.Get)
+ assert.NotNil(t, args.List)
+ assert.Equal(t, args.List, p.Subcommand())
+ assert.Equal(t, []string{"ls"}, p.SubcommandNames())
+}
+
func TestSubcommandsWithOptions(t *testing.T) {
type getCmd struct {
Name string
@@ -275,6 +305,60 @@ func TestNestedSubcommands(t *testing.T) {
}
}
+func TestNestedSubcommandsWithAliases(t *testing.T) {
+ type child struct{}
+ type parent struct {
+ Child *child `arg:"subcommand:child|ch"`
+ }
+ type grandparent struct {
+ Parent *parent `arg:"subcommand:parent|pa"`
+ }
+ type root struct {
+ Grandparent *grandparent `arg:"subcommand:grandparent|gp"`
+ }
+
+ {
+ var args root
+ p, err := pparse("gp parent child", &args)
+ require.NoError(t, err)
+ require.NotNil(t, args.Grandparent)
+ require.NotNil(t, args.Grandparent.Parent)
+ require.NotNil(t, args.Grandparent.Parent.Child)
+ assert.Equal(t, args.Grandparent.Parent.Child, p.Subcommand())
+ assert.Equal(t, []string{"gp", "parent", "child"}, p.SubcommandNames())
+ }
+
+ {
+ var args root
+ p, err := pparse("grandparent pa", &args)
+ require.NoError(t, err)
+ require.NotNil(t, args.Grandparent)
+ require.NotNil(t, args.Grandparent.Parent)
+ require.Nil(t, args.Grandparent.Parent.Child)
+ assert.Equal(t, args.Grandparent.Parent, p.Subcommand())
+ assert.Equal(t, []string{"grandparent", "pa"}, p.SubcommandNames())
+ }
+
+ {
+ var args root
+ p, err := pparse("grandparent", &args)
+ require.NoError(t, err)
+ require.NotNil(t, args.Grandparent)
+ require.Nil(t, args.Grandparent.Parent)
+ assert.Equal(t, args.Grandparent, p.Subcommand())
+ assert.Equal(t, []string{"grandparent"}, p.SubcommandNames())
+ }
+
+ {
+ var args root
+ p, err := pparse("", &args)
+ require.NoError(t, err)
+ require.Nil(t, args.Grandparent)
+ assert.Nil(t, p.Subcommand())
+ assert.Empty(t, p.SubcommandNames())
+ }
+}
+
func TestSubcommandsWithPositionals(t *testing.T) {
type listCmd struct {
Pattern string `arg:"positional"`
@@ -411,3 +495,14 @@ func TestValForNilStruct(t *testing.T) {
v := p.val(path{fields: []reflect.StructField{subField, subField}})
assert.False(t, v.IsValid())
}
+
+func TestSubcommandInvalidInternal(t *testing.T) {
+ // this situation should never arise in practice but still good to test for it
+ var cmd struct{}
+ p, err := NewParser(Config{}, &cmd)
+ require.NoError(t, err)
+
+ p.subcommand = []string{"should", "never", "happen"}
+ sub := p.Subcommand()
+ assert.Nil(t, sub)
+}
diff --git a/usage.go b/usage.go
index a9f9844..66a5be9 100644
--- a/usage.go
+++ b/usage.go
@@ -9,40 +9,31 @@ import (
// the width of the left column
const colWidth = 25
-// Fail prints usage information to stderr and exits with non-zero status
+// Fail prints usage information to p.Config.Out and exits with status code 2.
func (p *Parser) Fail(msg string) {
- p.failWithSubcommand(msg, p.cmd)
+ p.FailSubcommand(msg)
}
-// FailSubcommand prints usage information for a specified subcommand to stderr,
-// then exits with non-zero status. To write usage information for a top-level
+// FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
+// then exits with status code 2. To write usage information for a top-level
// subcommand, provide just the name of that subcommand. To write usage
// information for a subcommand that is nested under another subcommand, provide
// a sequence of subcommand names starting with the top-level subcommand and so
// on down the tree.
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
- cmd, err := p.lookupCommand(subcommand...)
+ err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
if err != nil {
return err
}
- p.failWithSubcommand(msg, cmd)
- return nil
-}
-// failWithSubcommand prints usage information for the given subcommand to stderr and exits with non-zero status
-func (p *Parser) failWithSubcommand(msg string, cmd *command) {
- p.writeUsageForSubcommand(p.config.Out, cmd)
fmt.Fprintln(p.config.Out, "error:", msg)
- p.config.Exit(-1)
+ p.config.Exit(2)
+ return nil
}
// WriteUsage writes usage information to the given writer
func (p *Parser) WriteUsage(w io.Writer) {
- cmd := p.cmd
- if p.lastCmd != nil {
- cmd = p.lastCmd
- }
- p.writeUsageForSubcommand(w, cmd)
+ p.WriteUsageForSubcommand(w, p.subcommand...)
}
// WriteUsageForSubcommand writes the usage information for a specified
@@ -55,40 +46,45 @@ func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) erro
if err != nil {
return err
}
- p.writeUsageForSubcommand(w, cmd)
- return nil
-}
-// writeUsageForSubcommand writes usage information for the given subcommand
-func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
var positionals, longOptions, shortOptions []*spec
+ var hasVersionOption bool
for _, spec := range cmd.specs {
switch {
case spec.positional:
positionals = append(positionals, spec)
case spec.long != "":
longOptions = append(longOptions, spec)
+ if spec.long == "version" {
+ hasVersionOption = true
+ }
case spec.short != "":
shortOptions = append(shortOptions, spec)
}
}
- if p.version != "" {
- fmt.Fprintln(w, p.version)
- }
-
// make a list of ancestor commands so that we print with full context
+ // also determine if any ancestor has a version option spec
var ancestors []string
ancestor := cmd
for ancestor != nil {
+ for _, spec := range ancestor.specs {
+ if spec.long == "version" {
+ hasVersionOption = true
+ }
+ }
ancestors = append(ancestors, ancestor.name)
ancestor = ancestor.parent
}
+ if !hasVersionOption && p.version != "" {
+ fmt.Fprintln(w, p.version)
+ }
+
// print the beginning of the usage string
- fmt.Fprint(w, "Usage:")
- for i := len(ancestors) - 1; i >= 0; i-- {
- fmt.Fprint(w, " "+ancestors[i])
+ fmt.Fprintf(w, "Usage: %s", p.cmd.name)
+ for _, s := range subcommand {
+ fmt.Fprint(w, " "+s)
}
// write the option component of the usage message
@@ -149,47 +145,66 @@ func (p *Parser) writeUsageForSubcommand(w io.Writer, cmd *command) {
}
fmt.Fprint(w, "\n")
+ return nil
}
-func printTwoCols(w io.Writer, left, help string, defaultVal string, envVal string) {
- lhs := " " + left
+// print prints a line like this:
+//
+// --option FOO A description of the option [default: 123]
+//
+// If the text on the left is longer than a certain threshold, the description is moved to the next line:
+//
+// --verylongoptionoption VERY_LONG_VARIABLE
+// A description of the option [default: 123]
+//
+// If multiple "extras" are provided then they are put inside a single set of square brackets:
+//
+// --option FOO A description of the option [default: 123, env: FOO]
+func print(w io.Writer, item, description string, bracketed ...string) {
+ lhs := " " + item
fmt.Fprint(w, lhs)
- if help != "" {
+ if description != "" {
if len(lhs)+2 < colWidth {
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
} else {
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
}
- fmt.Fprint(w, help)
+ fmt.Fprint(w, description)
}
- bracketsContent := []string{}
+ var brack string
+ for _, s := range bracketed {
+ if s != "" {
+ if brack != "" {
+ brack += ", "
+ }
+ brack += s
+ }
+ }
- if defaultVal != "" {
- bracketsContent = append(bracketsContent,
- fmt.Sprintf("default: %s", defaultVal),
- )
+ if brack != "" {
+ fmt.Fprintf(w, " [%s]", brack)
}
+ fmt.Fprint(w, "\n")
+}
- if envVal != "" {
- bracketsContent = append(bracketsContent,
- fmt.Sprintf("env: %s", envVal),
- )
+func withDefault(s string) string {
+ if s == "" {
+ return ""
}
+ return "default: " + s
+}
- if len(bracketsContent) > 0 {
- fmt.Fprintf(w, " [%s]", strings.Join(bracketsContent, ", "))
+func withEnv(env string) string {
+ if env == "" {
+ return ""
}
- fmt.Fprint(w, "\n")
+ return "env: " + env
}
// WriteHelp writes the usage string followed by the full help string for each option
func (p *Parser) WriteHelp(w io.Writer) {
- cmd := p.cmd
- if p.lastCmd != nil {
- cmd = p.lastCmd
- }
- p.writeHelpForSubcommand(w, cmd)
+ p.WriteHelpForSubcommand(w, p.subcommand...)
}
// WriteHelpForSubcommand writes the usage string followed by the full help
@@ -202,12 +217,7 @@ func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error
if err != nil {
return err
}
- p.writeHelpForSubcommand(w, cmd)
- return nil
-}
-// writeHelp writes the usage string for the given subcommand
-func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
var hasVersionOption bool
for _, spec := range cmd.specs {
@@ -216,6 +226,9 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
positionals = append(positionals, spec)
case spec.long != "":
longOptions = append(longOptions, spec)
+ if spec.long == "version" {
+ hasVersionOption = true
+ }
case spec.short != "":
shortOptions = append(shortOptions, spec)
case spec.short == "" && spec.long == "":
@@ -223,16 +236,31 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
}
}
+ // obtain a flattened list of options from all ancestors
+ // also determine if any ancestor has a version option spec
+ var globals []*spec
+ ancestor := cmd.parent
+ for ancestor != nil {
+ for _, spec := range ancestor.specs {
+ if spec.long == "version" {
+ hasVersionOption = true
+ break
+ }
+ }
+ globals = append(globals, ancestor.specs...)
+ ancestor = ancestor.parent
+ }
+
if p.description != "" {
fmt.Fprintln(w, p.description)
}
- p.writeUsageForSubcommand(w, cmd)
+ p.WriteUsageForSubcommand(w, subcommand...)
// write the list of positionals
if len(positionals) > 0 {
fmt.Fprint(w, "\nPositional arguments:\n")
for _, spec := range positionals {
- printTwoCols(w, spec.placeholder, spec.help, "", "")
+ print(w, spec.placeholder, spec.help)
}
}
@@ -244,28 +272,14 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
}
for _, spec := range longOptions {
p.printOption(w, spec)
- if spec.long == "version" {
- hasVersionOption = true
- }
}
}
- // obtain a flattened list of options from all ancestors
- var globals []*spec
- ancestor := cmd.parent
- for ancestor != nil {
- globals = append(globals, ancestor.specs...)
- ancestor = ancestor.parent
- }
-
// write the list of global options
if len(globals) > 0 {
fmt.Fprint(w, "\nGlobal options:\n")
for _, spec := range globals {
p.printOption(w, spec)
- if spec.long == "version" {
- hasVersionOption = true
- }
}
}
@@ -296,13 +310,15 @@ func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) {
if len(cmd.subcommands) > 0 {
fmt.Fprint(w, "\nCommands:\n")
for _, subcmd := range cmd.subcommands {
- printTwoCols(w, subcmd.name, subcmd.help, "", "")
+ names := append([]string{subcmd.name}, subcmd.aliases...)
+ print(w, strings.Join(names, ", "), subcmd.help)
}
}
if p.epilogue != "" {
fmt.Fprintln(w, "\n"+p.epilogue)
}
+ return nil
}
func (p *Parser) printOption(w io.Writer, spec *spec) {
@@ -314,7 +330,7 @@ func (p *Parser) printOption(w io.Writer, spec *spec) {
ways = append(ways, synopsis(spec, "-"+spec.short))
}
if len(ways) > 0 {
- printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultString, spec.env)
+ print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
}
}
@@ -330,33 +346,14 @@ func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
ways = append(ways, spec.help)
}
- printTwoCols(w, spec.env, strings.Join(ways, " "), spec.defaultString, "")
-}
-
-// lookupCommand finds a subcommand based on a sequence of subcommand names. The
-// first string should be a top-level subcommand, the next should be a child
-// subcommand of that subcommand, and so on. If no strings are given then the
-// root command is returned. If no such subcommand exists then an error is
-// returned.
-func (p *Parser) lookupCommand(path ...string) (*command, error) {
- cmd := p.cmd
- for _, name := range path {
- var found *command
- for _, child := range cmd.subcommands {
- if child.name == name {
- found = child
- }
- }
- if found == nil {
- return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
- }
- cmd = found
- }
- return cmd, nil
+ print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
}
func synopsis(spec *spec, form string) string {
- if spec.cardinality == zero {
+ // if the user omits the placeholder tag then we pick one automatically,
+ // but if the user explicitly specifies an empty placeholder then we
+ // leave out the placeholder in the help message
+ if spec.cardinality == zero || spec.placeholder == "" {
return form
}
return form + " " + spec.placeholder
diff --git a/usage_test.go b/usage_test.go
index 1a64ad4..a958abb 100644
--- a/usage_test.go
+++ b/usage_test.go
@@ -260,6 +260,233 @@ Options:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
+func TestUsageWithUserDefinedVersionFlag(t *testing.T) {
+ expectedUsage := "Usage: example [--version]"
+
+ expectedHelp := `
+Usage: example [--version]
+
+Options:
+ --version this is a user-defined version flag
+ --help, -h display this help and exit
+`
+
+ var args struct {
+ ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
+ }
+
+ os.Args[0] = "example"
+ p, err := NewParser(Config{}, &args)
+ require.NoError(t, err)
+
+ var help bytes.Buffer
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ var usage bytes.Buffer
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+}
+
+func TestUsageWithVersionAndUserDefinedVersionFlag(t *testing.T) {
+ expectedUsage := "Usage: example [--version]"
+
+ expectedHelp := `
+Usage: example [--version]
+
+Options:
+ --version this is a user-defined version flag
+ --help, -h display this help and exit
+`
+
+ var args struct {
+ versioned
+ ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
+ }
+
+ os.Args[0] = "example"
+ p, err := NewParser(Config{}, &args)
+ require.NoError(t, err)
+
+ var help bytes.Buffer
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ var usage bytes.Buffer
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+}
+
+type subcommand struct {
+ Number int `arg:"-n,--number" help:"compute something on the given number"`
+}
+
+func TestUsageWithVersionAndSubcommand(t *testing.T) {
+ expectedUsage := "example 3.2.1\nUsage: example <command> [<args>]"
+
+ expectedHelp := `
+example 3.2.1
+Usage: example <command> [<args>]
+
+Options:
+ --help, -h display this help and exit
+ --version display version and exit
+
+Commands:
+ cmd
+`
+
+ var args struct {
+ versioned
+ Cmd *subcommand `arg:"subcommand"`
+ }
+
+ os.Args[0] = "example"
+ p, err := NewParser(Config{}, &args)
+ require.NoError(t, err)
+
+ var help bytes.Buffer
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ var usage bytes.Buffer
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+
+ expectedUsage = "example 3.2.1\nUsage: example cmd [--number NUMBER]"
+
+ expectedHelp = `
+example 3.2.1
+Usage: example cmd [--number NUMBER]
+
+Options:
+ --number NUMBER, -n NUMBER
+ compute something on the given number
+ --help, -h display this help and exit
+ --version display version and exit
+`
+ _ = p.Parse([]string{"cmd"})
+
+ help = bytes.Buffer{}
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ usage = bytes.Buffer{}
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+}
+
+func TestUsageWithUserDefinedVersionFlagAndSubcommand(t *testing.T) {
+ expectedUsage := "Usage: example [--version] <command> [<args>]"
+
+ expectedHelp := `
+Usage: example [--version] <command> [<args>]
+
+Options:
+ --version this is a user-defined version flag
+ --help, -h display this help and exit
+
+Commands:
+ cmd
+`
+
+ var args struct {
+ Cmd *subcommand `arg:"subcommand"`
+ ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
+ }
+
+ os.Args[0] = "example"
+ p, err := NewParser(Config{}, &args)
+ require.NoError(t, err)
+
+ var help bytes.Buffer
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ var usage bytes.Buffer
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+
+ expectedUsage = "Usage: example cmd [--number NUMBER]"
+
+ expectedHelp = `
+Usage: example cmd [--number NUMBER]
+
+Options:
+ --number NUMBER, -n NUMBER
+ compute something on the given number
+
+Global options:
+ --version this is a user-defined version flag
+ --help, -h display this help and exit
+`
+ _ = p.Parse([]string{"cmd"})
+
+ help = bytes.Buffer{}
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ usage = bytes.Buffer{}
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+}
+
+func TestUsageWithVersionAndUserDefinedVersionFlagAndSubcommand(t *testing.T) {
+ expectedUsage := "Usage: example [--version] <command> [<args>]"
+
+ expectedHelp := `
+Usage: example [--version] <command> [<args>]
+
+Options:
+ --version this is a user-defined version flag
+ --help, -h display this help and exit
+
+Commands:
+ cmd
+`
+
+ var args struct {
+ versioned
+ Cmd *subcommand `arg:"subcommand"`
+ ShowVersion bool `arg:"--version" help:"this is a user-defined version flag"`
+ }
+
+ os.Args[0] = "example"
+ p, err := NewParser(Config{}, &args)
+ require.NoError(t, err)
+
+ var help bytes.Buffer
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ var usage bytes.Buffer
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+
+ expectedUsage = "Usage: example cmd [--number NUMBER]"
+
+ expectedHelp = `
+Usage: example cmd [--number NUMBER]
+
+Options:
+ --number NUMBER, -n NUMBER
+ compute something on the given number
+
+Global options:
+ --version this is a user-defined version flag
+ --help, -h display this help and exit
+`
+ _ = p.Parse([]string{"cmd"})
+
+ help = bytes.Buffer{}
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ usage = bytes.Buffer{}
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+}
+
type described struct{}
// Described returns the description for this program
@@ -415,6 +642,50 @@ Options:
assert.Equal(t, expectedUsage, usage.String())
}
+func TestUsageWithSubcommands(t *testing.T) {
+ expectedUsage := "Usage: example child [--values VALUES]"
+
+ expectedHelp := `
+Usage: example child [--values VALUES]
+
+Options:
+ --values VALUES Values
+
+Global options:
+ --verbose, -v verbosity level
+ --help, -h display this help and exit
+`
+
+ var args struct {
+ Verbose bool `arg:"-v" help:"verbosity level"`
+ Child *struct {
+ Values []float64 `help:"Values"`
+ } `arg:"subcommand:child"`
+ }
+
+ os.Args[0] = "example"
+ p, err := NewParser(Config{}, &args)
+ require.NoError(t, err)
+
+ _ = p.Parse([]string{"child"})
+
+ var help bytes.Buffer
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ var help2 bytes.Buffer
+ p.WriteHelpForSubcommand(&help2, "child")
+ assert.Equal(t, expectedHelp[1:], help2.String())
+
+ var usage bytes.Buffer
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+
+ var usage2 bytes.Buffer
+ p.WriteUsageForSubcommand(&usage2, "child")
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String()))
+}
+
func TestUsageWithNestedSubcommands(t *testing.T) {
expectedUsage := "Usage: example child nested [--enable] OUTPUT"
@@ -450,6 +721,8 @@ Global options:
_ = p.Parse([]string{"child", "nested", "value"})
+ assert.Equal(t, []string{"child", "nested"}, p.SubcommandNames())
+
var help bytes.Buffer
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
@@ -471,7 +744,7 @@ func TestNonexistentSubcommand(t *testing.T) {
var args struct {
sub *struct{} `arg:"subcommand"`
}
- p, err := NewParser(Config{}, &args)
+ p, err := NewParser(Config{Exit: func(int) {}}, &args)
require.NoError(t, err)
var b bytes.Buffer
@@ -522,6 +795,35 @@ Options:
assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
}
+func TestUsageWithEmptyPlaceholder(t *testing.T) {
+ expectedUsage := "Usage: example [-a] [--b] [--c]"
+
+ expectedHelp := `
+Usage: example [-a] [--b] [--c]
+
+Options:
+ -a some help for a
+ --b some help for b
+ --c, -c some help for c
+ --help, -h display this help and exit
+`
+ var args struct {
+ ShortOnly string `arg:"-a,--" placeholder:"" help:"some help for a"`
+ LongOnly string `arg:"--b" placeholder:"" help:"some help for b"`
+ Both string `arg:"-c,--c" placeholder:"" help:"some help for c"`
+ }
+ p, err := NewParser(Config{Program: "example"}, &args)
+ require.NoError(t, err)
+
+ var help bytes.Buffer
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+
+ var usage bytes.Buffer
+ p.WriteUsage(&usage)
+ assert.Equal(t, expectedUsage, strings.TrimSpace(usage.String()))
+}
+
func TestUsageWithShortFirst(t *testing.T) {
expectedUsage := "Usage: example [-c CAT] [--dog DOG]"
@@ -630,7 +932,7 @@ error: something went wrong
p.Fail("something went wrong")
assert.Equal(t, expectedStdout[1:], stdout.String())
- assert.Equal(t, -1, exitCode)
+ assert.Equal(t, 2, exitCode)
}
func TestFailSubcommand(t *testing.T) {
@@ -653,7 +955,7 @@ error: something went wrong
require.NoError(t, err)
assert.Equal(t, expectedStdout[1:], stdout.String())
- assert.Equal(t, -1, exitCode)
+ assert.Equal(t, 2, exitCode)
}
type lengthOf struct {
@@ -687,3 +989,29 @@ Options:
p.WriteHelp(&help)
assert.Equal(t, expectedHelp[1:], help.String())
}
+
+func TestHelpShowsSubcommandAliases(t *testing.T) {
+ expectedHelp := `
+Usage: example <command> [<args>]
+
+Options:
+ --help, -h display this help and exit
+
+Commands:
+ remove, rm, r remove something from somewhere
+ simple do something simple
+ halt, stop stop now
+`
+
+ var args struct {
+ Remove *struct{} `arg:"subcommand:remove|rm|r" help:"remove something from somewhere"`
+ Simple *struct{} `arg:"subcommand" help:"do something simple"`
+ Stop *struct{} `arg:"subcommand:halt|stop" help:"stop now"`
+ }
+ p, err := NewParser(Config{Program: "example"}, &args)
+ require.NoError(t, err)
+
+ var help bytes.Buffer
+ p.WriteHelp(&help)
+ assert.Equal(t, expectedHelp[1:], help.String())
+}