Skip to content

Commit

Permalink
Implement breadth-first positional parsing. Enable positional Option.…
Browse files Browse the repository at this point in the history
…Default. Expose argument.Parsed
  • Loading branch information
vsachs committed Aug 5, 2022
1 parent c1c45f5 commit f965350
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 76 deletions.
27 changes: 16 additions & 11 deletions README.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -162,15 +168,13 @@ 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.
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:
```
Expand All @@ -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

Expand Down
42 changes: 24 additions & 18 deletions argparse.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 != "" {
Expand Down
102 changes: 65 additions & 37 deletions argparse_test.go
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -3057,25 +3057,53 @@ func TestPos9(t *testing.T) {
}
}

func TestPosErr1(t *testing.T) {
func TestSubcommandParsed(t *testing.T) {
errArgs1 := []string{"pos", "subcommand1"}
parser := NewParser("pos", "")

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() != "[_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 {
t.Error("intval did not match expected")
}
}

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"}
Expand All @@ -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", "")
Expand Down Expand Up @@ -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")
}
}
7 changes: 6 additions & 1 deletion argument.go
Expand Up @@ -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
Expand Down Expand Up @@ -48,6 +48,7 @@ type Arg interface {
GetLname() string
GetResult() interface{}
GetPositional() bool
GetParsed() bool
}

func (o arg) GetPositional() bool {
Expand All @@ -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
}
Expand Down

0 comments on commit f965350

Please sign in to comment.