Tutorial

This quick tutorial will guide you through all Cliar's features. We'll start with a simple "Hello World" example and progressively add features to it.

Download the complete app: greeter.py

Hello World

Here's the simplest "Hello World" program with Cliar:

from cliar import Cliar

class Greeter(Cliar):
    def hello(self):
        print('Hello World!')

if __name__ == '__main__':
    Greeter().parse()

In Cliar, CLI is a subclass of Cliar, with its methods turned into commands and their params into args.

Save this code into greeter.py and run it:

$ python greeter.py hello
Hello World!

Optional Flags

Let's add a --shout flag to hello:

    def hello(self, shout=False):
        greeting = 'Hello World!'
        print(greeting.upper() if shout else greeting)

Try running greeter.py with and without the newly defined flag:

$ python greeter.py hello --shout
HELLO WORLD!

$ python greeter.py hello -s
HELLO WORLD!

$ python greeter.py hello
Hello World!

Positional Arguments

Positional args are added the same way as flags.

    def hello(self, name, shout=False):
        greeting = f'Hello {name}!'
        print(greeting.upper() if shout else greeting)

Try it:

$ python greeter.py hello John
Hello John!

$ python greeter.py hello John --shout
HELLO JOHN!

$ python greeter.py hello -s John
HELLO JOHN!

$ python greeter.py hello
usage: greeter.py hello [-h] [-s] name
greeter.py hello: error: the following arguments are required: name

Help Messages

Cliar automatically registers --help flag for the program itself and its every command:

$ python greeter.py --help
usage: greeter.py [-h] {hello} ...

optional arguments:
  -h, --help  show this help message and exit

commands:
  {hello}     Available commands:
    hello

$ python greeter.py hello --help
usage: greeter.py hello [-h] [-s] name

positional arguments:
  name

optional arguments:
  -h, --help   show this help message and exit
  -s, --shout

Command help messages are generated from docstrings. Let's add them:

class Greeter(Cliar):
    '''Greeter app created with Cliar.'''

    def hello(self, name, shout=False):
        '''Say hello.'''

        greeting = f'Hello {name}!'
        print(greeting.upper() if shout else greeting)

and view the updated help message:

$ python greeter.py -h
usage: greeter.py [-h] {hello} ...

Greeter app created with Cliar.

optional arguments:
  -h, --help  show this help message and exit

commands:
  {hello}     Available commands:
    hello     Say hello.

$ python greeter.py hello -h
usage: greeter.py hello [-h] [-s] name

Say hello.

positional arguments:
  name

optional arguments:
  -h, --help   show this help message and exit
  -s, --shout

To add description for arguments, use set_help decorator:

from cliar import Cliar, set_help

...

    @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'})
    def hello(self, name, shout=False):
        '''Say hello.'''

        greeting = f'Hello {name}!'
        print(greeting.upper() if shout else greeting)

The decorator takes a mapping from param names to help messages.

Call the help message for hello command:

$ python greeter.py hello -h
usage: greeter.py hello [-h] [-s] name

Say hello.

positional arguments:
  name         Who to greet

optional arguments:
  -h, --help   show this help message and exit
  -s, --shout  Shout the greeting

To show default values for flags, add show_defaults = True to set_help:

...
    @set_help(
        {'name': 'Who to greet', 'shout': 'Shout the greeting'},
        show_defaults = True
    )
    def hello(self, name, shout=False):
...

Call help again to see the default value:

$ python greeter.py hello -h
usage: greeter.py hello [-h] [-s] name

Say hello.

positional arguments:
  name         Who to greet

optional arguments:
  -h, --help   show this help message and exit
  -s, --shout  Shout the greeting (default: False)

Metavars

Metavar is a placeholder of a positional arg as it appears in the help message. By default, Cliar uses the param name as its metavar. So, for name param the metavar is called name:

$ python greeter.py hello -h
usage: greeter.py hello [-h] [-s] name

Say hello.

positional arguments:
  name         Who to greet

optional arguments:
  -h, --help   show this help message and exit
  -s, --shout  Set to shout the greeting

To set a different metavar for a param, usd set_metavars decorator:

from cliar import Cliar, set_help, set_metavars

...

    @set_metavars({'name': 'NAME'})
    @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'})
    def hello(self, name, shout=False):
        '''Say hello.'''

        greeting = f'Hello {name}!'
        print(greeting.upper() if shout else greeting)

The decorator takes a mapping from param names to metavars.

Call the help message for hello:

usage: greeter.py hello [-h] [-s] NAME

Say hello.

positional arguments:
  NAME         Who to greet

optional arguments:
  -h, --help   show this help message and exit
  -s, --shout  Set to shout the greeting

Type Casting

Cliar casts arg types of args on the fly. To use type casting, add type hints or default values to params.

Let's add -n flag that will tell how many times to repeat the greeting:

    @set_metavars({'name': 'NAME'})
    @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'})
    def hello(self, name, n=1, shout=False):
        '''Say hello.'''

        greeting = f'Hello {name}!'

        for _ in range(n):
            print(greeting.upper() if shout else greeting)

Let's call hello with the new flag:

$ python greeter.py hello John -n 2
Hello John!
Hello John!

If we pass a non-integer value to -n, an error occurs:

$ python greeter.py hello John -n foo
usage: greeter.py hello [-h] [-n N] [-s] NAME
greeter.py hello: error: argument -n/--n: invalid int value: 'foo'

Hint

You can use any callable as a param type, and it will be called to cast the param type during parsing. One useful example is using open as the param type:

def read_from_file(input_file: open):
    lines = input_file.readlines()

If you pass a path to such a param, Cliar will open it and pass the resulting file-like object to the handler body. And when the handler returns, Cliar will make sure the file gets closed.

Argument Names

By default, Cliar takes the param name, replaces underscores with dashes, and uses that as the corresponding arg name: name is turned into --name, and upper_limit into --upper-limit; the first letter is used as a short option: -n for --name, -u for --upper-limit.

To use different arg names, use set_arg_map decorator:

from cliar import Cliar, set_help, set_metavars, set_arg_map

...

    @set_arg_map({'n': 'repeat'})
    @set_metavars({'name': 'NAME'})
    @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'})
    def hello(self, name, n=1, shout=False):
        '''Say hello.'''

        greeting = f'Hello {name}!'

        for _ in range(n):
            print(greeting.upper() if shout else greeting)

Now use --repeat or -r instead of -n:

$ python greeter.py hello John --repeat 2
Hello John!
Hello John!

$ python greeter.py hello John -r 2
Hello John!
Hello John!

Hint

This decorator lets you use Python's reserved words like --for and --with as arg names.

You can also override argument short names specifically, with set_sharg_map decorator:

from cliar import Cliar, set_help, set_metavars, set_arg_map, set_sharg_map

...

    @set_arg_map({'n': 'repeat'})
    @set_sharg_map({'n': 'n'})
    @set_metavars({'name': 'NAME'})
    @set_help({'name': 'Who to greet', 'shout': 'Shout the greeting'})

    def hello(self, name, n=1, shout=False):
        '''Say hello.'''

        greeting = f'Hello {name}!'

        for _ in range(n):
            print(greeting.upper() if shout else greeting)

Now you can use -n instead of -r:

$ python greeter.py hello John --repeat 2
Hello John!
Hello John!

$ python greeter.py hello John -n 2
Hello John!
Hello John!

This is useful when you have several arguments that start with the same letter, which creates a conflict between short arg names.

To disable short argument variant entirely, set the short arg name to None: @set_sharg_map({'argname': None}).

Multiple Commands

Adding more commands to the CLI simply means adding more methods to the CLI class:

class Greeter(Cliar):
    def goodbye(self, name):
        '''Say goodbye'''

        print(f'Goodbye {name}!')

    @set_arg_map({'n': 'repeat'})
    ...

With this code addition, you can call goodbye command:

$ python greeter.py goodbye Mary
Goodbye Mary!

Nested Commands

You can have any level of nested commands by adding Cliar CLIs as class attributes.

For example, let's add a utils subcommand with its own time subcommand that has now command:

class Time(Cliar):
    def now(self, utc=False):
        now_ctime = datetime.utcnow().ctime() if utc else datetime.now().ctime()
        print(f'UTC time is {now_ctime}')

class Utils(Cliar):
    time = Time

class Greeter(Cliar):
    '''Greeter app created with in Cliar.'''

    utils = Utils

    def _root(self, version=False):
        ...

You can now call now command through utils:

$ python greeter.py utils time now
Local time is Sun Jul 21 15:25:52 2019

$ python greeter.py utils time now --utc
UTC time is Sun Jul 21 11:25:57 2019

Command Aliases

To add aliases to a command, use add_aliases decorator:

from cliar import Cliar, set_help, set_metavars, set_arg_map, add_aliases

...

    @add_aliases(['mientras', 'пока'])
    def goodbye(self, name):
        '''Say goodbye'''

        print(f'Goodbye {name}!')

Now you can call goodbye command by its aliases:

$ python greeter.py mientras Maria
Goodbye Maria!

$ python greeter.py пока Маша
Goodbye Маша!

Command Names

By default, CLI commands are named after the corresponding methods. To override this behavior and set a custom command name, use set_name decorator:

from cliar import Cliar, set_help, set_metavars, set_arg_map, add_aliases, set_name


class Greeter(Cliar):
    '''Greeter app created with in Cliar.'''

    @set_name('factorial')                  # Name the command `factorial`
    def calculate_factorial(self, n: int):  # because `calculate_factorial`
        '''Calculate factorial'''           # is too long for CLI.

        print(f'n! = {factorial(n)}')

Now calculate_factorial is called with factorial command:

$ python greeter.py factorial 4
n! = 24

$ python greeter.py calculate_factorial 4
usage: greeter.py [-h] {factorial,goodbye,mientras,пока,hello} ...
greeter.py: error: argument command: invalid choice: 'calculate_factorial' (choose from 'factorial', 'goodbye', 'mientras', 'пока', 'hello')

Ignore Methods

By default, Cliar converts all non-static and non-class methods of the Cliar subclass into CLI commands.

There are two ways to tell Cliar not to convert a method into a command: start its name with an underscore or use ignore decorator:

from math import factorial, tau, pi

from cliar import Cliar, set_help, set_metavars, set_arg_map, add_aliases, set_name, ignore


class Greeter(Cliar):
    '''Greeter app created with in Cliar.'''

    def _get_tau_value(self):
        return tau

    @ignore
    def get_pi_value(self):
        return pi

    def constants(self):
        print(f'τ = {self._get_tau_value()}')
        print(f'π = {self.get_pi_value()}')

    ...

Only constants method will be exposed as a CLI command:

$ python greeter.py constants
τ = 6.283185307179586
π = 3.141592653589793

$ python greeter.py get-pi-value
usage: greeter.py [-h] {factorial,constants,goodbye,mientras,пока,hello} ...
greeter.py: error: argument command: invalid choice: 'get-pi-value' (choose from 'factorial', 'constants', 'goodbye', 'mientras', 'пока', 'hello')

Root Command

To assign action to the root command, i.e. the script itself, define _root method:

class Greeter(Cliar):
    '''Greeter app created with in Cliar.'''

    def _root(self, version=False):
        print('Greeter 1.0.0.' if version else 'Welcome to Greeter!')
    ...

If you run greeter.py with --version or -v flag, you'll see its version. If you call greeter.py without any flags or commands, you'll see a welcome message:

$ python greeter.py
Welcome to Greeter!

$ python greeter.py --version
Greeter 1.0.0.

Global Arguments

Global arguments defined in _root can be accessed in commands via self.global_args:

    ...

    def constants(self):
        if self.global_args.get('version'):
            print('Greeter 1.0.0.')

        print(f'τ = {self._get_tau_value()}')
        print(f'π = {self.get_pi_value()}')

    ...

Run constants with --version:

$ python greeter.py --version constants
Greeter 1.0.0.
τ = 6.283185307179586
π = 3.141592653589793

This works with nested commands, too. Global arguments of nested commands override global arguments of their parents.