Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions argparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
54 changes: 42 additions & 12 deletions argparse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Comment thread
syrshax marked this conversation as resolved.
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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
24 changes: 14 additions & 10 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down