From 80008400764b3117480ca7fb2edf15180131602e Mon Sep 17 00:00:00 2001 From: syrshax Date: Fri, 23 Jan 2026 20:24:38 +0100 Subject: [PATCH 1/8] allow arguments with short flag only long optional as detailed in the issue #123 now tools can no longer have a limitation of forcing long flags --- argparse.go | 2 +- argparse_test.go | 37 +++++++++++++++++++++++++------------ command.go | 22 +++++++++++++--------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/argparse.go b/argparse.go index cb2cf28..e1b2fb7 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 diff --git a/argparse_test.go b/argparse_test.go index 24c265e..b4881c3 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -92,7 +92,6 @@ func TestFlagAddArgumentFail(t *testing.T) { } 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"}, } @@ -444,7 +443,6 @@ 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"}, } @@ -670,7 +668,6 @@ 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"}, } @@ -794,7 +791,6 @@ 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"}, } @@ -965,7 +961,6 @@ 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"}, } @@ -1172,7 +1167,6 @@ 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"}, } @@ -1250,7 +1244,6 @@ 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"}, } @@ -1316,7 +1309,6 @@ 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"}, } @@ -1382,7 +1374,6 @@ 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"}, } @@ -1435,7 +1426,6 @@ 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"}, } @@ -1500,7 +1490,6 @@ 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"}, } @@ -2393,7 +2382,6 @@ 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"}, } @@ -3231,3 +3219,28 @@ func TestPositionalDefaults(t *testing.T) { t.Errorf(`*pos4 expected "pos4", but got "%s"`, *pos4) } } + +func TestOptionalLongArgSuccess(t *testing.T) { + testArgs := []string{"progname", "-s", "sarg", "-i", "11", "-f"} + p := NewParser("pos", "description") + + s := p.String("s", "", &Options{Help: "short arg string"}) + i := p.Int("i", "", &Options{Help: "short arg int"}) + f := p.Flag("f", "", &Options{Help: "short arg flag"}) + + if err := p.Parse(testArgs); err != nil { + t.Error(err.Error()) + } + + if *s != "sarg" { + t.Errorf(`*s expected 'sarg', but got "%s`, *s) + } + + if *i != 11 { + t.Errorf(`*i expected '11', but got "%d`, *i) + } + + if *f != true { + t.Errorf(`*f expected 'sarg', but got "%t`, *f) + } +} diff --git a/command.go b/command.go index f87d411..a36e016 100644 --- a/command.go +++ b/command.go @@ -26,8 +26,8 @@ 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") + 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 { From dea6fe955db627c9179abc593aad7fd490e350a8 Mon Sep 17 00:00:00 2001 From: syrshax Date: Sat, 24 Jan 2026 10:36:32 +0100 Subject: [PATCH 2/8] A tt with the 4 cases --- argparse_test.go | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/argparse_test.go b/argparse_test.go index b4881c3..2dbd3e4 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -3220,27 +3220,32 @@ func TestPositionalDefaults(t *testing.T) { } } -func TestOptionalLongArgSuccess(t *testing.T) { - testArgs := []string{"progname", "-s", "sarg", "-i", "11", "-f"} - p := NewParser("pos", "description") - - s := p.String("s", "", &Options{Help: "short arg string"}) - i := p.Int("i", "", &Options{Help: "short arg int"}) - f := p.Flag("f", "", &Options{Help: "short arg flag"}) - - if err := p.Parse(testArgs); err != nil { - t.Error(err.Error()) +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}, } - if *s != "sarg" { - t.Errorf(`*s expected 'sarg', but got "%s`, *s) - } - - if *i != 11 { - t.Errorf(`*i expected '11', but got "%d`, *i) - } + 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) + } + }() - if *f != true { - t.Errorf(`*f expected 'sarg', but got "%t`, *f) + p := NewParser("prog", "desc") + p.String(tc.short, tc.long, nil) + }) } } From 48289f6d6c202e2df8321ad3f2c39dc206822a9d Mon Sep 17 00:00:00 2001 From: syrshax Date: Sat, 24 Jan 2026 10:47:34 +0100 Subject: [PATCH 3/8] Added all type cases with both empty args --- argparse_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/argparse_test.go b/argparse_test.go index 2dbd3e4..1fe113b 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -92,6 +92,7 @@ func TestFlagAddArgumentFail(t *testing.T) { } 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: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add Flag: either long name or short 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"}, } @@ -445,6 +446,7 @@ func TestFlagCounterAddArgumentFail(t *testing.T) { 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 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,6 +672,7 @@ func TestStringAddArgumentFail(t *testing.T) { 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 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) { @@ -793,6 +796,7 @@ func TestIntAddArgumentFail(t *testing.T) { 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 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) { @@ -963,6 +967,7 @@ func TestFileAddArgumentFail(t *testing.T) { 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 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) { @@ -1169,6 +1174,7 @@ func TestFileListAddArgumentFail(t *testing.T) { 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 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) { @@ -1246,6 +1252,7 @@ func TestFloatListAddArgumentFail(t *testing.T) { 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 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) { @@ -1311,6 +1318,7 @@ func TestIntListAddArgumentFail(t *testing.T) { 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 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) { @@ -1376,6 +1384,7 @@ func TestStringListAddArgumentFail(t *testing.T) { 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 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) { @@ -1428,6 +1437,7 @@ func TestListAddArgumentFail(t *testing.T) { 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 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) { @@ -1492,6 +1502,7 @@ func TestSelectorAddArgumentFail(t *testing.T) { 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 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) { @@ -2384,6 +2395,7 @@ func TestFloatAddArgumentFail(t *testing.T) { 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 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) { From c228577fad17eea34687239ed199205c0f69d5fa Mon Sep 17 00:00:00 2001 From: syrshax Date: Sat, 24 Jan 2026 10:50:45 +0100 Subject: [PATCH 4/8] Updated README to cover short or long args can be specified --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 4de3e600eb34daff94b79ec60fa576d29262bd61 Mon Sep 17 00:00:00 2001 From: syrshax Date: Sat, 24 Jan 2026 18:08:13 +0100 Subject: [PATCH 5/8] fix: changed test to the end of list for consistency between all other tests --- argparse_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/argparse_test.go b/argparse_test.go index 1fe113b..c12bc61 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -92,9 +92,9 @@ func TestFlagAddArgumentFail(t *testing.T) { } 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: "Both names empty", shortArg: "", longArg: "", failureMessage: "unable to add Flag: either long name or short 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) { From 6c9247612d10f2aa0e7eedfb17b5ba9716742057 Mon Sep 17 00:00:00 2001 From: syrshax Date: Sat, 24 Jan 2026 18:14:07 +0100 Subject: [PATCH 6/8] nit: placed test near the beginning of the list --- argparse_test.go | 60 ++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/argparse_test.go b/argparse_test.go index c12bc61..9ee8811 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -86,6 +86,36 @@ 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 @@ -3231,33 +3261,3 @@ func TestPositionalDefaults(t *testing.T) { t.Errorf(`*pos4 expected "pos4", but got "%s"`, *pos4) } } - -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) - }) - } -} From 856dc9f9eb0f6f263c3928beb8d78d678f0eb21b Mon Sep 17 00:00:00 2001 From: syrshax Date: Sat, 24 Jan 2026 21:46:42 +0100 Subject: [PATCH 7/8] updated comment to match new feat --- command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command.go b/command.go index a36e016..5fffcf6 100644 --- a/command.go +++ b/command.go @@ -25,7 +25,7 @@ func (o *Command) help(sname, lname string) { } func (o *Command) addArg(a *arg) error { - // 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") } From 3b7ee5f27b2015016999482befd9838a8b0e75fd Mon Sep 17 00:00:00 2001 From: syrshax Date: Mon, 26 Jan 2026 09:42:13 +0100 Subject: [PATCH 8/8] fix: as requested by @jlombera help command with empty long arg was being printed wrongly --- argparse.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/argparse.go b/argparse.go index e1b2fb7..a2c10a4 100644 --- a/argparse.go +++ b/argparse.go @@ -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)