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
23 changes: 21 additions & 2 deletions README.md
Expand Up @@ -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
Expand Down Expand Up @@ -134,6 +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 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!

Expand All @@ -153,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",
})
Expand All @@ -183,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

Expand Down
74 changes: 74 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,11 @@ 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 do not require the flag name to precede them and must come in a specific order.
// Positional sets Required=true, Default=nil, Shortname=""
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, interesting, while Default == nil makes sense (positional either provided or it is not), I am not so sure about Required == true.

Assuming for a sub-command tree we defined 5 positionals: progname cmd1 cmd2 prognamePositional1 prognamePositional2 cmd1Positional1 cmd1Positional2 cmd2Positional, and input provided on CLI progname cmd1 cmd2 somestr1 somestr2 somestr3, the only way we can match positional arguments is in the order they provided. Hence:

  • prognamePositional1 == somestr1
  • prognamePositional2 == somestr2
  • cmd1Positional1 == somestr3
  • cmd1Positional2 == nil
  • cmd2Positional == nil

Which would make it possible to define "optional" positionals, as in -- if it was not provided, it is not set to any value, meantime any that provided will be matched to their corresponding positional in exactly same order (that is in example above prognamePositional1 should always be matched to somestr1

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

progname cmd1 cmd2 prognamePositional1 prognamePositional2 cmd1Positional1 cmd1Positional2 cmd2Positional
----->
progname cmd1 cmd2 somestr1 somestr2 somestr3
----->
prognamePositional1 == somestr1
prognamePositional2 == somestr2
cmd1Positional1 == somestr3
cmd1Positional2 == nil
cmd2Positional == nil

I believe I can allow this relatively easily but it's going to open a can of worms. What if the only optional positionals are actually prognamePositional1 and prognamePositional2? We can't distinguish which positionals the user is trying to fill (unless we actually allow naming of the positionals on the CLI, which... basically defeats the whole point), so we will still end up with the same result above except that the last two are required and nil, giving us an error. Effectively they are all required in this scenario.

Do we want to require that any optional positionals be at the tail end of added arguments? If so this has the implication:

  • That all sub-commands of any command with optionals can now only add optional positionals.

// 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.
//
Expand All @@ -99,6 +109,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 +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 {
vsachs marked this conversation as resolved.
Show resolved Hide resolved
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 +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.
Expand All @@ -305,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
Expand Down Expand Up @@ -334,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.
Expand Down Expand Up @@ -468,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.
Expand Down Expand Up @@ -700,6 +773,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, " "))
}
Expand Down