Skip to content

Commit

Permalink
Merge pull request #156 from alexflint/usage-for-subcommands
Browse files Browse the repository at this point in the history
add FailSubcommand, WriteUsageForSubcommand, WriteHelpForSubcommand
  • Loading branch information
alexflint authored Sep 18, 2021
2 parents 66cb696 + f2f8764 commit a4afd6a
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 13 deletions.
95 changes: 94 additions & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,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")

Expand Down Expand Up @@ -290,6 +290,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
Expand Down
4 changes: 2 additions & 2 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 75 additions & 10 deletions usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}

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

// writeUsageForCommand writes usage information for the given subcommand
func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) {
// 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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -252,6 +295,28 @@ 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 {
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
Expand Down
68 changes: 68 additions & 0 deletions usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

0 comments on commit a4afd6a

Please sign in to comment.