Metadata-Version: 2.1
Name: quickparse
Version: 0.9.1
Summary: Simple command line argument parser for Python
Home-page: https://github.com/silkyanteater/quickparse
License: MIT
Keywords: command line,argument,parser
Author: silkyanteater
Author-email: cyclopesrufus@gmail.com
Requires-Python: >=3.5,<4.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Project-URL: Repository, https://github.com/silkyanteater/quickparse
Description-Content-Type: text/markdown

# QuickParse
Simple command line argument parser for Python

## Example

`list_things.py`:
```python
from quickparse import QuickParse

def list_things(a_list, quickparse):
    if quickparse.numeric:
        if isinstance(quickparse.numeric, tuple):
            print(', '.join(map(str, a_list[:quickparse.numeric[-1]])))
        else:
            print(', '.join(map(str, a_list[:quickparse.numeric])))
    else:
        print("How many items? Give a numeric value like '-3'")

commands_config = {
    'ls': list_things,
    '': lambda: print("Command is missing, use 'ls'"),
}

things = ['apple', 'banana', 'blueberry', 'orange', 'pear', 'pineapple']

QuickParse(commands_config).execute(things)
```

Run it:
```sh
$ python list_things.py ls -5
apple, banana, blueberry, orange, pear
```

The way it works:
- `commands_config` tells QuickParse to look for `ls` as a command and call `list_things` on it - when no commands show help
- QuickParse parses arguments as normal while `ls` is privileged as a command
- QuickParse finds `-5` so it adds as `quickparse.numeric = 5` (`quickparse` being the `QuickParse` instance that otherwise would come as `quickparse = QuickParse(commands_config)`)
- QuickParse sees `list_things` being associated to `ls`, so `quickparse.execute(things)` calls it, passing on the arguments of `execute(..)` - one positional argument in this case
- since `list_things` expects a named argument `quickparse`, QuickParse makes sure it passes on the reference to its own instance of `quickparse`
- if there are multiple numeric flags are given all are passed down with `quickparse.numeric` in a tuple

## GNU Argument Syntax implementation with extensions
GNU Argument Syntax: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html

### Extensions
#### Numeric '-' values
```bash
$ my_cmd -12
```
#### Numeric '+' values
```bash
$ my_cmd +12
```
#### Long '-' options - only with explicit config
```bash
$ my_cmd -list
```
By default it becomes `-l -i -s -t`, but adding `QuickParse(options_config = [ ('-list', ) ])` will stop unpacking.
#### Long '+' options by default
```bash
$ my_cmd +list
```
#### Equivalent options - using options_config
```bash
$ my_cmd -l
```
is equivalent to
```bash
$ my_cmd --list
```
if adding `QuickParse(options_config = [ ('-l', '--list') ])`
#### Command-subcommand hierarchy and function bindings - using commands_config
Defining a random sample from `git` looks like this:
```python
commands_config = {
    '': do_show_help,
    'commit': do_commit,
    'log': do_log,
    'stash': {
        '': do_stash,
        'list': do_stash_list,
    }
}

options_config = [
    ('-a', '--all'),
]

QuickParse(commands_config, options_config).execute()
```
Commands are called according to commands_config.  
That is `$ git log -3` calls `do_log`  
`do_log` may look like this:
```python
def do_log(quickparse):
    print(get_log_entries()[:quickparse.numeric])
```
If there is a named argument in `do_log`'s signature called `quickparse`, the instance coming from `QuickParse(commands_config, options_config)` is passed down holding all the results of parsing.  
Parsing happens by using the defaults and applying what `options_config` adds to it.

## Argument Formats
| &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Argument&nbsp;Format&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Example&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Remarks |
| --- | --- | --- |
| `-<number>` | `$ my_cmd -12` | (default) |
| `+<number>` | `$ my_cmd +12` | (default) |
| `-<single_letter>` | `$ my_cmd -x` | (default) |
| `+<single_letter>` | `$ my_cmd +x` | (default) |
| `-<single_letter><value>` | `$ my_cmd -nFoo` | unpacking is the default: -n -F -o<br>`options_config` needs a type entry saying it expects a value (other than bool) |
| `+<single_letter><value>` | `$ my_cmd +nFoo` | unpacking is the default: +n +F +o<br>`options_config` needs a type entry saying it expects a value (other than bool) |
| `-<single_letter>=<value>` | `$ my_cmd -n=Foo` | (default) |
| `+<single_letter>=<value>` | `$ my_cmd +n=Foo` | (default) |
| `-<single_letter> <value>` | `$ my_cmd -n Foo` | `options_config` needs a type entry saying it expects a value (other than bool) |
| `+<single_letter> <value>` | `$ my_cmd +n Foo` | `options_config` needs a type entry saying it expects a value (other than bool) |
| `-<letters>` | `$ my_cmd -abc` | unpacking is the default: -a -b -c<br>if in `options_config` it's taken as `-abc` |
| `+<letters>` | `$ my_cmd +abc` | unpacking is the default: +a +b +c<br>if in `options_config` it's taken as `+abc` |
| `-<letters>=<value>` | `$ my_cmd -name=Foo` | (default) |
| `+<letters>=<value>` | `$ my_cmd +name=Foo` | (default) |
| `--<letters>` | `$ my_cmd --list` | (default) |
| `--<letters>=<value>` | `$ my_cmd --message=Bar` | (default) |
| `--<letters> <value>` | `$ my_cmd --message Bar` | `options_config` needs a type entry saying it expects a value (other than bool) |
| `--` | `$ my_cmd -- --param-anyway` | parameters delimiter<br>(default) |

`<letters>` means [a-zA-Z] and '-'s not in the first place

### An argument like '-a*' gets unpacked if...
- '-a' is not defined to expect a value
- the '*' part has only letters, not '-' or '='

### How to change the interpretation of `-swing`
It can mean (default):  
`-s -w -i -n -g`  
or  
`-s wing` / `-s=wing`  
To acheve the latter make the parser aware that '-s' expects a `str` value:
```python
options_config = [
    ('-s', str),
]
```

### Make the parser aware that an option expects a value after a space
Add type explicitly in `options_config`.  
For just getting as it is add `str`.

### How to define option types
Use build-in types like `int` or `float`, or create a callable that raises exceptions.  
Using `bool` is a special case: parser will not expect a value but explicitly adds an error if one provided.

### How to add empty value to an option
`--option=`
Some commands support '-' as empty value like `curl -C - -O http://domanin.com/`  
To avoid ambiguities this syntax is not supported.  
Use `--option=` instead.

## How to define options
`options_test.py`:
```python
from quickparse import QuickParse

options_config = [
    ('-u', '--utc', '--universal'),
    ('-l', '--long'),
    ('-n', '--name', str),
]

quickparse = QuickParse(options_config=options_config)

print(f'quickparse.options: {quickparse.options}')
print(f'quickparse.errors: {quickparse.errors}')
```

Run it:
```bash
$ python options_test.py
quickparse.options: {}
quickparse.errors: {}

$ python options_test.py -u
quickparse.options: {'-u': True, '--utc': True, '--universal': True}
quickparse.errors: {}

$ python options_test.py -ul
quickparse.options: {'-u': True, '--utc': True, '--universal': True, '-l': True, '--long': True}
quickparse.errors: {}

$ python options_test.py -uln
quickparse.options: {'-u': True, '--utc': True, '--universal': True, '-l': True, '--long': True, '-n': True, '--name': True}
quickparse.errors: {'-n': {'type': 1, 'message': "No value got for '-n/--name' - validator: str"}, '--name': {'type': 1, 'message': "No value got for '-n/--name' - validator: str"}}

$ python options_test.py -ul -nthe_name
quickparse.options: {'-u': True, '--utc': True, '--universal': True, '-l': True, '--long': True, '-n': 'the_name', '--name': 'the_name'}
quickparse.errors: {}

$ python options_test.py -ul -n the_name
quickparse.options: {'-u': True, '--utc': True, '--universal': True, '-l': True, '--long': True, '-n': 'the_name', '--name': 'the_name'}
quickparse.errors: {}

$ python options_test.py -ul -n=the_name
quickparse.options: {'-u': True, '--utc': True, '--universal': True, '-l': True, '--long': True, '-n': 'the_name', '--name': 'the_name'}
quickparse.errors: {}

$ python options_test.py -ul --name the_name
quickparse.options: {'-u': True, '--utc': True, '--universal': True, '-l': True, '--long': True, '--name': 'the_name', '-n': 'the_name'}
quickparse.errors: {}

$ python options_test.py -ul --name=the_name
quickparse.options: {'-u': True, '--utc': True, '--universal': True, '-l': True, '--long': True, '--name': 'the_name', '-n': 'the_name'}
quickparse.errors: {}
```

## Test your command line arguments
`quickparse_test_args.py` (committed in the repo):
```python
from pprint import pformat

from quickparse import QuickParse


def do_show_help():
    print("Executing 'do_show_help'...")

def do_commit():
    print("Executing 'do_commit'...")

def do_log(quickparse):
    print("Executing 'do_log'...")

def do_stash():
    print("Executing 'do_stash'...")

def do_stash_list():
    print("Executing 'do_stash_list'...")

commands_config = {
    '': do_show_help,
    'commit': do_commit,
    'log': do_log,
    'stash': {
        '': do_stash,
        'list': do_stash_list,
    }
}

options_config = [
    ('-m', '--message', str),
    ('-p', '--patch'),
]


quickparse = QuickParse(commands_config, options_config)


print(f'Commands:\n{pformat(quickparse.commands)}')
print(f'Parameters:\n{pformat(quickparse.parameters)}')
print(f'Options:\n{pformat(quickparse.options)}')
print(f'\'-\' numeric argument:\n{pformat(quickparse.numeric)}')
print(f'\'+\' numeric argument:\n{pformat(quickparse.plusnumeric)}')
print(f'Functions to call:\n{pformat(quickparse.to_execute)}')

quickparse.execute()
```

## Error handling

If the parser parameters 'commands_config' or 'options_config' are not valid, ValueError is rased from the underlying AssertionError.  
If the arguments are not compliant with the config (e.g. no value provided for an option that requires one) then no exceptions are raised but an `errors` list is populated on the `QuickParse` object.

See the error object again from `options_test.py`

```bash
$ python options_test.py -uln
quickparse.options: {'-u': True, '--utc': True, '--universal': True, '-l': True, '--long': True, '-n': True, '--name': True}
quickparse.errors: {'-n': {'type': 1, 'message': "No value got for '-n/--name' - validator: str"}, '--name': {'type': 1, 'message': "No value got for '-n/--name' - validator: str"}}
```

`quickparse.errors` dict is about validation of options. These are the types:
```python
ERROR_TYPE_VALIDATION = 0
ERROR_VALUE_NOT_FOUND = 1
ERROR_INCOMPLETE_COMMAND = 2
```

quickparse.has_errors is also available to check if any errors occurred.

## Validation
Well, I still need to elaborate the docs on this but here is a quick example snippet.  
```python
quickparse.validate({
    'parameters': { 'mincount': 1, },
    'options': {
        'mandatory': '--branch',
        'optional': '--stage',
    },
    'numeric': { 'maxcount': 0 },
    'plusnumeric': { 'maxcount': 0 },
})
assert 'parameters.mincount' not in quickparse.errors, f'Add a target'
assert not quickparse.has_errors, '\n'.join(quickparse.error_messages)
```

