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 62871db
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 55 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
77 changes: 61 additions & 16 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 @@ -3057,25 +3074,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 +3133,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 +3179,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
31 changes: 22 additions & 9 deletions command.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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++ {
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 62871db

Please sign in to comment.