summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--example_test.go89
-rw-r--r--parse.go44
-rw-r--r--usage.go48
3 files changed, 154 insertions, 27 deletions
diff --git a/example_test.go b/example_test.go
index eb8d315..b58085f 100644
--- a/example_test.go
+++ b/example_test.go
@@ -104,7 +104,7 @@ func Example_multipleMixed() {
}
// This example shows the usage string generated by go-arg
-func Example_usageString() {
+func Example_helpText() {
// These are the args you would pass in on the command line
os.Args = split("./example --help")
@@ -136,8 +136,8 @@ func Example_usageString() {
// --help, -h display this help and exit
}
-// This example shows the usage string generated by go-arg
-func Example_usageStringWithSubcommand() {
+// This example shows the usage string generated by go-arg when using subcommands
+func Example_helpTextWithSubcommand() {
// These are the args you would pass in on the command line
os.Args = split("./example --help")
@@ -172,3 +172,86 @@ func Example_usageStringWithSubcommand() {
// get fetch an item and print it
// list list available items
}
+
+// This example shows the usage string generated by go-arg when using subcommands
+func Example_helpTextForSubcommand() {
+ // These are the args you would pass in on the command line
+ os.Args = split("./example get --help")
+
+ type getCmd struct {
+ Item string `arg:"positional" help:"item to fetch"`
+ }
+
+ type listCmd struct {
+ Format string `help:"output format"`
+ Limit int
+ }
+
+ var args struct {
+ Verbose bool
+ Get *getCmd `arg:"subcommand" help:"fetch an item and print it"`
+ List *listCmd `arg:"subcommand" help:"list available items"`
+ }
+
+ // This is only necessary when running inside golang's runnable example harness
+ osExit = func(int) {}
+
+ MustParse(&args)
+
+ // output:
+ // Usage: example get ITEM
+ //
+ // Positional arguments:
+ // ITEM item to fetch
+ //
+ // Options:
+ // --help, -h display this help and exit
+}
+
+// This example shows the error string generated by go-arg when an invalid option is provided
+func Example_errorText() {
+ // These are the args you would pass in on the command line
+ os.Args = split("./example --optimize INVALID")
+
+ var args struct {
+ Input string `arg:"positional"`
+ Output []string `arg:"positional"`
+ Verbose bool `arg:"-v" help:"verbosity level"`
+ Dataset string `help:"dataset to use"`
+ Optimize int `arg:"-O,help:optimization level"`
+ }
+
+ // This is only necessary when running inside golang's runnable example harness
+ osExit = func(int) {}
+ stderr = os.Stdout
+
+ MustParse(&args)
+
+ // output:
+ // Usage: example [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] INPUT [OUTPUT [OUTPUT ...]]
+ // error: error processing --optimize: strconv.ParseInt: parsing "INVALID": invalid syntax
+}
+
+// This example shows the error string generated by go-arg when an invalid option is provided
+func Example_errorTextForSubcommand() {
+ // These are the args you would pass in on the command line
+ os.Args = split("./example get --count INVALID")
+
+ type getCmd struct {
+ Count int
+ }
+
+ var args struct {
+ Get *getCmd `arg:"subcommand"`
+ }
+
+ // This is only necessary when running inside golang's runnable example harness
+ osExit = func(int) {}
+ stderr = os.Stdout
+
+ MustParse(&args)
+
+ // output:
+ // Usage: example get [--count COUNT]
+ // error: error processing --count: strconv.ParseInt: parsing "INVALID": invalid syntax
+}
diff --git a/parse.go b/parse.go
index 3a48880..e0de705 100644
--- a/parse.go
+++ b/parse.go
@@ -63,6 +63,7 @@ type command struct {
dest path
specs []*spec
subcommands []*command
+ parent *command
}
// ErrHelp indicates that -h or --help were provided
@@ -77,18 +78,19 @@ func MustParse(dest ...interface{}) *Parser {
if err != nil {
fmt.Println(err)
osExit(-1)
+ return nil // just in case osExit was monkey-patched
}
err = p.Parse(flags())
switch {
case err == ErrHelp:
- p.WriteHelp(os.Stdout)
+ p.writeHelpForCommand(os.Stdout, p.lastCmd)
osExit(0)
case err == ErrVersion:
fmt.Println(p.version)
osExit(0)
case err != nil:
- p.Fail(err.Error())
+ p.failWithCommand(err.Error(), p.lastCmd)
}
return p
@@ -123,6 +125,9 @@ type Parser struct {
config Config
version string
description string
+
+ // the following fields change curing processing of command line arguments
+ lastCmd *command
}
// Versioned is the interface that the destination struct should implement to
@@ -297,6 +302,7 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
return false
}
+ subcmd.parent = &cmd
subcmd.help = field.Tag.Get("help")
cmd.subcommands = append(cmd.subcommands, subcmd)
@@ -349,21 +355,19 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) {
// Parse processes the given command line option, storing the results in the field
// of the structs from which NewParser was constructed
func (p *Parser) Parse(args []string) error {
- // If -h or --help were specified then print usage
- for _, arg := range args {
- if arg == "-h" || arg == "--help" {
- return ErrHelp
- }
- if arg == "--version" {
- return ErrVersion
- }
- if arg == "--" {
- break
+ err := p.process(args)
+ if err != nil {
+ // If -h or --help were specified then make sure help text supercedes other errors
+ for _, arg := range args {
+ if arg == "-h" || arg == "--help" {
+ return ErrHelp
+ }
+ if arg == "--" {
+ break
+ }
}
}
-
- // Process all command line arguments
- return p.process(args)
+ return err
}
// process environment vars for the given arguments
@@ -415,6 +419,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
// 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))
@@ -465,9 +470,18 @@ func (p *Parser) process(args []string) error {
}
curCmd = subcmd
+ p.lastCmd = curCmd
continue
}
+ // check for special --help and --version flags
+ switch arg {
+ case "-h", "--help":
+ return ErrHelp
+ case "--version":
+ return ErrVersion
+ }
+
// check for an equals sign, as in "--foo=bar"
var value string
opt := strings.TrimLeft(arg, "-")
diff --git a/usage.go b/usage.go
index 42c564b..69e4e62 100644
--- a/usage.go
+++ b/usage.go
@@ -12,17 +12,30 @@ import (
// the width of the left column
const colWidth = 25
+// to allow monkey patching in tests
+var stderr = os.Stderr
+
// Fail prints usage information to stderr and exits with non-zero status
func (p *Parser) Fail(msg string) {
- p.WriteUsage(os.Stderr)
- fmt.Fprintln(os.Stderr, "error:", msg)
- os.Exit(-1)
+ p.failWithCommand(msg, p.cmd)
+}
+
+// failWithCommand prints usage information for the given subcommand to stderr and exits with non-zero status
+func (p *Parser) failWithCommand(msg string, cmd *command) {
+ p.writeUsageForCommand(stderr, cmd)
+ fmt.Fprintln(stderr, "error:", msg)
+ osExit(-1)
}
// WriteUsage writes usage information to the given writer
func (p *Parser) WriteUsage(w io.Writer) {
+ p.writeUsageForCommand(w, p.cmd)
+}
+
+// writeUsageForCommand writes usage information for the given subcommand
+func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
var positionals, options []*spec
- for _, spec := range p.cmd.specs {
+ for _, spec := range cmd.specs {
if spec.positional {
positionals = append(positionals, spec)
} else {
@@ -34,7 +47,19 @@ func (p *Parser) WriteUsage(w io.Writer) {
fmt.Fprintln(w, p.version)
}
- fmt.Fprintf(w, "Usage: %s", p.cmd.name)
+ // make a list of ancestor commands so that we print with full context
+ var ancestors []string
+ ancestor := cmd
+ for ancestor != nil {
+ ancestors = append(ancestors, ancestor.name)
+ ancestor = ancestor.parent
+ }
+
+ // print the beginning of the usage string
+ fmt.Fprintf(w, "Usage:")
+ for i := len(ancestors) - 1; i >= 0; i-- {
+ fmt.Fprint(w, " "+ancestors[i])
+ }
// write the option component of the usage message
for _, spec := range options {
@@ -88,8 +113,13 @@ func printTwoCols(w io.Writer, left, help string, defaultVal *string) {
// WriteHelp writes the usage string followed by the full help string for each option
func (p *Parser) WriteHelp(w io.Writer) {
+ p.writeHelpForCommand(w, p.cmd)
+}
+
+// writeHelp writes the usage string for the given subcommand
+func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) {
var positionals, options []*spec
- for _, spec := range p.cmd.specs {
+ for _, spec := range cmd.specs {
if spec.positional {
positionals = append(positionals, spec)
} else {
@@ -100,7 +130,7 @@ func (p *Parser) WriteHelp(w io.Writer) {
if p.description != "" {
fmt.Fprintln(w, p.description)
}
- p.WriteUsage(w)
+ p.writeUsageForCommand(w, cmd)
// write the list of positionals
if len(positionals) > 0 {
@@ -132,9 +162,9 @@ func (p *Parser) WriteHelp(w io.Writer) {
}
// write the list of subcommands
- if len(p.cmd.subcommands) > 0 {
+ if len(cmd.subcommands) > 0 {
fmt.Fprint(w, "\nCommands:\n")
- for _, subcmd := range p.cmd.subcommands {
+ for _, subcmd := range cmd.subcommands {
printTwoCols(w, subcmd.name, subcmd.help, nil)
}
}