Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of positional arguments #102

Merged
merged 12 commits into from Aug 8, 2022
30 changes: 27 additions & 3 deletions README.md
Expand Up @@ -66,6 +66,17 @@ 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.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.
For example like this `$ progname --debug-level WARN`
```go
Expand Down Expand Up @@ -134,14 +145,18 @@ 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 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!

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 @@ -156,7 +171,7 @@ type Options struct {
}
```

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.
Expand All @@ -165,7 +180,7 @@ Example:
```
dirpath := parser.String("d", "dirpath",
&argparse.Options{
Require: false,
Required: false,
Help: "the input files' folder path",
Default: "input",
})
Expand All @@ -183,6 +198,15 @@ 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` 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
80 changes: 80 additions & 0 deletions argparse.go
Expand Up @@ -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

Expand Down Expand Up @@ -80,6 +85,14 @@ 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). Set to true by using *Positional functions.
// 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 All @@ -99,6 +112,9 @@ type Options struct {
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
Expand Down Expand Up @@ -257,6 +273,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{}
}
opts.positional = true

// We supply a long name for documentation and internal logic
name := fmt.Sprintf(positionalArgName, o.name, len(o.args))
vsachs marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand All @@ -281,6 +309,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{}
}
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.
Expand All @@ -305,6 +345,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{}
}
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
Expand Down Expand Up @@ -334,6 +386,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{}
}
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.
Expand Down Expand Up @@ -468,6 +532,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{}
}
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.
Expand Down Expand Up @@ -694,12 +770,16 @@ 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 != "" {
unparsed = append(unparsed, v)
}
}

if result == nil && len(unparsed) > 0 {
return errors.New("unknown arguments " + strings.Join(unparsed, " "))
}
Expand Down