diff options
| -rw-r--r-- | README.md | 22 | ||||
| -rw-r--r-- | example_test.go | 7 | ||||
| -rw-r--r-- | parse.go | 52 | ||||
| -rw-r--r-- | parse_test.go | 33 | ||||
| -rw-r--r-- | subcommand.go | 42 | ||||
| -rw-r--r-- | subcommand_test.go | 95 | ||||
| -rw-r--r-- | usage.go | 191 | ||||
| -rw-r--r-- | usage_test.go | 334 |
8 files changed, 630 insertions, 146 deletions
@@ -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) @@ -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) +} @@ -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()) +} |
