diff --git a/README.md b/README.md index 0560083..fe00a12 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ There are a few caveats (or more like design choices) to know about: * Shorthand arguments MUST be a single character. Shorthand arguments are prepended with single dash `"-"` * If not convenient shorthand argument can be completely skipped by passing empty string `""` as first argument * Shorthand arguments ONLY for `parser.Flag()` and `parser.FlagCounter()` can be combined into single argument same as `ps -aux`, `rm -rf` or `lspci -vvk` -* Long arguments must be specified and cannot be empty. They are prepended with double dash `"--"` +* Either short or long argument must be specified (at least one is required). Long arguments are prepended with double dash `"--"` * You cannot define two same arguments. Only first one will be used. For example doing `parser.Flag("t", "test", nil)` followed by `parser.String("t", "test2", nil)` will not work as second `String` argument will be ignored (note that both have `"t"` as shorthand argument). However since it is case-sensitive library, you can work arounf it by capitalizing one of the arguments * There is a pre-defined argument for `-h|--help`, so from above attempting to define any argument using `h` as shorthand will fail * `parser.Parse()` returns error in case of something going wrong, but it is not expected to cover ALL cases diff --git a/argparse.go b/argparse.go index cb2cf28..a2c10a4 100644 --- a/argparse.go +++ b/argparse.go @@ -16,7 +16,7 @@ const DisableDescription = "DISABLEDDESCRIPTIONWILLNOTSHOWUP" // will panic. const positionalArgName = "_positionalArg_%s_%d" -//disable help can be invoked from the parse and then needs to be propogated to subcommands +// disable help can be invoked from the parse and then needs to be propogated to subcommands var disableHelp = false // Command is a basic type for this package. It represents top level Parser as well as any commands and sub-commands @@ -694,7 +694,9 @@ func arguments2Result(result string, arguments []*arg, maxWidth int) string { } else { arg = arg + " " } - arg = arg + "--" + argument.lname + if argument.lname != "" { + arg = arg + "--" + argument.lname + } arg = arg + strings.Repeat(" ", argPadding-len(arg)) if argument.opts != nil && argument.opts.Help != "" { arg = addToLastLine(arg, argument.getHelpMessage(), maxWidth, argPadding, true) diff --git a/argparse_test.go b/argparse_test.go index 24c265e..9ee8811 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -86,15 +86,45 @@ func TestInternalFunctionCheck(t *testing.T) { a.parsed = false } +func TestOptionalLongArg(t *testing.T) { + tt := []struct { + name string + short, long string + shouldPanic bool + }{ + {"both filled", "s", "string", false}, + {"only long", "", "string", false}, + {"only short", "s", "", false}, + {"both empty", "", "", true}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if tc.shouldPanic && r == nil { + t.Errorf("expected panic but got none") + } + if !tc.shouldPanic && r != nil { + t.Errorf("unexpected panic: %v", r) + } + }() + + p := NewParser("prog", "desc") + p.String(tc.short, tc.long, nil) + }) + } +} + func TestFlagAddArgumentFail(t *testing.T) { type testCase struct { testName, shortArg, longArg, failureMessage string } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add Flag: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add Flag: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add Flag: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add Flag: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add Flag: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -444,9 +474,9 @@ func TestFlagCounterAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add FlagCounter: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add FlagCounter: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add FlagCounter: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add FlagCounter: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add FlagCounter: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -670,9 +700,9 @@ func TestStringAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add String: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add String: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add String: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add String: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add String: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -794,9 +824,9 @@ func TestIntAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add Int: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add Int: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add Int: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add Int: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add Int: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -965,9 +995,9 @@ func TestFileAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add File: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add File: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add File: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add File: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add File: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -1172,9 +1202,9 @@ func TestFileListAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add FileList: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add FileList: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add FileList: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add FileList: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add FileList: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -1250,9 +1280,9 @@ func TestFloatListAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add FloatList: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add FloatList: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add FloatList: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add FloatList: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add FloatList: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -1316,9 +1346,9 @@ func TestIntListAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add IntList: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add IntList: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add IntList: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add IntList: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add IntList: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -1382,9 +1412,9 @@ func TestStringListAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add StringList: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add StringList: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add StringList: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add StringList: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add StringList: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -1435,9 +1465,9 @@ func TestListAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add StringList: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add StringList: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add StringList: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add StringList: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add StringList: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -1500,9 +1530,9 @@ func TestSelectorAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add Selector: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add Selector: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add Selector: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add Selector: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add Selector: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { @@ -2393,9 +2423,9 @@ func TestFloatAddArgumentFail(t *testing.T) { } tt := []testCase{ testCase{testName: "Long short name", shortArg: "ff", longArg: "flag2", failureMessage: "unable to add Float: short name must not exceed 1 character"}, - testCase{testName: "Long name not provided", shortArg: "f", longArg: "", failureMessage: "unable to add Float: long name should be provided"}, testCase{testName: "Long name twice", shortArg: "f", longArg: "flag1", failureMessage: "unable to add Float: long name flag1 occurs more than once"}, testCase{testName: "Short name twice", shortArg: "F", longArg: "flag2", failureMessage: "unable to add Float: short name F occurs more than once"}, + testCase{testName: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add Float: either long name or short name should be provided"}, } for _, tc := range tt { t.Run(tc.testName, func(t *testing.T) { diff --git a/command.go b/command.go index f87d411..5fffcf6 100644 --- a/command.go +++ b/command.go @@ -25,9 +25,9 @@ func (o *Command) help(sname, lname string) { } func (o *Command) addArg(a *arg) error { - // long name should be provided - if a.lname == "" { - return fmt.Errorf("long name should be provided") + // either long name or short name should be provided + if a.lname == "" && a.sname == "" { + return fmt.Errorf("either long name or short name should be provided") } // short name could be provided and must not exceed 1 character if len(a.sname) > 1 { @@ -42,7 +42,7 @@ func (o *Command) addArg(a *arg) error { if a.sname != "" && a.sname == v.sname { return fmt.Errorf("short name %s occurs more than once", a.sname) } - if a.lname == v.lname { + if a.lname != "" && a.lname == v.lname { return fmt.Errorf("long name %s occurs more than once", a.lname) } } @@ -66,7 +66,7 @@ func (o *Command) addArg(a *arg) error { return nil } -//parseSubCommands - Parses subcommands if any +// parseSubCommands - Parses subcommands if any func (o *Command) parseSubCommands(args *[]string) error { if o.commands != nil && len(o.commands) > 0 { // If we have subcommands and 0 args left @@ -92,10 +92,13 @@ func (o *Command) parseSubCommands(args *[]string) error { // Breadth-first parse style for positionals // Each command proceeds left to right consuming as many -// positionals as it needs before beginning sub-command parsing +// +// positionals as it needs before beginning sub-command parsing +// // All flags must have been parsed and reduced prior to calling this // Positionals will consume any remaining values, -// disregarding if they have dashes or equals signs or other "delims". +// +// disregarding if they have dashes or equals signs or other "delims". func (o *Command) parsePositionals(inputArgs *[]string) error { for _, oarg := range o.args { // Two-stage parsing, this is the second stage @@ -129,7 +132,7 @@ func (o *Command) parsePositionals(inputArgs *[]string) error { return nil } -//parseArguments - Parses arguments +// parseArguments - Parses arguments func (o *Command) parseArguments(inputArgs *[]string) error { // Iterate over the args for _, oarg := range o.args { @@ -193,8 +196,9 @@ func (o *Command) parseArguments(inputArgs *[]string) error { // Will parse provided list of arguments // common usage would be to pass directly os.Args // Depth-first parsing: We will reach the deepest -// node of the command tree and then parse arguments, -// stepping back up only after each node is satisfied. +// +// node of the command tree and then parse arguments, +// stepping back up only after each node is satisfied. func (o *Command) parse(args *[]string) error { // If already been parsed do nothing if o.parsed {