From f965350764812a8335ca15919ddf0ff1e4154e1f Mon Sep 17 00:00:00 2001 From: vsachs Date: Fri, 5 Aug 2022 11:38:32 -0700 Subject: [PATCH] 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 {