diff --git a/README.md b/README.md index a9555b9..da69469 100644 --- a/README.md +++ b/README.md @@ -244,21 +244,23 @@ someprogram 4.3.0 ```go var args struct { - Short string `arg:"-s"` - Long string `arg:"--custom-long-option"` - ShortAndLong string `arg:"-x,--my-option"` + Short string `arg:"-s"` + Long string `arg:"--custom-long-option"` + ShortAndLong string `arg:"-x,--my-option"` + OnlyShort string `arg:"-o,--"` } arg.MustParse(&args) ``` ```shell $ ./example --help -Usage: [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION] +Usage: example [-o ONLYSHORT] [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION] Options: --short SHORT, -s SHORT --custom-long-option CUSTOM-LONG-OPTION --my-option MY-OPTION, -x MY-OPTION + -o ONLYSHORT --help, -h display this help and exit ``` diff --git a/parse.go b/parse.go index 0c65397..b7d159d 100644 --- a/parse.go +++ b/parse.go @@ -47,9 +47,9 @@ func (p path) Child(f reflect.StructField) path { // spec represents a command line option type spec struct { dest path - typ reflect.Type - long string - short string + field reflect.StructField // the struct field from which this option was created + long string // the --long form for this option, or empty if none + short string // the -s short form for this option, or empty if none multiple bool required bool positional bool @@ -275,9 +275,9 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) { // duplicate the entire path to avoid slice overwrites subdest := dest.Child(field) spec := spec{ - dest: subdest, - long: strings.ToLower(field.Name), - typ: field.Type, + dest: subdest, + field: field, + long: strings.ToLower(field.Name), } help, exists := field.Tag.Lookup("help") @@ -363,8 +363,10 @@ func cmdFromStruct(name string, dest path, t reflect.Type) (*command, error) { placeholder, hasPlaceholder := field.Tag.Lookup("placeholder") if hasPlaceholder { spec.placeholder = placeholder - } else { + } else if spec.long != "" { spec.placeholder = strings.ToUpper(spec.long) + } else { + spec.placeholder = strings.ToUpper(spec.field.Name) } // Check whether this field is supported. It's good to do this here rather than @@ -592,7 +594,7 @@ func (p *Parser) process(args []string) error { if i+1 == len(args) { return fmt.Errorf("missing value for %s", arg) } - if !nextIsNumeric(spec.typ, args[i+1]) && isFlag(args[i+1]) { + if !nextIsNumeric(spec.field.Type, args[i+1]) && isFlag(args[i+1]) { return fmt.Errorf("missing value for %s", arg) } value = args[i+1] @@ -617,13 +619,13 @@ func (p *Parser) process(args []string) error { if spec.multiple { err := setSlice(p.val(spec.dest), positionals, true) if err != nil { - return fmt.Errorf("error processing %s: %v", spec.long, err) + return fmt.Errorf("error processing %s: %v", spec.field.Name, err) } positionals = nil } else { err := scalar.ParseValue(p.val(spec.dest), positionals[0]) if err != nil { - return fmt.Errorf("error processing %s: %v", spec.long, err) + return fmt.Errorf("error processing %s: %v", spec.field.Name, err) } positionals = positionals[1:] } @@ -638,8 +640,8 @@ func (p *Parser) process(args []string) error { continue } - name := spec.long - if !spec.positional { + name := strings.ToLower(spec.field.Name) + if spec.long != "" && !spec.positional { name = "--" + spec.long } diff --git a/parse_test.go b/parse_test.go index a0334c7..ce3068e 100644 --- a/parse_test.go +++ b/parse_test.go @@ -231,6 +231,18 @@ func TestPlaceholder(t *testing.T) { assert.NoError(t, err) } +func TestNoLongName(t *testing.T) { + var args struct { + ShortOnly string `arg:"-s,--"` + EnvOnly string `arg:"--,env"` + } + setenv(t, "ENVONLY", "TestVal") + err := parse("-s TestVal2", &args) + assert.NoError(t, err) + assert.Equal(t, "TestVal", args.EnvOnly) + assert.Equal(t, "TestVal2", args.ShortOnly) +} + func TestCaseSensitive(t *testing.T) { var args struct { Lower bool `arg:"-v"` diff --git a/usage.go b/usage.go index 776ac03..cbbb021 100644 --- a/usage.go +++ b/usage.go @@ -36,12 +36,15 @@ func (p *Parser) WriteUsage(w io.Writer) { // writeUsageForCommand writes usage information for the given subcommand func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) { - var positionals, options []*spec + var positionals, longOptions, shortOptions []*spec for _, spec := range cmd.specs { - if spec.positional { + switch { + case spec.positional: positionals = append(positionals, spec) - } else { - options = append(options, spec) + case spec.long != "": + longOptions = append(longOptions, spec) + case spec.short != "": + shortOptions = append(shortOptions, spec) } } @@ -64,7 +67,19 @@ func (p *Parser) writeUsageForCommand(w io.Writer, cmd *command) { } // write the option component of the usage message - for _, spec := range options { + for _, spec := range shortOptions { + // prefix with a space + fmt.Fprint(w, " ") + if !spec.required { + fmt.Fprint(w, "[") + } + fmt.Fprint(w, synopsis(spec, "-"+spec.short)) + if !spec.required { + fmt.Fprint(w, "]") + } + } + + for _, spec := range longOptions { // prefix with a space fmt.Fprint(w, " ") if !spec.required { @@ -144,12 +159,15 @@ func (p *Parser) WriteHelp(w io.Writer) { // writeHelp writes the usage string for the given subcommand func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) { - var positionals, options []*spec + var positionals, longOptions, shortOptions []*spec for _, spec := range cmd.specs { - if spec.positional { + switch { + case spec.positional: positionals = append(positionals, spec) - } else { - options = append(options, spec) + case spec.long != "": + longOptions = append(longOptions, spec) + case spec.short != "": + shortOptions = append(shortOptions, spec) } } @@ -166,10 +184,13 @@ func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) { } } - // write the list of options - if len(options) > 0 || cmd.parent == nil { + // write the list of options with the short-only ones first to match the usage string + if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil { fmt.Fprint(w, "\nOptions:\n") - for _, spec := range options { + for _, spec := range shortOptions { + p.printOption(w, spec) + } + for _, spec := range longOptions { p.printOption(w, spec) } } @@ -215,11 +236,16 @@ func (p *Parser) writeHelpForCommand(w io.Writer, cmd *command) { } func (p *Parser) printOption(w io.Writer, spec *spec) { - left := synopsis(spec, "--"+spec.long) - if spec.short != "" { - left += ", " + synopsis(spec, "-"+spec.short) + ways := make([]string, 0, 2) + if spec.long != "" { + ways = append(ways, synopsis(spec, "--"+spec.long)) + } + if spec.short != "" { + ways = append(ways, synopsis(spec, "-"+spec.short)) + } + if len(ways) > 0 { + printTwoCols(w, strings.Join(ways, ", "), spec.help, spec.defaultVal, spec.env) } - printTwoCols(w, left, spec.help, spec.defaultVal, spec.env) } func synopsis(spec *spec, form string) string { diff --git a/usage_test.go b/usage_test.go index 5d379a1..6dee402 100644 --- a/usage_test.go +++ b/usage_test.go @@ -309,3 +309,61 @@ Global options: p.WriteHelp(&help) assert.Equal(t, expectedHelp, help.String()) } + +func TestUsageWithoutLongNames(t *testing.T) { + expectedHelp := `Usage: example [-a PLACEHOLDER] -b SHORTONLY2 + +Options: + -a PLACEHOLDER some help [default: some val] + -b SHORTONLY2 some help2 + --help, -h display this help and exit +` + var args struct { + ShortOnly string `arg:"-a,--" help:"some help" default:"some val" placeholder:"PLACEHOLDER"` + ShortOnly2 string `arg:"-b,--,required" help:"some help2"` + } + p, err := NewParser(Config{Program: "example"}, &args) + assert.NoError(t, err) + var help bytes.Buffer + p.WriteHelp(&help) + assert.Equal(t, expectedHelp, help.String()) +} + +func TestUsageWithShortFirst(t *testing.T) { + expectedHelp := `Usage: example [-c CAT] [--dog DOG] + +Options: + -c CAT + --dog DOG + --help, -h display this help and exit +` + var args struct { + Dog string + Cat string `arg:"-c,--"` + } + p, err := NewParser(Config{Program: "example"}, &args) + assert.NoError(t, err) + var help bytes.Buffer + p.WriteHelp(&help) + assert.Equal(t, expectedHelp, help.String()) +} + +func TestUsageWithEnvOptions(t *testing.T) { + expectedHelp := `Usage: example [-s SHORT] + +Options: + -s SHORT [env: SHORT] + --help, -h display this help and exit +` + var args struct { + Short string `arg:"--,-s,env"` + EnvOnly string `arg:"--,env"` + EnvOnlyOverriden string `arg:"--,env:CUSTOM"` + } + + p, err := NewParser(Config{Program: "example"}, &args) + assert.NoError(t, err) + var help bytes.Buffer + p.WriteHelp(&help) + assert.Equal(t, expectedHelp, help.String()) +}