From 70c91865bf0cb6f591a7431eae36c405e1144218 Mon Sep 17 00:00:00 2001 From: vsachs Date: Wed, 27 Jul 2022 13:45:37 -0700 Subject: [PATCH 01/12] Initial prototype of positional arguments --- argparse.go | 12 ++++++++---- argument.go | 28 +++++++++++++++++++++++---- command.go | 55 +++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/argparse.go b/argparse.go index d035ca4..69e8695 100644 --- a/argparse.go +++ b/argparse.go @@ -80,6 +80,9 @@ type Parser struct { // Options are specific options for every argument. They can be provided if necessary. // Possible fields are: // +// Options.Positional - tells Parser that the argument is positional (implies Required) +// Positional arguments do not require the flag name to precede them and must come in a specific order. +// // Options.Required - tells Parser that this argument is required to be provided. // useful when specific Command requires some data provided. // @@ -95,10 +98,11 @@ type Parser struct { // in case if this argument was not supplied on command line. File default value is a string which it will be open with // provided options. In case if provided value type does not match expected, the error will be returned on run-time. type Options struct { - Required bool - Validate func(args []string) error - Help string - Default interface{} + Positional bool + Required bool + Validate func(args []string) error + Help string + Default interface{} } // NewParser creates new Parser object that will allow to add arguments for parsing diff --git a/argument.go b/argument.go index 0fca9d5..ed20029 100644 --- a/argument.go +++ b/argument.go @@ -47,6 +47,14 @@ type Arg interface { GetSname() string GetLname() string GetResult() interface{} + GetPositional() bool +} + +func (o arg) GetPositional() bool { + if o.opts != nil { + return o.opts.Positional + } + return false } func (o arg) GetOpts() *Options { @@ -86,7 +94,7 @@ func (o *arg) checkLongName(argument string) int { return 0 } -// checkShortName if argumet present. +// checkShortName if argument present. // checkShortName - returns the argumet's short name number of occurrences and error. // For shorthand argument - 0 if there is no occurrences, or count of occurrences. // Shorthand argument with parametr, mast be the only or last in the argument string. @@ -119,7 +127,7 @@ func (o *arg) checkShortName(argument string) (int, error) { return 0, nil } -// check if argumet present. +// check if argument present. // check - returns the argumet's number of occurrences and error. // For long name return value is 0 or 1. // For shorthand argument - 0 if there is no occurrences, or count of occurrences. @@ -133,6 +141,10 @@ func (o *arg) check(argument string) (int, error) { return o.checkShortName(argument) } +func (o *arg) reducePositional(position int, args *[]string) { + (*args)[position] = "" +} + func (o *arg) reduceLongName(position int, args *[]string) { argument := (*args)[position] // Check for long name only if not empty @@ -184,8 +196,12 @@ func (o *arg) reduceShortName(position int, args *[]string) { // clear out already used argument from args at position func (o *arg) reduce(position int, args *[]string) { - o.reduceLongName(position, args) - o.reduceShortName(position, args) + if o.GetPositional() { + o.reducePositional(position, args) + } else { + o.reduceLongName(position, args) + o.reduceShortName(position, args) + } } func (o *arg) parseInt(args []string, argCount int) error { @@ -400,6 +416,10 @@ func (o *arg) parseSomeType(args []string, argCount int) error { return err } +func (o *arg) parsePositional(arg string) error { + return o.parse([]string{arg}, 1) +} + func (o *arg) parse(args []string, argCount int) error { // If unique do not allow more than one time if o.unique && (o.parsed || argCount > 1) { diff --git a/command.go b/command.go index 9f2191f..2db110e 100644 --- a/command.go +++ b/command.go @@ -51,7 +51,13 @@ func (o *Command) addArg(a *arg) error { current = current.parent } a.parent = o + + if a.GetPositional() { + a.opts.Required = true + a.size = 1 // We could allow other sizes in the future + } o.args = append(o.args, a) + return nil } @@ -80,21 +86,44 @@ func (o *Command) parseSubCommands(args *[]string) error { } //parseArguments - Parses arguments -func (o *Command) parseArguments(args *[]string) error { +func (o *Command) parseArguments(inputArgs *[]string) error { // Iterate over the args - for i := 0; i < len(o.args); i++ { - oarg := o.args[i] - for j := 0; j < len(*args); j++ { - arg := (*args)[j] + for _, oarg := range o.args { + for j := 0; j < len(*inputArgs); j++ { + arg := (*inputArgs)[j] if arg == "" { continue } + if oarg.GetPositional() { + // Skip any flags + // This has the subtle effect of requiring flags + // to use `=` for their value pairing if any + // positionals are defined AND are not satisfied yet. + // If they don't use `=` then the positional parse + // will unknowingly consume the arg on next iteration. + // + // It would be possible to potentially avoid this + // requirement IF we choose to check whether the + // flag in question has a default. If not then we + // know either: + // it must be for that flag OR + // the user made an error + // However this is highly ambiguous so best avoided. + if strings.HasPrefix(arg, "-") { + continue + } + if err := oarg.parsePositional(arg); err != nil { + return err + } + oarg.reduce(j, inputArgs) + break // Positionals can only occur once + } if strings.Contains(arg, "=") { splitInd := strings.LastIndex(arg, "=") equalArg := []string{arg[:splitInd], arg[splitInd+1:]} if cnt, err := oarg.check(equalArg[0]); err != nil { return err - } else if cnt > 0 { + } else if cnt > 0 { // No args implies we supply default if equalArg[1] == "" { return fmt.Errorf("not enough arguments for %s", oarg.name()) } @@ -105,21 +134,21 @@ func (o *Command) parseArguments(args *[]string) error { if err != nil { return err } - oarg.reduce(j, args) + oarg.reduce(j, inputArgs) continue } } if cnt, err := oarg.check(arg); err != nil { return err } else if cnt > 0 { - if len(*args) < j+oarg.size { + if len(*inputArgs) < j+oarg.size { return fmt.Errorf("not enough arguments for %s", oarg.name()) } - err := oarg.parse((*args)[j+1:j+oarg.size], cnt) + err := oarg.parse((*inputArgs)[j+1:j+oarg.size], cnt) if err != nil { return err } - oarg.reduce(j, args) + oarg.reduce(j, inputArgs) continue } } @@ -127,10 +156,8 @@ func (o *Command) parseArguments(args *[]string) error { // Check if arg is required and not provided if oarg.opts != nil && oarg.opts.Required && !oarg.parsed { return fmt.Errorf("[%s] is required", oarg.name()) - } - - // Check for argument default value and if provided try to type cast and assign - if oarg.opts != nil && oarg.opts.Default != nil && !oarg.parsed { + } else if oarg.opts != nil && oarg.opts.Default != nil && !oarg.parsed { + // Check for argument default value and if provided try to type cast and assign err := oarg.setDefault() if err != nil { return err From 3d0a35c1d1b065ca7f06550a5d251815b339b392 Mon Sep 17 00:00:00 2001 From: vsachs Date: Thu, 28 Jul 2022 17:15:03 -0700 Subject: [PATCH 02/12] Unit tests and minor changes --- README.md | 3 + argparse.go | 3 + argparse_test.go | 212 +++++++++++++++++++++++++++++++++++++++++++++++ argument.go | 11 ++- command.go | 9 +- 5 files changed, 231 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7404f6b..ab8db14 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,9 @@ var myLogFiles *[]os.File = parser.FileList("l", "log-file", os.O_RDWR, 0600, .. ``` You can implement sub-commands in your CLI using `parser.NewCommand()` or go even deeper with `command.NewCommand()`. +Addition of a sub-command does not prevent usage of the parent command with or without flags. +Sub-commands are always parsed before arguments. +If a command has positional arguments and sub-commands then sub-commands take precedence. Since parser inherits from command, every command supports exactly same options as parser itself, thus allowing to add arguments specific to that command or more global arguments added on parser itself! diff --git a/argparse.go b/argparse.go index 69e8695..d2e04c3 100644 --- a/argparse.go +++ b/argparse.go @@ -82,6 +82,8 @@ type Parser struct { // // Options.Positional - tells Parser that the argument is positional (implies Required) // Positional arguments do not require the flag name to precede them and must come in a specific order. +// Positional sets Required=true, Default=nil, Shortname="" +// Existence of a positional means that any flags preceding the positional must use `=` to pair with their value. // // Options.Required - tells Parser that this argument is required to be provided. // useful when specific Command requires some data provided. @@ -704,6 +706,7 @@ func (o *Parser) Parse(args []string) error { unparsed = append(unparsed, v) } } + if result == nil && len(unparsed) > 0 { return errors.New("unknown arguments " + strings.Join(unparsed, " ")) } diff --git a/argparse_test.go b/argparse_test.go index 138517a..80872fa 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -2751,3 +2751,215 @@ func TestCommandHelpSetSnameOnly(t *testing.T) { t.Error("Help arugment names should have defaulted") } } + +func TestCommandPositional(t *testing.T) { + testArgs1 := []string{"pos", "heyo"} + parser := NewParser("pos", "") + strval := parser.String("s", "string", &Options{Positional: true}) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != "heyo" { + t.Errorf("Strval did not match expected") + } +} + +func TestCommandPositionalErr(t *testing.T) { + errArgs1 := []string{"pos"} + parser := NewParser("pos", "") + strval := parser.String("s", "beep", &Options{Positional: true}) + + if err := parser.Parse(errArgs1); err == nil { + t.Errorf("Positional required") + } else if err.Error() != "[beep] is required" { + // This is an admittedly slightly confusing error message + // but it basically means the parser bailed because the arg list + // is empty...? + t.Error(err.Error()) + } else if *strval != "" { + t.Errorf("Strval nonempty") + } +} + +func TestCommandPositionals(t *testing.T) { + testArgs1 := []string{"posint", "5", "abc", "1.0"} + parser := NewParser("posint", "") + intval := parser.Int("i", "integer", &Options{Positional: true}) + strval := parser.String("s", "string", &Options{Positional: true}) + floatval := parser.Float("f", "floats", &Options{Positional: true}) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *intval != 5 { + t.Error("Intval did not match expected") + } else if *strval != "abc" { + t.Error("Strval did not match expected") + } else if *floatval != 1.0 { + t.Error("Floatval did not match expected") + } +} + +func TestCommandPositionalsErr(t *testing.T) { + errArgs1 := []string{"posint", "abc", "abc", "1.0"} + parser := NewParser("posint", "") + _ = parser.Int("i", "cool", &Options{Positional: true}) + _ = parser.String("s", "string", &Options{Positional: true}) + _ = parser.Float("f", "floats", &Options{Positional: true}) + + if err := parser.Parse(errArgs1); err == nil { + t.Error("String argument accepted for integer") + } else if err.Error() != "[cool] bad integer value [abc]" { + t.Error(err.Error()) + } +} + +func TestPos1(t *testing.T) { + testArgs1 := []string{"pos", "subcommand1", "-i=2", "abc"} + parser := NewParser("pos", "") + + strval := parser.String("s", "string", &Options{Positional: true}) + com1 := parser.NewCommand("subcommand1", "beep") + intval := com1.Int("i", "integer", nil) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != "abc" { + t.Error("Strval did not match expected") + } else if *intval != 2 { + t.Error("intval did not match expected") + } +} + +func TestPos2(t *testing.T) { + testArgs1 := []string{"pos", "subcommand1", "a123"} + parser := NewParser("pos", "") + + strval := parser.String("s", "string", &Options{Positional: true}) + com1 := parser.NewCommand("subcommand1", "beep") + intval := com1.Int("i", "integer", nil) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != "a123" { + t.Error("Strval did not match expected") + } else if *intval != 0 { + t.Error("intval did not match expected") + } +} + +func TestPos3(t *testing.T) { + testArgs1 := []string{"pos", "subcommand1", "xyz", "--integer", "3"} + parser := NewParser("pos", "") + + strval := parser.String("s", "string", &Options{Positional: true}) + com1 := parser.NewCommand("subcommand1", "beep") + intval := com1.Int("i", "integer", nil) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != "xyz" { + t.Error("Strval did not match expected") + } else if *intval != 3 { + t.Error("intval did not match expected") + } +} + +func TestPos4(t *testing.T) { + testArgs1 := []string{"pos", "abc"} + parser := NewParser("pos", "") + + strval := parser.String("s", "string", &Options{Positional: true}) + com1 := parser.NewCommand("subcommand1", "beep") + intval := com1.Int("i", "integer", nil) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != "abc" { + t.Error("Strval did not match expected") + } else if *intval != 0 { + t.Error("intval did not match expected") + } +} +func TestPosErr1(t *testing.T) { + errArgs1 := []string{"pos", "subcommand1"} + parser := NewParser("pos", "") + + strval := parser.String("s", "posy", &Options{Positional: true, Default: "abc"}) + com1 := parser.NewCommand("subcommand1", "beep") + intval := com1.Int("i", "integer", nil) + + if err := parser.Parse(errArgs1); err == nil { + t.Error("Subcommand should be required") + } else if err.Error() != "[posy] is required" { + t.Error(err.Error()) + } else if *strval != "" { + t.Error("strval incorrectly defaulted:" + *strval) + } else if *intval != 0 { + t.Error("intval did not match expected") + } +} + +// Must break up for correct unit testing +// func TestCommandSubcommandPositionals(t *testing.T) { +// testArgs1 := []string{"pos", "subcommand2", "efg"} +// testArgs2 := []string{"pos", "subcommand1"} +// testArgs3 := []string{"pos", "subcommand2", "abc", "-i", "1"} +// testArgs4 := []string{"pos", "subcommand2", "abc", "--integer", "1"} +// testArgs5 := []string{"pos", "subcommand2", "abc", "-i=1"} +// testArgs6 := []string{"pos", "subcommand2", "abc", "--integer=1"} +// // flags before positional must use `=` for values +// testArgs7 := []string{"pos", "subcommand2", "-i=1", "abc"} +// testArgs8 := []string{"pos", "subcommand2", "--integer=1", "abc"} +// testArgs9 := []string{"pos", "subcommand3", "true"} +// // Error cases +// errArgs1 := []string{"pos", "subcommand2", "--i", "1"} +// errArgs2 := []string{"pos", "subcommand2", "--i", "1", "abc"} +// errArgs3 := []string{"pos", "subcommand3", "abc"} +// parser := NewParser("pos", "") + +// _ = parser.NewCommand("subcommand1", "") +// com2 := parser.NewCommand("subcommand2", "") +// com2.String("s", "string", &Options{Positional: true}) +// com2.Int("i", "integer", nil) +// com2.Flag("b", "bool", nil) +// com3 := parser.NewCommand("subcommand3", "") +// com3.Flag("b", "bool", &Options{Positional: true}) + +// if err := parser.Parse(testArgs1); err != nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(testArgs2); err != nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(testArgs3); err != nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(testArgs4); err != nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(testArgs5); err != nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(testArgs6); err != nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(testArgs7); err != nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(testArgs8); err != nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(testArgs9); err != nil { +// t.Error(err.Error()) +// } + +// if err := parser.Parse(errArgs1); err == nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(errArgs2); err == nil { +// t.Error(err.Error()) +// } +// if err := parser.Parse(errArgs3); err == nil { +// t.Error(err.Error()) +// } +// } diff --git a/argument.go b/argument.go index ed20029..4e3d8f7 100644 --- a/argument.go +++ b/argument.go @@ -128,7 +128,7 @@ func (o *arg) checkShortName(argument string) (int, error) { } // check if argument present. -// check - returns the argumet's number of occurrences and error. +// check - returns the argument's number of occurrences and error. // For long name return value is 0 or 1. // For shorthand argument - 0 if there is no occurrences, or count of occurrences. // Shorthand argument with parametr, mast be the only or last in the argument string. @@ -417,7 +417,11 @@ func (o *arg) parseSomeType(args []string, argCount int) error { } func (o *arg) parsePositional(arg string) error { - return o.parse([]string{arg}, 1) + if err := o.parse([]string{arg}, 1); err != nil { + return err + } + + return nil } func (o *arg) parse(args []string, argCount int) error { @@ -437,6 +441,9 @@ func (o *arg) parse(args []string, argCount int) error { } func (o *arg) name() string { + if o.GetPositional() { + return o.lname + } var name string if o.lname == "" { name = "-" + o.sname diff --git a/command.go b/command.go index 2db110e..80e4617 100644 --- a/command.go +++ b/command.go @@ -53,7 +53,9 @@ func (o *Command) addArg(a *arg) error { a.parent = o if a.GetPositional() { + a.sname = "" a.opts.Required = true + a.opts.Default = nil a.size = 1 // We could allow other sizes in the future } o.args = append(o.args, a) @@ -94,8 +96,8 @@ func (o *Command) parseArguments(inputArgs *[]string) error { if arg == "" { continue } - if oarg.GetPositional() { - // Skip any flags + if !strings.HasPrefix(arg, "-") && oarg.GetPositional() { + // If this arg is a flag we just skip parsing positionals // This has the subtle effect of requiring flags // to use `=` for their value pairing if any // positionals are defined AND are not satisfied yet. @@ -109,9 +111,6 @@ func (o *Command) parseArguments(inputArgs *[]string) error { // it must be for that flag OR // the user made an error // However this is highly ambiguous so best avoided. - if strings.HasPrefix(arg, "-") { - continue - } if err := oarg.parsePositional(arg); err != nil { return err } From 7c3906d284c5d7734a1123e44d023c8426dd34fa Mon Sep 17 00:00:00 2001 From: vsachs Date: Fri, 29 Jul 2022 09:35:26 -0700 Subject: [PATCH 03/12] Fail command.addArg for disallowed positional types --- argparse_test.go | 153 +++++++++++++++++++++++++++-------------------- command.go | 4 ++ 2 files changed, 91 insertions(+), 66 deletions(-) diff --git a/argparse_test.go b/argparse_test.go index 80872fa..ea376b6 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -2872,14 +2872,32 @@ func TestPos4(t *testing.T) { com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) - if err := parser.Parse(testArgs1); err != nil { + if err := parser.Parse(testArgs1); err != nil && + err.Error() != "[sub]Command required" { t.Error(err.Error()) - } else if *strval != "abc" { + } else if *strval != "" { t.Error("Strval did not match expected") } else if *intval != 0 { t.Error("intval did not match expected") } } + +func TestPos5(t *testing.T) { + errStr := "unable to add Flag: argument type cannot be positional" + parser := NewParser("pos", "") + var boolval *bool + // Catch the panic + defer func() { + err := recover() + if err.(error).Error() != errStr { + t.Error(err.(error).Error()) + } else if boolval != nil { + t.Error("Boolval was set") + } + }() + boolval = parser.Flag("", "booly", &Options{Positional: true}) +} + func TestPosErr1(t *testing.T) { errArgs1 := []string{"pos", "subcommand1"} parser := NewParser("pos", "") @@ -2899,67 +2917,70 @@ func TestPosErr1(t *testing.T) { } } -// Must break up for correct unit testing -// func TestCommandSubcommandPositionals(t *testing.T) { -// testArgs1 := []string{"pos", "subcommand2", "efg"} -// testArgs2 := []string{"pos", "subcommand1"} -// testArgs3 := []string{"pos", "subcommand2", "abc", "-i", "1"} -// testArgs4 := []string{"pos", "subcommand2", "abc", "--integer", "1"} -// testArgs5 := []string{"pos", "subcommand2", "abc", "-i=1"} -// testArgs6 := []string{"pos", "subcommand2", "abc", "--integer=1"} -// // flags before positional must use `=` for values -// testArgs7 := []string{"pos", "subcommand2", "-i=1", "abc"} -// testArgs8 := []string{"pos", "subcommand2", "--integer=1", "abc"} -// testArgs9 := []string{"pos", "subcommand3", "true"} -// // Error cases -// errArgs1 := []string{"pos", "subcommand2", "--i", "1"} -// errArgs2 := []string{"pos", "subcommand2", "--i", "1", "abc"} -// errArgs3 := []string{"pos", "subcommand3", "abc"} -// parser := NewParser("pos", "") - -// _ = parser.NewCommand("subcommand1", "") -// com2 := parser.NewCommand("subcommand2", "") -// com2.String("s", "string", &Options{Positional: true}) -// com2.Int("i", "integer", nil) -// com2.Flag("b", "bool", nil) -// com3 := parser.NewCommand("subcommand3", "") -// com3.Flag("b", "bool", &Options{Positional: true}) - -// if err := parser.Parse(testArgs1); err != nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(testArgs2); err != nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(testArgs3); err != nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(testArgs4); err != nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(testArgs5); err != nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(testArgs6); err != nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(testArgs7); err != nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(testArgs8); err != nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(testArgs9); err != nil { -// t.Error(err.Error()) -// } - -// if err := parser.Parse(errArgs1); err == nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(errArgs2); err == nil { -// t.Error(err.Error()) -// } -// if err := parser.Parse(errArgs3); err == nil { -// t.Error(err.Error()) -// } -// } +func TestCommandSubcommandPositionals(t *testing.T) { + testArgs1 := []string{"pos", "subcommand2", "efg"} + testArgs2 := []string{"pos", "subcommand1"} + testArgs3 := []string{"pos", "subcommand2", "abc", "-i", "1"} + testArgs4 := []string{"pos", "subcommand2", "abc", "--integer", "1"} + testArgs5 := []string{"pos", "subcommand2", "abc", "-i=1"} + testArgs6 := []string{"pos", "subcommand2", "abc", "--integer=1"} + // flags before positional must use `=` for values + testArgs7 := []string{"pos", "subcommand2", "-i=1", "abc"} + testArgs8 := []string{"pos", "subcommand2", "--integer=1", "abc"} + testArgs9 := []string{"pos", "subcommand3", "second"} + // Error cases + errArgs1 := []string{"pos", "subcommand2", "-i", "1"} + errArgs2 := []string{"pos", "subcommand2", "-i", "1", "abc"} + errArgs3 := []string{"pos", "subcommand3", "abc"} + + newParser := func() *Parser { + parser := NewParser("pos", "") + _ = parser.NewCommand("subcommand1", "") + com2 := parser.NewCommand("subcommand2", "") + com2.String("s", "string", &Options{Positional: true}) + com2.Int("i", "integer", nil) + com2.Flag("b", "bool", nil) + com3 := parser.NewCommand("subcommand3", "") + com3.Selector("", "select", []string{"first", "second"}, + &Options{Positional: true}) + return parser + } + + if err := newParser().Parse(testArgs1); err != nil { + t.Error(err.Error()) + } + if err := newParser().Parse(testArgs2); err != nil { + t.Error(err.Error()) + } + if err := newParser().Parse(testArgs3); err != nil { + t.Error(err.Error()) + } + if err := newParser().Parse(testArgs4); err != nil { + t.Error(err.Error()) + } + if err := newParser().Parse(testArgs5); err != nil { + t.Error(err.Error()) + } + if err := newParser().Parse(testArgs6); err != nil { + t.Error(err.Error()) + } + if err := newParser().Parse(testArgs7); err != nil { + t.Error(err.Error()) + } + if err := newParser().Parse(testArgs8); err != nil { + t.Error(err.Error()) + } + if err := newParser().Parse(testArgs9); err != nil { + t.Error(err.Error()) + } + + if err := newParser().Parse(errArgs1); err == nil { + t.Error("Expected error") + } + if err := newParser().Parse(errArgs2); err == nil { + t.Error("Expected error") + } + if err := newParser().Parse(errArgs3); err == nil { + t.Error("Expected error") + } +} diff --git a/command.go b/command.go index 80e4617..ad05b6b 100644 --- a/command.go +++ b/command.go @@ -53,6 +53,10 @@ func (o *Command) addArg(a *arg) error { a.parent = o if a.GetPositional() { + switch a.argType { + case Flag, StringList, IntList, FloatList, FileList: + return fmt.Errorf("argument type cannot be positional") + } a.sname = "" a.opts.Required = true a.opts.Default = nil From 1e3af52edfa0c5cdec7dc6cddff53ddd49bdd030 Mon Sep 17 00:00:00 2001 From: vsachs Date: Fri, 29 Jul 2022 10:01:54 -0700 Subject: [PATCH 04/12] Remove incorrect comment --- command.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/command.go b/command.go index ad05b6b..b32f999 100644 --- a/command.go +++ b/command.go @@ -101,20 +101,6 @@ func (o *Command) parseArguments(inputArgs *[]string) error { continue } if !strings.HasPrefix(arg, "-") && oarg.GetPositional() { - // If this arg is a flag we just skip parsing positionals - // This has the subtle effect of requiring flags - // to use `=` for their value pairing if any - // positionals are defined AND are not satisfied yet. - // If they don't use `=` then the positional parse - // will unknowingly consume the arg on next iteration. - // - // It would be possible to potentially avoid this - // requirement IF we choose to check whether the - // flag in question has a default. If not then we - // know either: - // it must be for that flag OR - // the user made an error - // However this is highly ambiguous so best avoided. if err := oarg.parsePositional(arg); err != nil { return err } From 24b09dd2999a90ccb2a30fc8a3916d3f4ffd5729 Mon Sep 17 00:00:00 2001 From: vsachs Date: Fri, 29 Jul 2022 10:02:42 -0700 Subject: [PATCH 05/12] Add examples and info to the README --- README.md | 26 +++++++++++++++++++++----- argparse_test.go | 19 ++++++++++++++++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ab8db14..88b3f02 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,12 @@ String will allow you to get a string from arguments, such as `$ progname --stri var myString *string = parser.String("s", "string", ...) ``` +Positional arguments can be used like this `$ progname value1`. +See [Basic Option Structure](#basic-option-structure) and [Positionals](#positionals). +```go +var myString *string = parser.String("", "posarg", &Options{Positional: true}) +``` + Selector works same as a string, except that it will only allow specific values. For example like this `$ progname --debug-level WARN` ```go @@ -134,9 +140,9 @@ var myLogFiles *[]os.File = parser.FileList("l", "log-file", os.O_RDWR, 0600, .. ``` You can implement sub-commands in your CLI using `parser.NewCommand()` or go even deeper with `command.NewCommand()`. -Addition of a sub-command does not prevent usage of the parent command with or without flags. -Sub-commands are always parsed before arguments. -If a command has positional arguments and sub-commands then sub-commands take precedence. +Addition of a sub-command implies that a subcommand is required. +Sub-commands are always parsed before arguments. +If a command has `Positional` arguments and sub-commands then sub-commands take precedence. Since parser inherits from command, every command supports exactly same options as parser itself, thus allowing to add arguments specific to that command or more global arguments added on parser itself! @@ -156,19 +162,21 @@ type Options struct { Validate func(args []string) error Help string Default interface{} + Positional bool } ``` -You can Set `Required` to let it know if it should ask for arguments. +You can set `Required` to let it know if it should ask for arguments. Or you can set `Validate` as a lambda function to make it know while value is valid. Or you can set `Help` for your beautiful help document. Or you can set `Default` will set the default value if user does not provide a value. +You can set `Positional` to indicate an arg is required in a specific position but shouldn't include "--name". Positionals are required in the order they are added. Flags can precede or follow positionals. Example: ``` dirpath := parser.String("d", "dirpath", &argparse.Options{ - Require: false, + Required: false, Help: "the input files' folder path", Default: "input", }) @@ -186,6 +194,14 @@ There are a few caveats (or more like design choices) to know about: * `parser.Parse()` returns error in case of something going wrong, but it is not expected to cover ALL cases * Any arguments that left un-parsed will be regarded as error +##### Positionals +* `Positional` has a set of effects and conditions: + * It will always set Required=True and Default=nil + * It will ignore the shortname + * It will fail (and app will panic) to add the argument if it is one of: + * Flag + * StringList, IntList, FloatList, FileList + #### Contributing diff --git a/argparse_test.go b/argparse_test.go index ea376b6..3602bc6 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -2814,7 +2814,7 @@ func TestCommandPositionalsErr(t *testing.T) { } func TestPos1(t *testing.T) { - testArgs1 := []string{"pos", "subcommand1", "-i=2", "abc"} + testArgs1 := []string{"pos", "subcommand1", "-i", "2", "abc"} parser := NewParser("pos", "") strval := parser.String("s", "string", &Options{Positional: true}) @@ -2898,6 +2898,23 @@ func TestPos5(t *testing.T) { boolval = parser.Flag("", "booly", &Options{Positional: true}) } +func TestPos6(t *testing.T) { + testArgs1 := []string{"pos", "subcommand1", "-i=2", "abc"} + parser := NewParser("pos", "") + + strval := parser.String("s", "string", &Options{Positional: true}) + com1 := parser.NewCommand("subcommand1", "beep") + intval := com1.Int("i", "integer", nil) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != "abc" { + t.Error("Strval did not match expected") + } else if *intval != 2 { + t.Error("intval did not match expected") + } +} + func TestPosErr1(t *testing.T) { errArgs1 := []string{"pos", "subcommand1"} parser := NewParser("pos", "") From b53ab635f9ea83d615fdefb9b1313e238b1ce559 Mon Sep 17 00:00:00 2001 From: vsachs Date: Mon, 1 Aug 2022 13:08:02 -0700 Subject: [PATCH 06/12] Refactor code so that Options.Positional is now private field set in new arg functions --- argparse.go | 79 ++++++++++++++++++++++++++++++++++++++++++++---- argparse_test.go | 61 ++++++++++++++++++++++--------------- argument.go | 2 +- command.go | 4 +-- 4 files changed, 113 insertions(+), 33 deletions(-) diff --git a/argparse.go b/argparse.go index d2e04c3..231071a 100644 --- a/argparse.go +++ b/argparse.go @@ -11,6 +11,11 @@ import ( // DisableDescription can be assigned as a command or arguments description to hide it from the Usage output const DisableDescription = "DISABLEDDESCRIPTIONWILLNOTSHOWUP" +// Positional Prefix +// This must not overlap with any other arguments given or library +// will panic. +const positionalArgName = "_positionalArg_%s_%d" + //disable help can be invoked from the parse and then needs to be propogated to subcommands var disableHelp = false @@ -80,7 +85,7 @@ type Parser struct { // Options are specific options for every argument. They can be provided if necessary. // Possible fields are: // -// Options.Positional - tells Parser that the argument is positional (implies Required) +// Options.positional - tells Parser that the argument is positional (implies Required). Set to true by using *Positional functions. // Positional arguments do not require the flag name to precede them and must come in a specific order. // Positional sets Required=true, Default=nil, Shortname="" // Existence of a positional means that any flags preceding the positional must use `=` to pair with their value. @@ -100,11 +105,13 @@ type Parser struct { // in case if this argument was not supplied on command line. File default value is a string which it will be open with // provided options. In case if provided value type does not match expected, the error will be returned on run-time. type Options struct { - Positional bool - Required bool - Validate func(args []string) error - Help string - Default interface{} + Required bool + Validate func(args []string) error + Help string + Default interface{} + + // Private modifiers + positional bool } // NewParser creates new Parser object that will allow to add arguments for parsing @@ -263,6 +270,18 @@ func (o *Command) String(short string, long string, opts *Options) *string { return &result } +// See func String documentation +func (o *Command) StringPositional(opts *Options) *string { + if opts == nil { + opts = &Options{positional: true} + } else { + opts.positional = true + } + // We supply a long name for documentation and internal logic + name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) + return o.String("", name, opts) +} + // Int creates new int argument, which will attempt to parse following argument as int. // Takes as arguments short name (must be single character or an empty string) // long name and (optional) options. @@ -287,6 +306,18 @@ func (o *Command) Int(short string, long string, opts *Options) *int { return &result } +// See func Int documentation +func (o *Command) IntPositional(opts *Options) *int { + if opts == nil { + opts = &Options{positional: true} + } else { + opts.positional = true + } + // We supply a long name for documentation and internal logic + name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) + return o.Int("", name, opts) +} + // Float creates new float argument, which will attempt to parse following argument as float64. // Takes as arguments short name (must be single character or an empty string) // long name and (optional) options. @@ -311,6 +342,18 @@ func (o *Command) Float(short string, long string, opts *Options) *float64 { return &result } +// See func Float documentation +func (o *Command) FloatPositional(opts *Options) *float64 { + if opts == nil { + opts = &Options{positional: true} + } else { + opts.positional = true + } + // We supply a long name for documentation and internal logic + name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) + return o.Float("", name, opts) +} + // File creates new file argument, which is when provided will check if file exists or attempt to create it // depending on provided flags (same as for os.OpenFile). // It takes same as all other arguments short and long names, additionally it takes flags that specify @@ -340,6 +383,18 @@ func (o *Command) File(short string, long string, flag int, perm os.FileMode, op return &result } +// See func File documentation +func (o *Command) FilePositional(flag int, perm os.FileMode, opts *Options) *os.File { + if opts == nil { + opts = &Options{positional: true} + } else { + opts.positional = true + } + // We supply a long name for documentation and internal logic + name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) + return o.File("", name, flag, perm, opts) +} + // List creates new list argument. This is the argument that is allowed to be present multiple times on CLI. // All appearances of this argument on CLI will be collected into the list of default type values which is strings. // If no argument provided, then the list is empty. @@ -474,6 +529,18 @@ func (o *Command) Selector(short string, long string, options []string, opts *Op return &result } +// See func Selector documentation +func (o *Command) SelectorPositional(allowed []string, opts *Options) *string { + if opts == nil { + opts = &Options{positional: true} + } else { + opts.positional = true + } + // We supply a long name for documentation and internal logic + name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) + return o.Selector("", name, allowed, opts) +} + // message2String puts msg in result string // done boolean indicates if result is ready to be returned // Accepts an interface that can be error, string or fmt.Stringer that will be prepended to a message. diff --git a/argparse_test.go b/argparse_test.go index 3602bc6..e21da8c 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -2755,7 +2755,7 @@ func TestCommandHelpSetSnameOnly(t *testing.T) { func TestCommandPositional(t *testing.T) { testArgs1 := []string{"pos", "heyo"} parser := NewParser("pos", "") - strval := parser.String("s", "string", &Options{Positional: true}) + strval := parser.StringPositional(nil) if err := parser.Parse(testArgs1); err != nil { t.Error(err.Error()) @@ -2764,17 +2764,30 @@ func TestCommandPositional(t *testing.T) { } } +// Lightly test the Options field +func TestCommandPositionalOptions(t *testing.T) { + testArgs1 := []string{"pos", "heyo"} + parser := NewParser("pos", "") + validated := false + strval := parser.StringPositional(&Options{Validate: func(args []string) error { validated = true; return nil }}) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != "heyo" { + t.Errorf("Strval did not match expected") + } else if !validated { + t.Errorf("Validate function not run") + } +} + func TestCommandPositionalErr(t *testing.T) { errArgs1 := []string{"pos"} parser := NewParser("pos", "") - strval := parser.String("s", "beep", &Options{Positional: true}) + strval := parser.StringPositional(nil) if err := parser.Parse(errArgs1); err == nil { t.Errorf("Positional required") - } else if err.Error() != "[beep] is required" { - // This is an admittedly slightly confusing error message - // but it basically means the parser bailed because the arg list - // is empty...? + } else if err.Error() != "[_positionalArg_pos_1] is required" { t.Error(err.Error()) } else if *strval != "" { t.Errorf("Strval nonempty") @@ -2784,9 +2797,9 @@ func TestCommandPositionalErr(t *testing.T) { func TestCommandPositionals(t *testing.T) { testArgs1 := []string{"posint", "5", "abc", "1.0"} parser := NewParser("posint", "") - intval := parser.Int("i", "integer", &Options{Positional: true}) - strval := parser.String("s", "string", &Options{Positional: true}) - floatval := parser.Float("f", "floats", &Options{Positional: true}) + intval := parser.IntPositional(nil) + strval := parser.StringPositional(nil) + floatval := parser.FloatPositional(nil) if err := parser.Parse(testArgs1); err != nil { t.Error(err.Error()) @@ -2802,13 +2815,13 @@ func TestCommandPositionals(t *testing.T) { func TestCommandPositionalsErr(t *testing.T) { errArgs1 := []string{"posint", "abc", "abc", "1.0"} parser := NewParser("posint", "") - _ = parser.Int("i", "cool", &Options{Positional: true}) - _ = parser.String("s", "string", &Options{Positional: true}) - _ = parser.Float("f", "floats", &Options{Positional: true}) + _ = parser.IntPositional(nil) + _ = parser.StringPositional(nil) + _ = parser.FloatPositional(nil) if err := parser.Parse(errArgs1); err == nil { t.Error("String argument accepted for integer") - } else if err.Error() != "[cool] bad integer value [abc]" { + } else if err.Error() != "[_positionalArg_posint_1] bad integer value [abc]" { t.Error(err.Error()) } } @@ -2817,7 +2830,7 @@ func TestPos1(t *testing.T) { testArgs1 := []string{"pos", "subcommand1", "-i", "2", "abc"} parser := NewParser("pos", "") - strval := parser.String("s", "string", &Options{Positional: true}) + strval := parser.StringPositional(nil) com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) @@ -2834,7 +2847,7 @@ func TestPos2(t *testing.T) { testArgs1 := []string{"pos", "subcommand1", "a123"} parser := NewParser("pos", "") - strval := parser.String("s", "string", &Options{Positional: true}) + strval := parser.StringPositional(nil) com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) @@ -2851,7 +2864,7 @@ func TestPos3(t *testing.T) { testArgs1 := []string{"pos", "subcommand1", "xyz", "--integer", "3"} parser := NewParser("pos", "") - strval := parser.String("s", "string", &Options{Positional: true}) + strval := parser.StringPositional(nil) com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) @@ -2868,7 +2881,7 @@ func TestPos4(t *testing.T) { testArgs1 := []string{"pos", "abc"} parser := NewParser("pos", "") - strval := parser.String("s", "string", &Options{Positional: true}) + strval := parser.StringPositional(nil) com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) @@ -2882,6 +2895,7 @@ func TestPos4(t *testing.T) { } } +// Test is covering internal logical error func TestPos5(t *testing.T) { errStr := "unable to add Flag: argument type cannot be positional" parser := NewParser("pos", "") @@ -2895,14 +2909,14 @@ func TestPos5(t *testing.T) { t.Error("Boolval was set") } }() - boolval = parser.Flag("", "booly", &Options{Positional: true}) + boolval = parser.Flag("", "booly", &Options{positional: true}) } func TestPos6(t *testing.T) { testArgs1 := []string{"pos", "subcommand1", "-i=2", "abc"} parser := NewParser("pos", "") - strval := parser.String("s", "string", &Options{Positional: true}) + strval := parser.StringPositional(nil) com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) @@ -2919,13 +2933,13 @@ func TestPosErr1(t *testing.T) { errArgs1 := []string{"pos", "subcommand1"} parser := NewParser("pos", "") - strval := parser.String("s", "posy", &Options{Positional: true, Default: "abc"}) + strval := parser.StringPositional(nil) com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) if err := parser.Parse(errArgs1); err == nil { t.Error("Subcommand should be required") - } else if err.Error() != "[posy] is required" { + } else if err.Error() != "[_positionalArg_pos_1] is required" { t.Error(err.Error()) } else if *strval != "" { t.Error("strval incorrectly defaulted:" + *strval) @@ -2954,12 +2968,11 @@ func TestCommandSubcommandPositionals(t *testing.T) { parser := NewParser("pos", "") _ = parser.NewCommand("subcommand1", "") com2 := parser.NewCommand("subcommand2", "") - com2.String("s", "string", &Options{Positional: true}) + com2.StringPositional(nil) com2.Int("i", "integer", nil) com2.Flag("b", "bool", nil) com3 := parser.NewCommand("subcommand3", "") - com3.Selector("", "select", []string{"first", "second"}, - &Options{Positional: true}) + com3.SelectorPositional([]string{"first", "second"}, nil) return parser } diff --git a/argument.go b/argument.go index 4e3d8f7..cc4634d 100644 --- a/argument.go +++ b/argument.go @@ -52,7 +52,7 @@ type Arg interface { func (o arg) GetPositional() bool { if o.opts != nil { - return o.opts.Positional + return o.opts.positional } return false } diff --git a/command.go b/command.go index b32f999..96e5e0a 100644 --- a/command.go +++ b/command.go @@ -53,8 +53,8 @@ func (o *Command) addArg(a *arg) error { a.parent = o if a.GetPositional() { - switch a.argType { - case Flag, StringList, IntList, FloatList, FileList: + switch a.argType { // Secondary guard + case Flag, FlagCounter, StringList, IntList, FloatList, FileList: return fmt.Errorf("argument type cannot be positional") } a.sname = "" From 10d85f1e1129bd661dde63ab0a5ad429493088ad Mon Sep 17 00:00:00 2001 From: vsachs Date: Mon, 1 Aug 2022 13:34:00 -0700 Subject: [PATCH 07/12] Augment tests to cover new functions better --- argparse_test.go | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/argparse_test.go b/argparse_test.go index e21da8c..4bb0c6e 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -2797,9 +2797,9 @@ func TestCommandPositionalErr(t *testing.T) { func TestCommandPositionals(t *testing.T) { testArgs1 := []string{"posint", "5", "abc", "1.0"} parser := NewParser("posint", "") - intval := parser.IntPositional(nil) + intval := parser.IntPositional(&Options{Required: false}) strval := parser.StringPositional(nil) - floatval := parser.FloatPositional(nil) + floatval := parser.FloatPositional(&Options{Default: 1.5}) if err := parser.Parse(testArgs1); err != nil { t.Error(err.Error()) @@ -2826,6 +2826,20 @@ func TestCommandPositionalsErr(t *testing.T) { } } +// Just test we don't panic on add +// Actual I/O during unit tests already covered by TestFileSimple1 +func TestFilePositional(t *testing.T) { + parser := NewParser("pos", "") + t1 := parser.FilePositional(os.O_RDWR, 0666, nil) + t2 := parser.FilePositional(os.O_RDWR, 0666, &Options{Help: "beep!"}) + + if t1 == nil { + t.Error("File pos was nil") + } else if t2 == nil { + t.Error("File pos was nil") + } +} + func TestPos1(t *testing.T) { testArgs1 := []string{"pos", "subcommand1", "-i", "2", "abc"} parser := NewParser("pos", "") @@ -2929,6 +2943,19 @@ func TestPos6(t *testing.T) { } } +func TestPos7(t *testing.T) { + testArgs1 := []string{"pos", "beep"} + parser := NewParser("pos", "") + + strval := parser.SelectorPositional([]string{"beep"}, &Options{Help: "wow"}) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != "beep" { + t.Error("Strval did not match expected") + } +} + func TestPosErr1(t *testing.T) { errArgs1 := []string{"pos", "subcommand1"} parser := NewParser("pos", "") From c62e945af2facfa82a11120099ea98d9bd6681ce Mon Sep 17 00:00:00 2001 From: Alexey Kamenskiy Date: Wed, 3 Aug 2022 22:08:48 +1000 Subject: [PATCH 08/12] Add TestPos8 --- argparse_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/argparse_test.go b/argparse_test.go index 4bb0c6e..d3cbe8a 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -2956,6 +2956,46 @@ func TestPos7(t *testing.T) { } } +func TestPos8(t *testing.T) { + testArgs1 := []string{"pos", "cmd1", "cmd2", "progPos", "cmd1pos1", "-s", "some string", "cmd1pos2", "cmd2pos1"} + parser := NewParser("pos", "") + + cmd1 := parser.NewCommand("cmd1", "") + cmd2 := cmd1.NewCommand("cmd2", "") + + cmd2pos1 := cmd2.StringPositional(nil) + progPos := parser.StringPositional(nil) + cmd1pos1 := cmd1.StringPositional(nil) + strval := cmd1.String("s", "str", nil) + cmd1pos2 := cmd1.StringPositional(nil) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } + + if !cmd1.Happened() { + t.Errorf("cmd1 not happened") + } + if !cmd2.Happened() { + t.Errorf("cmd2 not happened") + } + if *strval != "some string" { + t.Errorf(`*strval expected "some string", but got "%s"`, *strval) + } + if *progPos != "progPos" { + t.Errorf(`*progPos expected "progPos", but got "%s"`, *strval) + } + if *cmd1pos1 != "cmd1pos1" { + t.Errorf(`*cmd1pos1 expected "cmd1pos1", but got "%s"`, *strval) + } + if *cmd1pos2 != "cmd1pos2" { + t.Errorf(`*cmd1pos2 expected "cmd1pos1", but got "%s"`, *strval) + } + if *cmd2pos1 != "cmd2pos1" { + t.Errorf(`*cmd2pos1 expected "cmd2pos1", but got "%s"`, *strval) + } +} + func TestPosErr1(t *testing.T) { errArgs1 := []string{"pos", "subcommand1"} parser := NewParser("pos", "") From 1e2e679fbe85102cfc25bde2a78dd9310086eea3 Mon Sep 17 00:00:00 2001 From: vsachs Date: Wed, 3 Aug 2022 09:54:57 -0700 Subject: [PATCH 09/12] Add tests for parse ordering issues --- argparse_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/argparse_test.go b/argparse_test.go index d3cbe8a..9e63215 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -2963,6 +2963,13 @@ func TestPos8(t *testing.T) { cmd1 := parser.NewCommand("cmd1", "") cmd2 := cmd1.NewCommand("cmd2", "") + // The precedence of commands is playing a role here. + // We are parsing cmd2's positionals first from left to right. + // Consequently: + // cmd2.cmd2pos1 = progPos + // cmd1.cmd1pos1 = cmd1pos1 + // cmd1.cmd1pos2 = cmd1pos2 + // parser.progPos = cmd2pos1 cmd2pos1 := cmd2.StringPositional(nil) progPos := parser.StringPositional(nil) cmd1pos1 := cmd1.StringPositional(nil) @@ -2983,16 +2990,70 @@ func TestPos8(t *testing.T) { t.Errorf(`*strval expected "some string", but got "%s"`, *strval) } if *progPos != "progPos" { - t.Errorf(`*progPos expected "progPos", but got "%s"`, *strval) + t.Errorf(`*progPos expected "progPos", but got "%s"`, *progPos) } if *cmd1pos1 != "cmd1pos1" { - t.Errorf(`*cmd1pos1 expected "cmd1pos1", but got "%s"`, *strval) + t.Errorf(`*cmd1pos1 expected "cmd1pos1", but got "%s"`, *cmd1pos1) } if *cmd1pos2 != "cmd1pos2" { - t.Errorf(`*cmd1pos2 expected "cmd1pos1", but got "%s"`, *strval) + t.Errorf(`*cmd1pos2 expected "cmd1pos1", but got "%s"`, *cmd1pos2) } if *cmd2pos1 != "cmd2pos1" { - t.Errorf(`*cmd2pos1 expected "cmd2pos1", but got "%s"`, *strval) + t.Errorf(`*cmd2pos1 expected "cmd2pos1", but got "%s"`, *cmd2pos1) + } +} + +func TestPos9(t *testing.T) { + testArgs1 := []string{"pos", "cmd1", "cmd2", "progPos", "cmd1pos1", "-s", "some string", "cmd1pos2", "cmd2pos1"} + parser := NewParser("pos", "") + + cmd1 := parser.NewCommand("cmd1", "") + cmd2 := cmd1.NewCommand("cmd2", "") + + // The precedence of commands is playing a role here. + // We are parsing cmd2's positionals first from left to right. + // Consequently: + // cmd2.cmd2pos1 = progPos + // cmd1.cmd1pos1 = cmd1pos1 + // cmd1.cmd1pos2 = cmd1pos2 + // parser.progPos = cmd2pos1 + cmd2pos1 := cmd2.StringPositional(nil) + progPos := parser.StringPositional(nil) + cmd1pos1 := cmd1.StringPositional(nil) + cmd1pos2 := cmd1.StringPositional(nil) + // Altering the add order of cmd1pos2 versus strval + // changes how the parsing occurs since we parse linearly based on order of addition. + // If strval comes first it cleanly consumes "some string" + // but if cmd1pos2 comes first it skips "-s" then consumes "some string". + // This problem can be circumvented if "-s" has no default + // but it's unsolvably ambiguous if "-s" has a default. + // In effect users must employ `=` as in '-s="some string"'. + strval := cmd1.String("s", "str", nil) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } + + if !cmd1.Happened() { + t.Errorf("cmd1 not happened") + } + if !cmd2.Happened() { + t.Errorf("cmd2 not happened") + } + if *strval != "some string" { + t.Errorf(`*strval expected "some string", but got "%s"`, *strval) + } + if *progPos != "progPos" { + t.Errorf(`*progPos expected "progPos", but got "%s"`, *progPos) + } + if *cmd1pos1 != "cmd1pos1" { + t.Errorf(`*cmd1pos1 expected "cmd1pos1", but got "%s"`, *cmd1pos1) + } + if *cmd1pos2 != "cmd1pos2" { + t.Errorf(`*cmd1pos2 expected "cmd1pos1", but got "%s"`, *cmd1pos2) + } + if *cmd2pos1 != "cmd2pos1" { + t.Errorf(`*cmd2pos1 expected "cmd2pos1", but got "%s"`, *cmd2pos1) } } From c1c45f5d773d646e56cccdf86308fa1281ecccab Mon Sep 17 00:00:00 2001 From: vsachs Date: Wed, 3 Aug 2022 11:17:04 -0700 Subject: [PATCH 10/12] Utilize two-stage parsing with positionals subsequent to flags --- argparse_test.go | 10 +++++----- command.go | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/argparse_test.go b/argparse_test.go index 9e63215..9141a21 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -3087,10 +3087,10 @@ func TestCommandSubcommandPositionals(t *testing.T) { testArgs7 := []string{"pos", "subcommand2", "-i=1", "abc"} testArgs8 := []string{"pos", "subcommand2", "--integer=1", "abc"} testArgs9 := []string{"pos", "subcommand3", "second"} + testArgs10 := []string{"pos", "subcommand2", "-i", "1", "abc"} // Error cases errArgs1 := []string{"pos", "subcommand2", "-i", "1"} - errArgs2 := []string{"pos", "subcommand2", "-i", "1", "abc"} - errArgs3 := []string{"pos", "subcommand3", "abc"} + errArgs2 := []string{"pos", "subcommand3", "abc"} newParser := func() *Parser { parser := NewParser("pos", "") @@ -3131,6 +3131,9 @@ func TestCommandSubcommandPositionals(t *testing.T) { if err := newParser().Parse(testArgs9); err != nil { t.Error(err.Error()) } + if err := newParser().Parse(testArgs10); err != nil { + t.Error(err.Error()) + } if err := newParser().Parse(errArgs1); err == nil { t.Error("Expected error") @@ -3138,7 +3141,4 @@ func TestCommandSubcommandPositionals(t *testing.T) { if err := newParser().Parse(errArgs2); err == nil { t.Error("Expected error") } - if err := newParser().Parse(errArgs3); err == nil { - t.Error("Expected error") - } } diff --git a/command.go b/command.go index 96e5e0a..9d894a0 100644 --- a/command.go +++ b/command.go @@ -91,22 +91,46 @@ func (o *Command) parseSubCommands(args *[]string) error { return nil } +// All flags must have been parsed and reduced prior to calling this +// This will cause positionals to consume any remainig values, +// whether they have dashes or equals signs or whatever. +func (o *Command) parsePositionals(inputArgs *[]string) error { + for _, oarg := range o.args { + // Two-stage parsing, this is the second stage + if !oarg.GetPositional() || oarg.parsed { + continue + } + for j := 0; j < len(*inputArgs); j++ { + arg := (*inputArgs)[j] + if arg == "" { + continue + } + if err := oarg.parsePositional(arg); err != nil { + return err + } + oarg.reduce(j, inputArgs) + break // Positionals can only occur once + } + // Positionals are implicitly required, if unsatisfied error out + if oarg.opts.Required && !oarg.parsed { + return fmt.Errorf("[%s] is required", oarg.name()) + } + } + return nil +} + //parseArguments - Parses arguments func (o *Command) parseArguments(inputArgs *[]string) error { // Iterate over the args for _, oarg := range o.args { + if oarg.GetPositional() { // Two-stage parsing, this is the first stage + continue + } for j := 0; j < len(*inputArgs); j++ { arg := (*inputArgs)[j] if arg == "" { continue } - if !strings.HasPrefix(arg, "-") && oarg.GetPositional() { - if err := oarg.parsePositional(arg); err != nil { - return err - } - oarg.reduce(j, inputArgs) - break // Positionals can only occur once - } if strings.Contains(arg, "=") { splitInd := strings.LastIndex(arg, "=") equalArg := []string{arg[:splitInd], arg[splitInd+1:]} @@ -153,7 +177,7 @@ func (o *Command) parseArguments(inputArgs *[]string) error { } } } - return nil + return o.parsePositionals(inputArgs) } // Will parse provided list of arguments From f965350764812a8335ca15919ddf0ff1e4154e1f Mon Sep 17 00:00:00 2001 From: vsachs Date: Fri, 5 Aug 2022 11:38:32 -0700 Subject: [PATCH 11/12] Implement breadth-first positional parsing. Enable positional Option.Default. Expose argument.Parsed --- README.md | 27 ++++++++----- argparse.go | 42 ++++++++++--------- argparse_test.go | 102 ++++++++++++++++++++++++++++++----------------- argument.go | 7 +++- command.go | 31 +++++++++----- 5 files changed, 133 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 88b3f02..1bc8ee2 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,12 @@ var myString *string = parser.String("s", "string", ...) Positional arguments can be used like this `$ progname value1`. See [Basic Option Structure](#basic-option-structure) and [Positionals](#positionals). ```go -var myString *string = parser.String("", "posarg", &Options{Positional: true}) +var myString *string = parser.StringPositional(nil) +var myString *string = parser.FilePositional(nil) +var myString *string = parser.FloatPositional(nil) +var myString *string = parser.IntPositional(nil) +var myString *string = parser.SelectorPositional([]string{"a", "b"}, nil) +var myString1 *string = parser.StringPositional(Options{Default: "beep"}) ``` Selector works same as a string, except that it will only allow specific values. @@ -146,11 +151,12 @@ If a command has `Positional` arguments and sub-commands then sub-commands take Since parser inherits from command, every command supports exactly same options as parser itself, thus allowing to add arguments specific to that command or more global arguments added on parser itself! -You can also dynamically retrieve argument values: +You can also dynamically retrieve argument values and if they were parsed: ``` var myInteger *int = parser.Int("i", "integer", ...) parser.Parse() fmt.Printf("%d", *parser.GetArgs()[0].GetResult().(*int)) +fmt.Printf("%v", *parser.GetArgs()[0].GetParsed()) ``` #### Basic Option Structure @@ -162,7 +168,6 @@ type Options struct { Validate func(args []string) error Help string Default interface{} - Positional bool } ``` @@ -170,7 +175,6 @@ You can set `Required` to let it know if it should ask for arguments. Or you can set `Validate` as a lambda function to make it know while value is valid. Or you can set `Help` for your beautiful help document. Or you can set `Default` will set the default value if user does not provide a value. -You can set `Positional` to indicate an arg is required in a specific position but shouldn't include "--name". Positionals are required in the order they are added. Flags can precede or follow positionals. Example: ``` @@ -195,13 +199,14 @@ There are a few caveats (or more like design choices) to know about: * Any arguments that left un-parsed will be regarded as error ##### Positionals -* `Positional` has a set of effects and conditions: - * It will always set Required=True and Default=nil - * It will ignore the shortname - * It will fail (and app will panic) to add the argument if it is one of: - * Flag - * StringList, IntList, FloatList, FileList - +* `Positional` args have a set of effects and conditions: + * Always parsed after subcommands and non-positional args + * Always set Required=False + * Default is only used if the command or subcommand owning the arg `Happened` + * Parsed in Command root->leaf left->right order (breadth-first) + * Top level cmd consumes as many positionals as it can, from left to right + * Then in a descendeding loop for any command which `Happened` it repeats + * Positionals which are not satisfied (due to lack of input args) are not errors #### Contributing diff --git a/argparse.go b/argparse.go index 231071a..cb2cf28 100644 --- a/argparse.go +++ b/argparse.go @@ -86,9 +86,12 @@ type Parser struct { // Possible fields are: // // Options.positional - tells Parser that the argument is positional (implies Required). Set to true by using *Positional functions. -// Positional arguments do not require the flag name to precede them and must come in a specific order. -// Positional sets Required=true, Default=nil, Shortname="" -// Existence of a positional means that any flags preceding the positional must use `=` to pair with their value. +// Positional arguments must not have arg name preceding them and must come in a specific order. +// Positionals are parsed breadth-first (left->right from Command tree root to leaf) +// Positional sets Shortname="", ignores Required +// Positionals which are not satisfied will be nil but no error will be thrown +// Defaults are only set for unparsed positionals on commands which happened +// Use arg.GetParsed() to detect if arg was satisfied or not // // Options.Required - tells Parser that this argument is required to be provided. // useful when specific Command requires some data provided. @@ -273,10 +276,10 @@ func (o *Command) String(short string, long string, opts *Options) *string { // See func String documentation func (o *Command) StringPositional(opts *Options) *string { if opts == nil { - opts = &Options{positional: true} - } else { - opts.positional = true + opts = &Options{} } + opts.positional = true + // We supply a long name for documentation and internal logic name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) return o.String("", name, opts) @@ -309,10 +312,10 @@ func (o *Command) Int(short string, long string, opts *Options) *int { // See func Int documentation func (o *Command) IntPositional(opts *Options) *int { if opts == nil { - opts = &Options{positional: true} - } else { - opts.positional = true + opts = &Options{} } + opts.positional = true + // We supply a long name for documentation and internal logic name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) return o.Int("", name, opts) @@ -345,10 +348,10 @@ func (o *Command) Float(short string, long string, opts *Options) *float64 { // See func Float documentation func (o *Command) FloatPositional(opts *Options) *float64 { if opts == nil { - opts = &Options{positional: true} - } else { - opts.positional = true + opts = &Options{} } + opts.positional = true + // We supply a long name for documentation and internal logic name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) return o.Float("", name, opts) @@ -386,10 +389,10 @@ func (o *Command) File(short string, long string, flag int, perm os.FileMode, op // See func File documentation func (o *Command) FilePositional(flag int, perm os.FileMode, opts *Options) *os.File { if opts == nil { - opts = &Options{positional: true} - } else { - opts.positional = true + opts = &Options{} } + opts.positional = true + // We supply a long name for documentation and internal logic name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) return o.File("", name, flag, perm, opts) @@ -532,10 +535,10 @@ func (o *Command) Selector(short string, long string, options []string, opts *Op // See func Selector documentation func (o *Command) SelectorPositional(allowed []string, opts *Options) *string { if opts == nil { - opts = &Options{positional: true} - } else { - opts.positional = true + opts = &Options{} } + opts.positional = true + // We supply a long name for documentation and internal logic name := fmt.Sprintf(positionalArgName, o.name, len(o.args)) return o.Selector("", name, allowed, opts) @@ -767,6 +770,9 @@ func (o *Parser) Parse(args []string) error { copy(subargs, args) result := o.parse(&subargs) + if result == nil { + result = o.parsePositionals(&subargs) + } unparsed := make([]string, 0) for _, v := range subargs { if v != "" { diff --git a/argparse_test.go b/argparse_test.go index 9141a21..e86aa65 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -2780,17 +2780,33 @@ func TestCommandPositionalOptions(t *testing.T) { } } -func TestCommandPositionalErr(t *testing.T) { - errArgs1 := []string{"pos"} +func TestCommandPositionalUnsatisfied(t *testing.T) { + errArgs1 := []string{"pos", "--test1"} parser := NewParser("pos", "") strval := parser.StringPositional(nil) + flag1 := parser.Flag("", "test1", nil) - if err := parser.Parse(errArgs1); err == nil { - t.Errorf("Positional required") - } else if err.Error() != "[_positionalArg_pos_1] is required" { + if err := parser.Parse(errArgs1); err != nil { t.Error(err.Error()) } else if *strval != "" { t.Errorf("Strval nonempty") + } else if parser.GetArgs()[0].GetParsed() { + t.Errorf("Strval was parsed") + } else if *flag1 != true { + t.Errorf("flag not set") + } +} + +func TestCommandPositionalUnsatisfiedDefault(t *testing.T) { + errArgs1 := []string{"pos"} + parser := NewParser("pos", "") + defval := "defaultation" + strval := parser.StringPositional(&Options{Default: defval}) + + if err := parser.Parse(errArgs1); err != nil { + t.Error(err.Error()) + } else if *strval != defval { + t.Errorf("Strval (%s) != (%s)", *strval, defval) } } @@ -2899,8 +2915,9 @@ func TestPos4(t *testing.T) { com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) - if err := parser.Parse(testArgs1); err != nil && - err.Error() != "[sub]Command required" { + if err := parser.Parse(testArgs1); err == nil { + t.Error("Error expected") + } else if err.Error() != "[sub]Command required" { t.Error(err.Error()) } else if *strval != "" { t.Error("Strval did not match expected") @@ -2964,12 +2981,7 @@ func TestPos8(t *testing.T) { cmd2 := cmd1.NewCommand("cmd2", "") // The precedence of commands is playing a role here. - // We are parsing cmd2's positionals first from left to right. - // Consequently: - // cmd2.cmd2pos1 = progPos - // cmd1.cmd1pos1 = cmd1pos1 - // cmd1.cmd1pos2 = cmd1pos2 - // parser.progPos = cmd2pos1 + // We should be parsing in root->leaf, left->right order cmd2pos1 := cmd2.StringPositional(nil) progPos := parser.StringPositional(nil) cmd1pos1 := cmd1.StringPositional(nil) @@ -3010,26 +3022,14 @@ func TestPos9(t *testing.T) { cmd1 := parser.NewCommand("cmd1", "") cmd2 := cmd1.NewCommand("cmd2", "") - // The precedence of commands is playing a role here. - // We are parsing cmd2's positionals first from left to right. - // Consequently: - // cmd2.cmd2pos1 = progPos - // cmd1.cmd1pos1 = cmd1pos1 - // cmd1.cmd1pos2 = cmd1pos2 - // parser.progPos = cmd2pos1 + // The precedence of commands controls which values parsed to where + // We should be parsing in root->leaf, left->right order cmd2pos1 := cmd2.StringPositional(nil) progPos := parser.StringPositional(nil) cmd1pos1 := cmd1.StringPositional(nil) cmd1pos2 := cmd1.StringPositional(nil) - // Altering the add order of cmd1pos2 versus strval - // changes how the parsing occurs since we parse linearly based on order of addition. - // If strval comes first it cleanly consumes "some string" - // but if cmd1pos2 comes first it skips "-s" then consumes "some string". - // This problem can be circumvented if "-s" has no default - // but it's unsolvably ambiguous if "-s" has a default. - // In effect users must employ `=` as in '-s="some string"'. - strval := cmd1.String("s", "str", nil) + strval := cmd1.String("s", "str", nil) if err := parser.Parse(testArgs1); err != nil { t.Error(err.Error()) } @@ -3057,7 +3057,7 @@ func TestPos9(t *testing.T) { } } -func TestPosErr1(t *testing.T) { +func TestSubcommandParsed(t *testing.T) { errArgs1 := []string{"pos", "subcommand1"} parser := NewParser("pos", "") @@ -3065,10 +3065,10 @@ func TestPosErr1(t *testing.T) { com1 := parser.NewCommand("subcommand1", "beep") intval := com1.Int("i", "integer", nil) - if err := parser.Parse(errArgs1); err == nil { - t.Error("Subcommand should be required") - } else if err.Error() != "[_positionalArg_pos_1] is required" { + if err := parser.Parse(errArgs1); err != nil { t.Error(err.Error()) + } else if !com1.Happened() { + t.Error("Subcommand should have happened") } else if *strval != "" { t.Error("strval incorrectly defaulted:" + *strval) } else if *intval != 0 { @@ -3076,6 +3076,34 @@ func TestPosErr1(t *testing.T) { } } +func TestSubcommandMultiarg(t *testing.T) { + errArgs1 := []string{"ma0", "ma1", "ma2", "strval1", "2.0", "5", "1.0"} + parser := NewParser("ma0", "") + + strval := parser.StringPositional(nil) + floatval1 := parser.FloatPositional(nil) + com1 := parser.NewCommand("ma1", "beep") + intval := com1.IntPositional(nil) + com2 := com1.NewCommand("ma2", "beep") + floatval2 := com2.FloatPositional(nil) + + if err := parser.Parse(errArgs1); err != nil { + t.Error(err.Error()) + } else if !com1.Happened() { + t.Error("ma1 should have happened") + } else if !com2.Happened() { + t.Error("ma2 should have happened") + } else if *strval != "strval1" { + t.Error("strval did not match expected") + } else if *floatval1 != 2.0 { + t.Error("strval did not match expected") + } else if *intval != 5 { + t.Errorf("intval did not match expected: %v", *intval) + } else if *floatval2 != 1.0 { + t.Error("floatval did not match expected") + } +} + func TestCommandSubcommandPositionals(t *testing.T) { testArgs1 := []string{"pos", "subcommand2", "efg"} testArgs2 := []string{"pos", "subcommand1"} @@ -3088,9 +3116,9 @@ func TestCommandSubcommandPositionals(t *testing.T) { testArgs8 := []string{"pos", "subcommand2", "--integer=1", "abc"} testArgs9 := []string{"pos", "subcommand3", "second"} testArgs10 := []string{"pos", "subcommand2", "-i", "1", "abc"} + testArgs11 := []string{"pos", "subcommand2", "-i", "1"} // Error cases - errArgs1 := []string{"pos", "subcommand2", "-i", "1"} - errArgs2 := []string{"pos", "subcommand3", "abc"} + errArgs1 := []string{"pos", "subcommand3", "abc"} newParser := func() *Parser { parser := NewParser("pos", "") @@ -3134,11 +3162,11 @@ func TestCommandSubcommandPositionals(t *testing.T) { if err := newParser().Parse(testArgs10); err != nil { t.Error(err.Error()) } + if err := newParser().Parse(testArgs11); err != nil { + t.Error(err.Error()) + } if err := newParser().Parse(errArgs1); err == nil { t.Error("Expected error") } - if err := newParser().Parse(errArgs2); err == nil { - t.Error("Expected error") - } } diff --git a/argument.go b/argument.go index cc4634d..ac5bb19 100644 --- a/argument.go +++ b/argument.go @@ -14,7 +14,7 @@ type arg struct { sname string // Short name (in Parser will start with "-" lname string // Long name (in Parser will start with "--" size int // Size defines how many args after match will need to be consumed - unique bool // Specifies whether flag should be present only ones + unique bool // Specifies whether flag should be present only once parsed bool // Specifies whether flag has been parsed already fileFlag int // File mode to open file with filePerm os.FileMode // File permissions to set a file @@ -48,6 +48,7 @@ type Arg interface { GetLname() string GetResult() interface{} GetPositional() bool + GetParsed() bool } func (o arg) GetPositional() bool { @@ -57,6 +58,10 @@ func (o arg) GetPositional() bool { return false } +func (o arg) GetParsed() bool { + return o.parsed +} + func (o arg) GetOpts() *Options { return o.opts } diff --git a/command.go b/command.go index 9d894a0..f87d411 100644 --- a/command.go +++ b/command.go @@ -58,8 +58,7 @@ func (o *Command) addArg(a *arg) error { return fmt.Errorf("argument type cannot be positional") } a.sname = "" - a.opts.Required = true - a.opts.Default = nil + a.opts.Required = false a.size = 1 // We could allow other sizes in the future } o.args = append(o.args, a) @@ -91,13 +90,16 @@ func (o *Command) parseSubCommands(args *[]string) error { return nil } +// Breadth-first parse style for positionals +// Each command proceeds left to right consuming as many +// positionals as it needs before beginning sub-command parsing // All flags must have been parsed and reduced prior to calling this -// This will cause positionals to consume any remainig values, -// whether they have dashes or equals signs or whatever. +// Positionals will consume any remaining values, +// 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 - if !oarg.GetPositional() || oarg.parsed { + if !oarg.GetPositional() { continue } for j := 0; j < len(*inputArgs); j++ { @@ -111,9 +113,17 @@ func (o *Command) parsePositionals(inputArgs *[]string) error { oarg.reduce(j, inputArgs) break // Positionals can only occur once } - // Positionals are implicitly required, if unsatisfied error out - if oarg.opts.Required && !oarg.parsed { - return fmt.Errorf("[%s] is required", oarg.name()) + // positional was unsatisfiable, use the default + if !oarg.parsed { + err := oarg.setDefault() + if err != nil { + return err + } + } + } + for _, c := range o.commands { + if c.happened { // presumption of only one sub-command happening + return c.parsePositionals(inputArgs) } } return nil @@ -177,11 +187,14 @@ func (o *Command) parseArguments(inputArgs *[]string) error { } } } - return o.parsePositionals(inputArgs) + return nil } // 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. func (o *Command) parse(args *[]string) error { // If already been parsed do nothing if o.parsed { From c2b22b8ddc8cb899be54f03b4c378f5f59167726 Mon Sep 17 00:00:00 2001 From: Alexey Kamenskiy Date: Sun, 7 Aug 2022 12:36:38 +1000 Subject: [PATCH 12/12] Add tests for empty positional and for positional defaults --- argparse_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/argparse_test.go b/argparse_test.go index e86aa65..24c265e 100644 --- a/argparse_test.go +++ b/argparse_test.go @@ -3170,3 +3170,64 @@ func TestCommandSubcommandPositionals(t *testing.T) { t.Error("Expected error") } } + +func TestPositionalsLessArgumentsThanPositionals(t *testing.T) { + testArgs1 := []string{"pos", "cmd1", "progPos", "cmd1pos1"} + parser := NewParser("pos", "") + + cmd1 := parser.NewCommand("cmd1", "") + + // The precedence of commands is playing a role here. + // We should be parsing in root->leaf, left->right order + progPos := parser.StringPositional(nil) + cmd1pos1 := cmd1.StringPositional(nil) + cmd1pos2 := cmd1.StringPositional(nil) + strval := cmd1.String("s", "str", nil) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } + + if !cmd1.Happened() { + t.Errorf("cmd1 not happened") + } + if *strval != "" { + t.Errorf(`*strval expected "", but got "%s"`, *strval) + } + if *progPos != "progPos" { + t.Errorf(`*progPos expected "progPos", but got "%s"`, *progPos) + } + if *cmd1pos1 != "cmd1pos1" { + t.Errorf(`*cmd1pos1 expected "cmd1pos1", but got "%s"`, *cmd1pos1) + } + if *cmd1pos2 != "" { + t.Errorf(`*cmd1pos2 expected "", but got "%s"`, *cmd1pos2) + } +} + +func TestPositionalDefaults(t *testing.T) { + testArgs1 := []string{"pos"} + parser := NewParser("pos", "") + + pos1 := parser.StringPositional(&Options{Default: "pos1"}) + pos2 := parser.IntPositional(&Options{Default: 2}) + pos3 := parser.FloatPositional(&Options{Default: 3.3}) + pos4 := parser.SelectorPositional([]string{"notallowed", "pos4"}, &Options{Default: "pos4"}) + + if err := parser.Parse(testArgs1); err != nil { + t.Error(err.Error()) + } + + if *pos1 != "pos1" { + t.Errorf(`*pos1 expected "pos1", but got "%s"`, *pos1) + } + if *pos2 != 2 { + t.Errorf(`*pos2 expected "2", but got "%d"`, *pos2) + } + if *pos3 != 3.3 { + t.Errorf(`*pos3 expected "3.3", but got "%f"`, *pos3) + } + if *pos4 != "pos4" { + t.Errorf(`*pos4 expected "pos4", but got "%s"`, *pos4) + } +}