print help and usage at subcommand level if necessary

This commit is contained in:
Alex Flint 2019-05-03 15:49:44 -07:00
parent 15bf383f1d
commit b83047068d
3 changed files with 154 additions and 27 deletions

View File

@ -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
}

View File

@ -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, "-")

View File

@ -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)
}
}