Welcome to clg’s documentation!

Overview

This module is a wrapper to argparse module. Its goal is to generate a custom and advanced command-line from a formatted dictionary. The sequence of subparsers is not limited but it is not possible to have a (sub)parser that have both subparsers and options. The idea is really to not write dozens or hundreds of lines of code to generate the command-line but to outsource it to a file in a “classic” format (YAML, JSON, ...).

When parsing a dictionary in Python, keys are retrieved randomly, so with a simple dictionary, it is not possible to order keys (ie: options and subparsers). That’s why it is better to use OrderedDict (from the module collections) for ordering subparsers and options. The module provide a class for loading a YAML file as in. simplejson module for JSON file support it by default.

Groups and mutually exclusive groups from argparse are implemented but, as I wanted some relatively complex behaviours, “post” checks have been implemented. It means that after the command is parsed, according to the configuration, some additionaly checks can be done (like checking dependencies between options).

Installation

Module is on PyPi, so pip install clg is enough.

Otherwise sources are on github: https://github.com/fmenabe/python-clg

Structure of the configuration

The configuration corresponds of a chain of parsers. Each parsers have either subparsers or options and args. If the current parser have subparsers, the key subparsers is used and the value is a dictionary which keys are the names of subparsers. These subparsers can have subparsers or options, and so on.

When it is a “real” parser (ie: no subparsers but options and args), allowed keywords are:

  • usage
  • desc
  • options
  • args
  • groups
  • exclusive_groups
  • execute

usage

This allow to redifined the usage of the parser if the default usage generated by the argparse module is not enough.

desc

This allow to add a description of the parser parser between usage and options descriptions (see http://docs.python.org/dev/library/argparse.html#description for more details).

options

Define the options of the parser. This is a dictionary where keys are the names of the options and values a dictionary with the option parameters. By default, the name of the option in the configuration is use to generate an option in the parser which begin by -- and, underscores and spaces, are replaced by - (For example my_option in the configuration will add an option --my-option to the parser). The parameters of an option are not an exact mapping to argparse options parameters. Allowed parameters are:

  • short
  • help
  • type
  • required
  • default
  • choices
  • need
  • conflict

Note

As the help is automatically added to a parser, -h/--help options are not available.

short

As say previously, each option as a long format beginning with --. This section allow to add a short format for the option beginning with -.

help

String that indicate the purpose of the option. See http://docs.python.org/dev/library/argparse.html#help for more details. When the message is added in the parser, the pattern $DEFAULT$ is replaced by the value of default parameter.

type

By default, the type of an option is str. Allowed types are:

  • bool => argparse equivalent is action='store_true'
  • list => argparse equivalent is nargs='*'
  • any built-in types and functions

Note

When parsing the command-line, value of options that are not in the command-line or have not default value is None at the exception of the list type which have for default value an empty list ([]).

default

Default value of the option (see http://docs.python.org/dev/library/argparse.html#default for more details).

required

Boolean indicating if the option must be in the command (see http://docs.python.org/dev/library/argparse.html#required for more details).

choices

List of possible choices for this option (see http://docs.python.org/dev/library/argparse.html#choices for more details).

need

This option is one of the “post” checks performed after the command is parsed. This is a list containing the options that the current option absolutely need.

conflict

This option is another one of the “post” checks. This is a list containing options that absolutely not be in the command when the current option is used.

args

Arguments of the command. Same parameters that options.

groups

Group options (see http://docs.python.org/dev/library/argparse.html#argument-groups for more details). Theses options must be defined in the options section.

exclusive_groups

Define a group of exclusives options (see http://docs.python.org/dev/library/argparse.html#mutual-exclusion for more details). This section is a list of dictionaries. Dictionaries can contain a required key that indicate if at least one of the exclusive option must be in the command. The second key is options and indicate options of the exclusive group.

Example:

exclusive_groups:
    -
        required: True
        options:
            - name
            - file

execute

Section that indicate what must be done after the command is parsed.

For now only a module section has been implemented, which launch a function in an external file. For loading the file, the imp module is used. By default the find_module method of this module search the file in any directory of sys.path. By default, directory of the main program is in sys.path so any relative path will have this directory for root. If an absolute path is given, the dirname of the path will be pass to the find_module method.

The executed function defined in the external file must take only one argument: arguments from the command-line. If no function are defined, main function will be executed.

Example:

execute:
    module:
        path: lib/deploy.py
        function: main

Python program

This is the simpler part. You need to import the module clg and the module for loading your configuration file. Then you initialize the CommandLine object with the loaded configuration. Finally, you just need to use parse method for parsing the command. If there is an execute section, this one will be executed. In all case a Namespace with arguments is returned.

Note

Personnaly, I prefer YAML for this type of configuration file (in particular for the simple syntax and anchors), but it is possible to use JSON or any formats that manage python dictionaries.

With argparse, parsing of the command-line return a Namespace. Unfortunately, this namespace is not iterable and it not is possible to access elements like dictionnaries (with []). A custom Namespace has been implemented which implement __iter__ and __getitem__ functions for resolving this problem.

YAML example

import clg
import yaml

def main():
    command = clg.CommandLine(
        yaml.load(open('command.yml'), Loader=clg.YAMLOrderedDictLoader)
    )
    args = command.parse()

if __name__ == '__main__':
    main()

JSON example

import clg
import simplejson as json

def main():
    command = clg.CommandLine(
        json.loads(open('command.json'), object_pairs_hook=OrderedDict)
    )
    args = command.parse()

if __name__ == '__main__':
    main()

Examples

No subparsers

This example show a basic program with no subparsers (in YAML).

YAML file

options:
    foo:
        short: -f
        help: foo help
        required: True
    bar:
        short: -b
        help: bar help
        type: int
        default: 1

Program

import clg
import yaml
from pprint import pprint

command = clg.CommandLine(
    yaml.load(open('command.yml'), Loader=clg.YAMLOrderedDictLoader)
)
command.parse()
args = command.args
pprint(vars(args))
print args.foo, args['bar']

# Parse arguments.
for (arg, value) in sorted(args):
    print arg, value

Tests

# python prog.py --help
usage: prog.py [-h] -f FOO [-b BAR]

optional arguments:
  -h, --help         show this help message and exit
  -f FOO, --foo FOO  foo help
  -b BAR, --bar BAR  bar help

# python prog.py
usage: prog.py [-h] -f FOO [-b BAR]
prog.py: error: argument -f/--foo is required

# python prog.py -f test
{'bar': 1, 'foo': 'test'}
test 1
foo test
bar 1

# python prog.py -f test --bar 2
{'bar': 2, 'foo': 'test'}
test 2
foo test
bar 2

Example with subparsers

This example show a configuration with multiple parsers in YAML. This is really an example for showing what can be done with subparsers but with no other interest.

YAML file

subparsers:
    parser1:
        subparsers:
            parser11:
                options:
                    option111:
                        type: int
                        help: >
                            Help of the first option of the first subparser
                            of the fist parser.
                    option112:
                        type: list
                        help: >
                            Help of second option of the fist subparser of
                            the first parser.
            parser12:
                options:
                    option121:
                        type:bool
                        help: >
                            Help of the first option of the second subparser
                            of the fist parser.
                    option122:
                        type: bool
                        default: True
                        help: >
                            Help of the second option of the second subparser
                            of the fist parser.

    parser2:
        options:
            option21:
                help: Help of the first option of the second parser.
            option22:
                help: Help of the second option of the second parser.

Program

import clg
import yaml
from pprint import pprint

command = clg.CommandLine(
    yaml.load(open('command.yml'), Loader=clg.YAMLOrderedDictLoader)
)
args = command.parse()
pprint(vars(args))

Tests

# python prog.py
usage: prog.py [-h] {parser1,parser2} ...
prog.py: error: too few arguments

# python prog.py parser1
usage: prog.py parser1 [-h] {parser11,parser12} ...
prog.py parser1: error: too few arguments

# python prog.py parser1 parser11
{'command0': 'parser1',
 'command1': 'parser11',
 'option111': None,
 'option112': []}

# python prog.py parser1 parser11 --help
usage: prog.py parser1 parser11 [-h] [--option111 OPTION111]
                                [--option112 [OPTION112 [OPTION112 ...]]]

optional arguments:
  -h, --help            show this help message and exit
  --option111 OPTION111
                        Help of the first option of the first subparser of the
                        fist parser.
  --option112 [OPTION112 [OPTION112 ...]]
                        Help of second option of the fist subparser of the
                        first parser.

# python prog.py parser1 parser11 --option111 test
usage: prog.py parser1 parser11 [-h] [--option111 OPTION111]
                                [--option112 [OPTION112 [OPTION112 ...]]]
prog.py parser1 parser11: error: argument --option111: invalid int value: 'test'

# python prog.py parser1 parser11 --option112 foo bar
{'command0': 'parser1',
 'command1': 'parser11',
 'option111': None,
 'option112': ['foo', 'bar']}

# python prog.py parser1 parser12 --help
usage: prog.py parser1 parser12 [-h] [--option121] [--option122]

optional arguments:
  -h, --help   show this help message and exit
  --option121  Help of the first option of the second subparser of the fist
               parser.
  --option122  Help of the second option of the second subparser of the fist
               parser.

# python prog.py parser1 parser12
{'command0': 'parser1',
 'command1': 'parser12',
 'option121': False,
 'option122': False}

# python prog.py parser1 parser12 --option122
{'command0': 'parser1',
 'command1': 'parser12',
 'option121': False,
 'option122': True}

# python prog.py parser2
{'command0': 'parser2', 'option21': None, 'option22': None}

# python prog.py parser2 --option21 foo --option22 bar
{'command0': 'parser2', 'option21': 'foo', 'option22': 'bar'}

Real-life example

This example is a program I made for managing KVM guests. Actually, there is only two commands for deploying and migrating guests. For each of theses commands, it is possible to deploy/migrate one guest or to use a YAML file which allow to deploy/migrate multiple guests successively. For example, for deploying a new guest, we need the name of the guest (--name), the hypervisor on which it will be deploy (--dst-host), the model on which it is based (--model) and the network configuration (--interfaces). In per guest deployment, all theses parameters must be in the command-line. When using a YAML file (--file), the name and the network configuration must absolutely be defined in the deployment file. Others parameters will be retrieved from the command-line if they are not defined in the file.

To summarize, --name and --file options can’t be used at the same time. If --name is used, --dst-host, --model, --interfaces options must be in the command-line. If --file is used, --interfaces option must no be in the command-line but --dst-host and --model options may be in the command. There also are many options which are rarely used because they are optionals or have default values.

Each command use an external module for implemented the logic. A main function, taking the command-line Namespace as argument, has been implemented. For the example, theses functions will only print the command-line arguments.

YAML file

Get the file

Program

vm.py:

import clg
import yaml
from pprint import pprint

command = clg.CommandLine(
    yaml.load(open('command.yml'), Loader=clg.YAMLOrderedDictLoader)
)
command.parse()

lib/deploy.py

from pprint import pprint
def main(args):
    print "'main' function on 'deploy' module"
    pprint(vars(args))

lib/migrate.py

from pprint import pprint
def main(args):
    print "'main' function on 'migrate' module"
    pprint(vars(args))

Tests

# python prog.py
usage: prog.py [-h] {deploy,migrate} ...
prog.py: error: too few arguments

# python vm.py deploy --help
usage: vm.py deploy
         {
             -n NAME -d DEST -t MODEL
             -i IP,NETMASK,GATEWAY,VLAN [IP2,NETMASK2,VLAN2 ...]
         } | { -f YAML_FILE [-d DEST] [-t model] }
         [-c CORES] [-m MEMORY] [--resize SIZE] [--format FORMAT]
         [--disks SUFFIX1,SIZE1 [SUFFIX2,SIZE2 ...]]
         [--force] [--no_check] [--nbd DEV] [--no-autostart]
         [--vgroot VGROOT] [--lvroot LVROOT]
         [--src-host HOST] [--src-conf PATH] [--src-disks PATH]
         [--dst-conf PATH] [--dst-disks PATH]

optional arguments:
  -h, --help            show this help message and exit
  -n NAME, --name NAME  Name of the VM to deploy.
  -f FILE, --file FILE  YAML File for deploying many hosts. Required
                        parameters on the file are the name and the network
                        configuration. The others parameters are retrieving
                        from the command line (or default values). However,
                        destination and model have no defaults values and must
                        be defined somewhere!
  -d DST_HOST, --dst-host DST_HOST
                        Host on which deploy the new VM.
  -i [INTERFACES [INTERFACES ...]], --interfaces [INTERFACES [INTERFACES ...]]
                        Network interfaces separated by spaces. Parameters of
                        each interfaces are separated by commas. The first
                        interface has four parameters: IP address, netmask,
                        gateway and VLAN. The others interfaces have the same
                        parameters except the gateway.
  -t {redhat5.8,redhat6.3,centos5,ubuntu-lucid,ubuntu-natty,ubuntu-oneiric,ubuntu-precise,w2003,w2008-r2}, --model {redhat5.8,redhat6.3,centos5,ubuntu-lucid,ubuntu-natty,ubuntu-oneiric,ubuntu-precise,w2003,w2008-r2}
                        Model on which the new VM is based.
  -c CORES, --cores CORES
                        Number of cores assigned to the VM (default: 2).
  -m MEMORY, --memory MEMORY
                        Memory (in Gb) assigned to the VM (default: 1).
  --format {raw,qcow2}  Format of the image(s). If format is different from
                        'qcow2', the image is converting to the specified
                        format (this could be a little long!).
  --resize RESIZE       Resize (in fact, only increase) the main disk image
                        and, for linux system, allocate the new size on the
                        root LVM Volume Group. This option only work on KVM
                        host which have a version of qemu superior to 0.??!
  --disks [DISKS [DISKS ...]]
                        Add new disk(s). Parameters are a suffix and the size.
                        Filename of the created image is NAME-SUFFIX.FORMAT
                        (ex: mavm-datas.qcow2).
  --force               If a virtual machine already exists on destination
                        host, configuration and disk images are automaticaly
                        backuped then overwrited!
  --no-check            Ignore checking of resources (Use with cautions!).
  --no-autostart        Don't set autostart of the VM.
  --nbd NBD             NBD device to use (default: '/dev/nbd0').
  --vgroot VGROOT       Name of the LVM root Volume Group (default: 'sys').
  --lvroot LVROOT       Name of the LVM root Logical Volume (default: 'root')
  --src-host SRC_HOST   Host on which models are stored (default: 'bes1')
  --src-conf SRC_CONF   Path of configurations files on the source host
                        (default: '/vm/conf').
  --src-disks SRC_DISKS
                        Path of images files on the source host (default:
                        '/vm/disk').
  --dst-conf DST_CONF   Path of configurations files on the destination host
                        (default: '/vm/conf').
  --dst-disks DST_DISKS
                        Path of disks files on the destination host (default:
                        '/vm/disk')

# python vm.py deploy
usage: vm.py deploy
         {
             -n NAME -d DEST -t MODEL
             -i IP,NETMASK,GATEWAY,VLAN [IP2,NETMASK2,VLAN2 ...]
         } | { -f YAML_FILE [-d DEST] [-t model] }
         [-c CORES] [-m MEMORY] [--resize SIZE] [--format FORMAT]
         [--disks SUFFIX1,SIZE1 [SUFFIX2,SIZE2 ...]]
         [--force] [--no_check] [--nbd DEV] [--no-autostart]
         [--vgroot VGROOT] [--lvroot LVROOT]
         [--src-host HOST] [--src-conf PATH] [--src-disks PATH]
         [--dst-conf PATH] [--dst-disks PATH]
vm.py deploy: error: one of the arguments -n/--name -f/--file is required

# python vm.py deploy -n guest1
usage: vm.py deploy
         {
             -n NAME -d DEST -t MODEL
             -i IP,NETMASK,GATEWAY,VLAN [IP2,NETMASK2,VLAN2 ...]
         } | { -f YAML_FILE [-d DEST] [-t model] }
         [-c CORES] [-m MEMORY] [--resize SIZE] [--format FORMAT]
         [--disks SUFFIX1,SIZE1 [SUFFIX2,SIZE2 ...]]
         [--force] [--no_check] [--nbd DEV] [--no-autostart]
         [--vgroot VGROOT] [--lvroot LVROOT]
         [--src-host HOST] [--src-conf PATH] [--src-disks PATH]
         [--dst-conf PATH] [--dst-disks PATH]
vm.py deploy: error: argument --n/--name: need --d/--dst-host argument

# python vm.py deploy -n guest1 -d hypervisor1 -i 192.168.122.1,255.255.255.0,192.168.122.1,500 -t test
usage: vm.py deploy
         {
             -n NAME -d DEST -t MODEL
             -i IP,NETMASK,GATEWAY,VLAN [IP2,NETMASK2,VLAN2 ...]
         } | { -f YAML_FILE [-d DEST] [-t model] }
         [-c CORES] [-m MEMORY] [--resize SIZE] [--format FORMAT]
         [--disks SUFFIX1,SIZE1 [SUFFIX2,SIZE2 ...]]
         [--force] [--no_check] [--nbd DEV] [--no-autostart]
         [--vgroot VGROOT] [--lvroot LVROOT]
         [--src-host HOST] [--src-conf PATH] [--src-disks PATH]
         [--dst-conf PATH] [--dst-disks PATH]
vm.py deploy: error: argument -t/--model: invalid choice: 'test' (choose from 'redhat5.8', 'redhat6.3', 'centos5', 'ubuntu-lucid', 'ubuntu-natty', 'ubuntu-oneiric', 'ubuntu-precise', 'w2003', 'w2008-r2')

# python vm.py deploy -n guest1 -d hypervisor1 -i 192.168.122.2,255.255.255.0,192.168.122.1,500 -t ubuntu-precise -c 4 -m 4
'main' function on 'deploy' module
{'command0': 'deploy',
 'cores': 4,
 'disks': [],
 'dst_conf': '/vm/conf',
 'dst_disks': '/vm/disk',
 'dst_host': 'hypervisor1',
 'file': None,
 'force': False,
 'format': 'qcow2',
 'interfaces': ['192.168.122.1,255.255.255.0,192.168.122.1,500'],
 'lvroot': 'root',
 'memory': 4,
 'model': 'ubuntu-precise',
 'name': 'guest1',
 'nbd': '/dev/nbd0',
 'no_autostart': True,
 'no_check': False,
 'resize': None,
 'src_conf': '/vm/conf',
 'src_disks': '/vm/disk',
 'src_host': 'bes1',
 'vgroot': 'sys'}

# python vm.py deploy -f test.yml -n guest1
usage: vm.py deploy
         {
             -n NAME -d DEST -t MODEL
             -i IP,NETMASK,GATEWAY,VLAN [IP2,NETMASK2,VLAN2 ...]
         } | { -f YAML_FILE [-d DEST] [-t model] }
         [-c CORES] [-m MEMORY] [--resize SIZE] [--format FORMAT]
         [--disks SUFFIX1,SIZE1 [SUFFIX2,SIZE2 ...]]
         [--force] [--no_check] [--nbd DEV] [--no-autostart]
         [--vgroot VGROOT] [--lvroot LVROOT]
         [--src-host HOST] [--src-conf PATH] [--src-disks PATH]
         [--dst-conf PATH] [--dst-disks PATH]
vm.py deploy: error: argument -n/--name: not allowed with argument -f/--file

# python vm.py deploy -f test.yml -i 192.168.122.2,255.255.255.0,192.168.122.1,500
usage: vm.py deploy
         {
             -n NAME -d DEST -t MODEL
             -i IP,NETMASK,GATEWAY,VLAN [IP2,NETMASK2,VLAN2 ...]
         } | { -f YAML_FILE [-d DEST] [-t model] }
         [-c CORES] [-m MEMORY] [--resize SIZE] [--format FORMAT]
         [--disks SUFFIX1,SIZE1 [SUFFIX2,SIZE2 ...]]
         [--force] [--no_check] [--nbd DEV] [--no-autostart]
         [--vgroot VGROOT] [--lvroot LVROOT]
         [--src-host HOST] [--src-conf PATH] [--src-disks PATH]
         [--dst-conf PATH] [--dst-disks PATH]
vm.py deploy: error: argument --f/--file: conflict with --i/--interfaces argument

# python vm.py deploy -f test.yml -d hypervisor1
'main' function on 'deploy' module
{'command0': 'deploy',
 'cores': 2,
 'disks': [],
 'dst_conf': '/vm/conf',
 'dst_disks': '/vm/disk',
 'dst_host': 'hypervisor1',
 'file': 'test.yml',
 'force': False,
 'format': 'qcow2',
 'interfaces': None,
 'lvroot': 'root',
 'memory': 1,
 'model': None,
 'name': None,
 'nbd': '/dev/nbd0',
 'no_autostart': True,
 'no_check': False,
 'resize': None,
 'src_conf': '/vm/conf',
 'src_disks': '/vm/disk',
 'src_host': 'bes1',
 'vgroot': 'sys'}

Indices and tables