Declarative parser for command line interfaces
Project description
Nuclear - binding glue for CLI
nuclear
is a declarative parser for command line interfaces in Python.
It's a binding glue between CLI shell arguments and functions being invoked.
It mostly focuses on building multi level command trees.
nuclear
parses and validates the command line arguments provided by the user when starting a console application.
It then automatically invokes the appropriate action, based on the declared Command-Line Interface rules, injecting all the necessary parameters.
You don't need to write the "glue" code to bind & parse the parameters each time.
This makes writing console aplications simpler and clearer.
Demo
#!/usr/bin/env python3
from nuclear import CliBuilder, argument, flag, parameter, subcommand
CliBuilder().has(
subcommand('hello', run=say_hello).has(
argument('name'),
parameter('repeat', type=int, default=1),
flag('decode', help='Decode name as base64'),
),
subcommand('calculate').has(
subcommand('factorial', run=calculate_factorial,
help='Calculate factorial').has(
argument('n', type=int),
),
subcommand('primes', run=calculate_primes,
help='List prime numbers using Sieve of Eratosthenes').has(
argument('n', type=int, required=False, default=100,
help='maximum number to check'),
),
),
).run()
See demo.py for a complete example or demo-decorator.py (if you want to do the same using decorator-based syntax).
Get it now
pip install nuclear
Table of contents - Our chief weapons are...
- Auto-generated help and usage (
--help
) - Multilevel Sub-commands (e.g.
git remote add ...
syntax) - Flags: supporting both short (
-f
) and long (--force
), combining short flags (-tulpn
), multiple flag occurrences (-vvv
) - Named parameters: supporting both
--name value
and--name=value
, multiple parameter occurrences - Positional arguments (e.g.
git push <origin> <master>
) - Many positional arguments (e.g.
docker run cmd ubuntu </bin/bash -c /script.sh>
) - Key-value dictionaries (e.g.
--config key value
) - Logging with sublog
- Shell Auto-completion (getting most relevant hints on hitting
Tab
) - Custom auto-completers (providers of possible values)
- Parsing data types (int, boolean, time, date, file, etc.)
- CLI Builder
- Custom type validators / parsers
- Errors handling
- Quick start
- How does it work?
- Nuclear vs argparse
- Installation
- CLI Rules cheatsheet
How does it work?
- You define CLI rules for your program in a declarative tree using
CliBuilder
. Rules can bind your functions to be called later. - When running your program in a shell provided with command-line arguments, it starts
.run()
which does the parsing. nuclear
parses and validates all the parameters, flags, sub-commands, positional arguments, etc., and stores them internally.nuclear
finds the most relevant action (starting from the most specific) and invokes it.- When invoking a function,
nuclear
injects all its needed parameters based on the previously defined & parsed values.
You just need to bind the keywords with rules and nuclear
will take care of the rest for you.
Quick start
Let's create a simple command-line application using nuclear
.
Let's assume we already have our fancy functions as follows:
def say_hello(name: str, decode: bool, repeat: int):
if decode:
name = base64.b64decode(name).decode('utf-8')
print(' '.join([f"I'm a {name}!"] * repeat))
def calculate_factorial(n: int):
print(reduce(lambda x, y: x * y, range(1, n + 1)))
def calculate_primes(n: int):
print(sorted(reduce((lambda r, x: r - set(range(x**2, n, x)) if (x in r) else r),
range(2, n), set(range(2, n)))))
and we need a "glue" which binds them with a CLI (Command-Line Interface). We want it to be run with different keywords and parameters provided by user to the terminal shell in a following manner:
./quickstart.py hello NAME --decode --repeat=3
mapped tosay_hello
function,./quickstart.py calculate factorial N
mapped tocalculate_factorial
function,./quickstart.py calculate primes N
mapped tocalculate_primes
function,
We've just identified 2 main commands in a program: hello
and calculate
(which in turn contains 2 subcommands: factorial
& primes
). That forms a tree:
hello
command has one positional argumentNAME
, one boolean flagdecode
and one numerical parameterrepeat
.calculate
command has 2 another subcommands:factorial
subcommand has one positional argumentN
,primes
subcommand has one positional argumentN
,
So our CLI definition may be declared using nuclear
in a following way:
CliBuilder().has(
subcommand('hello', run=say_hello).has(
argument('name'),
parameter('repeat', type=int, default=1),
flag('decode', help='Decode name as base64'),
),
subcommand('calculate').has(
subcommand('factorial', run=calculate_factorial,
help='Calculate factorial').has(
argument('n', type=int),
),
subcommand('primes', run=calculate_primes,
help='List prime numbers using Sieve of Eratosthenes').has(
argument('n', type=int, required=False, default=100,
help='maximum number to check'),
),
),
)
Getting it all together, we've bound our function with a Command-Line Interface:
quickstart.py:
#!/usr/bin/env python3
import base64
from functools import reduce
from nuclear import CliBuilder, argument, flag, parameter, subcommand
def say_hello(name: str, decode: bool, repeat: int):
if decode:
name = base64.b64decode(name).decode('utf-8')
print(' '.join([f"I'm a {name}!"] * repeat))
def calculate_factorial(n: int):
print(reduce(lambda x, y: x * y, range(1, n + 1)))
def calculate_primes(n: int):
print(sorted(reduce((lambda r, x: r - set(range(x**2, n, x)) if (x in r) else r),
range(2, n), set(range(2, n)))))
CliBuilder().has(
subcommand('hello', run=say_hello).has(
argument('name'),
flag('decode', help='Decode name as base64'),
parameter('repeat', type=int, default=1),
),
subcommand('calculate').has(
subcommand('factorial', help='Calculate factorial', run=calculate_factorial).has(
argument('n', type=int),
),
subcommand('primes', help='List prime numbers using Sieve of Eratosthenes', run=calculate_primes).has(
argument('n', type=int, required=False, default=100, help='maximum number to check'),
),
),
).run()
Let's trace what is happening here:
CliBuilder()
builds CLI tree for entire application..has(...)
allows to embed other nested rules inside that builder. ReturnsCliBuilder
itself for further building.subcommand('hello', run=say_hello)
bindshello
command tosay_hello
function. From now, it will be invoked whenhello
command occurrs.subcommand.has(...)
embeds nested subrules on lower level for that subcommand only.argument('name')
declares positional argument. From now, first CLI argument (after binary name and commands) will be recognized asname
variable.flag('decode')
binds--decode
keyword to a flag nameddecode
. So as it may be used later on. Providinghelp
adds description to help screen.parameter('repeat', type=int, default=1)
binds--repeat
keyword to a parameter namedrepeat
, which type isint
and its default value is1
.- Finally, invoking
.run()
does all the magic. It gets system arguments list, starts to process them and invokes most relevant action.
Decorator builder
We can do the same using decorator-based syntax, which binds the functions to the CLI:
cli = CliBuilder()
@cli.add_command('hello')
def say_hello(name: str, decode: bool = False, repeat: int = 1):
"""Say hello to someone"""
if decode:
name = base64.b64decode(name).decode('utf-8')
print(' '.join([f"I'm a {name}!"] * repeat))
@cli.add_command('calculate', 'factorial')
def calculate_factorial(n: int):
"""Calculate factorial"""
print(reduce(lambda x, y: x * y, range(1, n + 1)))
@cli.add_command('calculate', 'primes')
def calculate_primes(n: int = 100):
"""List prime numbers using Sieve of Eratosthenes"""
print(sorted(reduce((lambda r, x: r - set(range(x**2, n, x)) if (x in r) else r),
range(2, n), set(range(2, n)))))
if __name__ == '__main__':
cli.run()
Help / Usage
CliBuilder
has some basic options added by default, e.g. --help
.
Thus, you can check the usage by running application with --help
flag:
foo@bar:~$ ./quickstart.py --help
Usage:
./quickstart.py [COMMAND] [OPTIONS]
Options:
-h, --help [SUBCOMMANDS...] - Display this help and exit
Commands:
hello NAME
calculate factorial N - Calculate factorial
calculate primes [N] - List prime numbers using Sieve of Eratosthenes
Run "./quickstart.py COMMAND --help" for more information on a command.
As prompted, we can check more detailed subcommand helps:
foo@bar:~$ ./quickstart.py hello --help
Usage:
./quickstart.py hello [OPTIONS] NAME
Arguments:
NAME
Options:
--decode - Decode name as base64
--repeat REPEAT - Default: 1
-h, --help [SUBCOMMANDS...] - Display this help and exit
Injecting parameters
Let's invoke say_hello
function on a first run.
Now when we execute our application with required argument provided, we get:
foo@bar:~$ ./quickstart.py hello world
I'm a world!
Note that world
has been recognized as name
argument.
We've binded say_hello
as a default action, so it has been invoked with particular parameters:
say_hello(name='world', decode=False, repeat=1)
- positional argument
name
has been assigned a'world'
value. - flag
decode
was not given, so it'sFalse
by default. - parameter
repeat
was not given either, so it was set to its default value1
.
Let's provide all of the parameters explicitly, then we get:
foo@bar:~$ ./quickstart.py hello UGlja2xl --decode --repeat=3
I'm a Pickle! I'm a Pickle! I'm a Pickle!
Or we can do the same in arbitrary order:
foo@bar:~$ ./quickstart.py hello --repeat 3 --decode UGlja2xl
I'm a Pickle! I'm a Pickle! I'm a Pickle!
Invoking other subcommands is just as easy:
foo@bar:~$ ./quickstart.py calculate primes 50
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 49]
When you are writing function for your action and you need to access some of the variables (flags, parameters, arguments, etc.),
just simply add a parameter to the function with a name same as the variable you need.
Then, the proper value will be parsed and injected by nuclear
.
nuclear
vs argparse
Why use nuclear
, since Python already has argparse
? Here are some subjective advantages of nuclear
:
- declarative way of CLI logic in one place,
- autocompletion out of the box,
- easier way of building multilevel sub-commands,
- automatic action binding & injecting arguments, no need to pass
args
to functions manually, - CLI logic separated from the application logic,
- simpler & concise CLI building - when reading the code, it's easier to distinguish particular CLI rules between them (i.e. flags from positional arguments, parameters or sub-commands),
- CLI definition code as a clear documentation.
Sub-commands done with argparse
:
def foo(args):
print(args.x * args.y)
def bar_go(args):
print(args.z)
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
def _print_help(_: argparse.Namespace):
parser.print_help(sys.stderr)
parser.set_defaults(func=_print_help)
parser_foo = subparsers.add_parser('foo', help='foo help')
parser_foo.add_argument('-x', type=int, default=1)
parser_foo.add_argument('y', type=float)
parser_foo.set_defaults(func=foo)
parser_bar = subparsers.add_parser('bar', help='"bar" help')
subparsers_bar = parser_bar.add_subparsers()
parser_bar_go = subparsers_bar.add_parser('go', help='"bar go" help')
parser_bar_go.add_argument('z')
parser_bar_go.set_defaults(func=bar_go)
args = parser.parse_args()
args.func(args)
with nuclear it's much simpler and cleaner:
def foo(x, y):
print(x * y)
def bar_go(z):
print(z)
CliBuilder().has(
subcommand('foo', help='foo help', run=foo).has(
parameter('-x', type=int, default=1),
argument('y', type=float),
),
subcommand('bar', help='"bar" help').has(
subcommand('go', help='"bar go" help', run=bar_go).has(
argument('z'),
),
),
).run()
Installation
Step 1. Prerequisites
- Python 3.6 or newer (
sudo apt install python3
on Debian/Ubuntu) - pip
Step 2. Install package using pip
Install package from PyPI repository using pip:
pip3 install nuclear
Install package in develop mode
You can install package in develop mode in order to make any changes for your own:
pip3 install -r requirements.txt
python3 setup.py develop
Testing
Running tests:
make setup
. venv/bin/activate
make test
Logging with sublog
sublog
is a logging system that allows you to:
- display variables besides log messages:
log.debug('message', airspeed=20)
, - wrap errors with context:
with wrap_context('ignition')
, - catch errors and show traceback in a concise, pretty format:
with logerr()
.
from nuclear.sublog import log, logerr, wrap_context
with logerr():
log.debug('checking engine', temperature=85.0, pressure='12kPa')
with wrap_context('ignition', request=42):
log.info('ignition ready', speed='zero')
with wrap_context('liftoff'):
raise RuntimeError('explosion')
CLI Rules cheatsheet
Here is the cheatsheet with the most important CLI rules:
#!/usr/bin/env python3
from nuclear import CliBuilder, argument, arguments, flag, parameter, subcommand, dictionary
def main():
CliBuilder('hello-app', version='1.0.0', help='welcome', run=say_hello).has(
flag('--verbose', '-v', help='verbosity', multiple=True),
parameter('repeat', 'r', help='how many times', type=int, required=False, default=1, choices=[1, 2, 3, 5, 8]),
argument('name', help='description', required=False, default='world', type=str, choices=['monty', 'python']),
arguments('cmd', joined_with=' '),
subcommand('run', help='runs something').has(
subcommand('now', 'n', run=lambda cmd: print(f'run now: {cmd}')),
),
dictionary('config', 'c', help='configuration', key_type=str, value_type=int)
).run()
def say_hello(name: str, verbose: int, repeat: int, cmd: str, config: dict):
print(f'Hello {name}')
if __name__ == '__main__':
main()
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.