diff --git a/example_test.go b/example_test.go index 2e9b875..2b7ce00 100644 --- a/example_test.go +++ b/example_test.go @@ -250,7 +250,7 @@ func Example_helpTextWithSubcommand() { } // This example shows the usage string generated by go-arg when using subcommands -func Example_helpTextForSubcommand() { +func Example_helpTextWhenUsingSubcommand() { // These are the args you would pass in on the command line os.Args = split("./example get --help") @@ -286,6 +286,99 @@ func Example_helpTextForSubcommand() { // --help, -h display this help and exit } +// This example shows how to print help for an explicit subcommand +func Example_writeHelpForSubcommand() { + // 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) {} + stdout = os.Stdout + + p, err := NewParser(Config{}, &args) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + err = p.WriteHelpForSubcommand(os.Stdout, "list") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // output: + // Usage: example list [--format FORMAT] [--limit LIMIT] + // + // Options: + // --format FORMAT output format + // --limit LIMIT + // + // Global options: + // --verbose + // --help, -h display this help and exit +} + +// This example shows how to print help for a subcommand that is nested several levels deep +func Example_writeHelpForSubcommandNested() { + // These are the args you would pass in on the command line + os.Args = split("./example get --help") + + type mostNestedCmd struct { + Item string + } + + type nestedCmd struct { + MostNested *mostNestedCmd `arg:"subcommand"` + } + + type topLevelCmd struct { + Nested *nestedCmd `arg:"subcommand"` + } + + var args struct { + TopLevel *topLevelCmd `arg:"subcommand"` + } + + // This is only necessary when running inside golang's runnable example harness + osExit = func(int) {} + stdout = os.Stdout + + p, err := NewParser(Config{}, &args) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + err = p.WriteHelpForSubcommand(os.Stdout, "toplevel", "nested", "mostnested") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + // output: + // Usage: example toplevel nested mostnested [--item ITEM] + // + // Options: + // --item ITEM + // --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 diff --git a/parse.go b/parse.go index 13e8195..40adbe1 100644 --- a/parse.go +++ b/parse.go @@ -85,13 +85,13 @@ func MustParse(dest ...interface{}) *Parser { err = p.Parse(flags()) switch { case err == ErrHelp: - p.writeHelpForCommand(stdout, p.lastCmd) + p.writeHelpForSubcommand(stdout, p.lastCmd) osExit(0) case err == ErrVersion: fmt.Fprintln(stdout, p.version) osExit(0) case err != nil: - p.failWithCommand(err.Error(), p.lastCmd) + p.failWithSubcommand(err.Error(), p.lastCmd) } return p diff --git a/usage.go b/usage.go index c121c45..e27ed5b 100644 --- a/usage.go +++ b/usage.go @@ -19,12 +19,27 @@ var ( // Fail prints usage information to stderr and exits with non-zero status func (p *Parser) Fail(msg string) { - p.failWithCommand(msg, p.cmd) + p.failWithSubcommand(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) +// FailSubcommand prints usage information for a specified subcommand to stderr, +// then exits with non-zero status. 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...) + 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(stderr, cmd) fmt.Fprintln(stderr, "error:", msg) osExit(-1) } @@ -35,11 +50,25 @@ func (p *Parser) WriteUsage(w io.Writer) { if p.lastCmd != nil { cmd = p.lastCmd } - p.writeUsageForCommand(w, cmd) + p.writeUsageForSubcommand(w, cmd) } -// writeUsageForCommand writes usage information for the given subcommand -func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) { +// WriteUsageForSubcommand writes the usage information for a specified +// subcommand. 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) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error { + cmd, err := p.lookupCommand(subcommand...) + 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 for _, spec := range cmd.specs { switch { @@ -158,11 +187,25 @@ func (p *Parser) WriteHelp(w io.Writer) { if p.lastCmd != nil { cmd = p.lastCmd } - p.writeHelpForCommand(w, cmd) + p.writeHelpForSubcommand(w, cmd) +} + +// WriteHelpForSubcommand writes the usage string followed by the full help +// string for a specified subcommand. To write help for a top-level subcommand, +// provide just the name of that subcommand. To write help 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) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error { + cmd, err := p.lookupCommand(subcommand...) + if err != nil { + return err + } + p.writeHelpForSubcommand(w, cmd) + return nil } // writeHelp writes the usage string for the given subcommand -func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) { +func (p *Parser) writeHelpForSubcommand(w io.Writer, cmd *command) { var positionals, longOptions, shortOptions []*spec for _, spec := range cmd.specs { switch { @@ -178,7 +221,7 @@ func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) { if p.description != "" { fmt.Fprintln(w, p.description) } - p.writeUsageForCommand(w, cmd) + p.writeUsageForSubcommand(w, cmd) // write the list of positionals if len(positionals) > 0 { @@ -252,6 +295,31 @@ func (p *Parser) printOption(w io.Writer, spec *spec) { } } +// 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 { + if cmd.name == "" { + return nil, fmt.Errorf("%q is not a top-level subcommand", name) + } + return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name) + } + cmd = found + } + return cmd, nil +} + func synopsis(spec *spec, form string) string { if spec.cardinality == zero { return form diff --git a/usage_test.go b/usage_test.go index 1b6c475..f790e34 100644 --- a/usage_test.go +++ b/usage_test.go @@ -352,9 +352,45 @@ Global options: p.WriteHelp(&help) assert.Equal(t, expectedHelp[1:], help.String()) + var help2 bytes.Buffer + p.WriteHelpForSubcommand(&help2, "child", "nested") + 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", "nested") + assert.Equal(t, expectedUsage, strings.TrimSpace(usage2.String())) +} + +func TestNonexistentSubcommand(t *testing.T) { + var args struct { + sub *struct{} `arg:"subcommand"` + } + p, err := NewParser(Config{}, &args) + require.NoError(t, err) + + var b bytes.Buffer + + err = p.WriteUsageForSubcommand(&b, "does_not_exist") + assert.Error(t, err) + + err = p.WriteHelpForSubcommand(&b, "does_not_exist") + assert.Error(t, err) + + err = p.FailSubcommand("something went wrong", "does_not_exist") + assert.Error(t, err) + + err = p.WriteUsageForSubcommand(&b, "sub", "does_not_exist") + assert.Error(t, err) + + err = p.WriteHelpForSubcommand(&b, "sub", "does_not_exist") + assert.Error(t, err) + + err = p.FailSubcommand("something went wrong", "sub", "does_not_exist") + assert.Error(t, err) } func TestUsageWithoutLongNames(t *testing.T) { @@ -468,3 +504,35 @@ error: something went wrong assert.Equal(t, expectedStdout[1:], b.String()) assert.Equal(t, -1, exitCode) } + +func TestFailSubcommand(t *testing.T) { + originalStderr := stderr + originalExit := osExit + defer func() { + stderr = originalStderr + osExit = originalExit + }() + + var b bytes.Buffer + stderr = &b + + var exitCode int + osExit = func(code int) { exitCode = code } + + expectedStdout := ` +Usage: example sub +error: something went wrong +` + + var args struct { + Sub *struct{} `arg:"subcommand"` + } + p, err := NewParser(Config{Program: "example"}, &args) + require.NoError(t, err) + + err = p.FailSubcommand("something went wrong", "sub") + require.NoError(t, err) + + assert.Equal(t, expectedStdout[1:], b.String()) + assert.Equal(t, -1, exitCode) +}