Usage

m.a.c.h is a single Python module which has two decorators for usages. The first decorator mach1 turns a normal Python class to a command line application with subcommand a-la git or docker. If the application has no need for subcommands you can simply define a default subcommand which will be invoked automatically.

Example mach1

from mach import mach1


@mach1()
class Hello:

    default = 'greet'

    # A doc string should always have a title

    # an empty space
    # name of the option followed by a hyphen
    # short description

    def greet(self, count: int=1, name: str=""):
        """Greets a user one or more times

        count - the number of times to greet the user
        name - the name of the user to greet
        """

        if not name:
            name = input('Your name: ')

        for c in range(count):
            print("Hello %s" % name)

    def part(self):
        """Politely part from a user"""
        print("It was nice to meet you!")


if __name__ == '__main__':
    Hello().run()

The greet.py has two sub-commands greet and part. You don’t neet to give the greet sub-command as an argument:

$ ./examples/greet.py
Your name: Tom
Hello Tom
$

The greet sub-command has two optional arguments which you can also give in the command line:

$ ./examples/greet.py greet --name tom --count 3
Hello tom
Hello tom
Hello tom

The application is automatically documented. The first line of a method docstring is documenting the subcommand:

$ ./examples/greet.py -h
usage: greet.py [-h] {greet,part} ...

positional arguments:
  {greet,part}  commands
    greet       Greets a user one or more times
    part        Politely part from a user

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

Using a carefully formatted docstring you can automatically document the options of your sub-commands. This documentation will be printed when a sub-command help option is invoked:

./examples/greet.py greet -h
usage: greet.py greet [-h] [--name NAME] [--count COUNT]

optional arguments:
  -h, --help            show this help message and exit
  --name NAME, -n NAME  the name of the user to greet (default: )
  --count COUNT, -c COUNT
                        the number of times to greet the user (default: 1)

Also note, that the automatically added options support both long and short variants. Hence, these invocataions are possible:

./examples/greet.py -c 3 -n Tom
./examples/greet.py --count 3 -n Tom
./examples/greet.py --count 3 --name Tom
./examples/greet.py -c 3 --name --Tom

Advanced mach1 with default values and JSON parsing

You can write methods with default values or with a certain number of open options as in **kwargs passed to a Python method:

See examples/uftpd.py for an implementation of a hypothetical FTP server example.

You can invoke this ftp server with:

$ ./examples/uftpd.py --foreground --level 3

This will run the server in the foreground with a verbosity level 3.

$ ./examples/uftpd.py –opts=’{“ftp”: 21}’ serving FTP on port 21

opts is automatically parsed as JSON. The server will run in the background and a verbosity level of 2.

Using mach2

The decorator mach2 adds on top of mach1 all the existing capabilities, the ability to turn a class to an interactive interpreter. The most simple interactive interpreter is a command line calculator:

import sys

from mach import mach2

@mach2()
class Calculator:

    def add(self, a: int, b: int):
        """adds two numbers and prints the result"""
        print("%s + %s => %d" % (a, b, int(a) + int(b)))

    def div(self, a: int, b: int):
        """divide one number  by the other"""
        print("%s / %s => %d" % (a, b, int(a) // int(b)))

    def exit(self):
        """exist to finish this session"""
        print("Come back soon ...")
        sys.exit(0)


if __name__ == '__main__':
    calc = Calculator()
    calc.intro = 'Welcome to the calc shell. Type help or ? to list commands.\n'
    calc.prompt = 'calc2 > '
    calc.run()

You can invoke this application via the command line by giving a sub-command:

$ ./examples/calc2.py add 5 6
6 + 5 => 11

Or start an interactive session by not giving any sub-command:

$ ./examples/calc2.py
Welcome to the calc shell. Type help or ? to list commands.

calc2 >

You can now type a command in the interactive interpreter:

calc2 > add 7 3
7 + 3 => 10
calc2 > div 16 8
16 / 8 => 2

As with mach1 doc-strings are used to documented your application functionality:

calc2 > help div
divide one number  by the other
calc2 > help add
adds two numbers and prints the result

Advanced mach1 with default values and JSON parsing

A simple calculator does not all the features mach2 offers. A better example is a hypothetical FTP client.

See examples/lftp.py.

Once started it waits for user input at the lftp prompt:

$ ./examples/lftp.py
Welcome to the lftp client. Type help or ? to list commands.

lftp > help

Documented commands (type help <topic>):
========================================
connect  exit  help  login  ls

lftp > help connect
connect to FTP host

host - the host IP or fqdn
port - the port listening to FTP

Typing the help command will list the available commands. Typing help connect lists the arguments that the command connect gets, by parsing the method’s docstring.

Since this command can now be invoked in any of the following ways:

lftp > connect 10.10.192.192
Connected to 10.10.192.192:21

lftp > connect host=foo.example.com port=21
Connected to foo.example.com:21

lftp > connect foo.example.com 2121
Connected to foo.example.com:2121

lftp > connect foo.example.com 21 opts='{"user": "oz123", "password": "s3kr35"}'
Connected to foo.example.com:21
Login success ...

The last invocation also shows that you can pass extra arguments as JSON.

The interpreter is checking how you invoke the commands. Hence this all don’t work:

lftp > connect foo 2121 bar
*** Unknown syntax: connect foo 2121 bar
lftp > help login
login to the FTP server
lftp > login oz123 s3kr35
Login success ...
lftp > login foobar secret error
*** Unknown syntax: login foobar secret error

Flags vs. Commands

Sometimes you want to add global flags to your applications. Here is an hypothetical CLI application:

$ ./bolt --verbositiy=2 clone https://...

This application launches the subcommand clone with the verbosity level 2. This can be done with:

@mach1()
class Bolt:
    """
    The main entry point for the program. This class does the CLI parsing
    and descides which action shoud be taken
    """
    def __init__(self):
        self.parser.add_argument("-v", "--verbosity")
        self._verbosity = 1

    def _set_verbosity(self, value):
        "set verbosity"
        self._verbosity = value

See the example bolt.py for more details.

Explicit shell or implicit shell using mach2

The example calc2.py and lftp have an implicit shell option. That is, if the program called with out arguments it will start an interactive shell session, like the Python interpreter itself.

However, you might not desire this behaviour. Instead you prefer an explicit argument for a shell invocation. If so, you can simply decorate your class with:

@mach2(explicit=True)
class Calculator:

    def add(self, a: int, b: int):
        """adds two numbers and prints the result"""
        print("%s + %s => %d" % (a, b, int(a) + int(b)))

    ...

Now, and interactive shell option is added:

$ ./examples/calc2.py -h
usage: calc2.py [-h] [--shell] {add,div,exit} ...

positional arguments:
  {add,div,exit}  commands
    add           adds two numbers and prints the result
    div           divide one number by the other
    exit          exist to finish this session

optional arguments:
  -h, --help      show this help message and exit
  --shell         run an interactive shell (default: False)
$ ./examples/calc2.py --shell
Welcome to the calc shell. Type help or ? to list commands.

calc2 >

Inheritence and ‘private’ methods

The examples shown above always create a command line interface from all methods defined in a class. So if we have a class which inherits methods from another class, all methods will have a ‘public’ command line interface:

class Foo:
     def foo(self):
         pass
     def bar(self):
         pass

@mach1()
class Baz(Foo)
    def do(self):
        pass

This a will create a command line interface for do but also for foo and bar. This can be avoided by naming the class method with a leading underscore _:

class Foo:
     def _foo(self):
         pass
     def _bar(self):
         pass

@mach1()
class Baz(Foo)
    def do(self):
        self._foo()

This creats a command line interface only for do, and the ‘private’ methods are hidden.

Extra long help for subcommands

You can use an extended help format for subcommands. Just add after describing the options of each subcommand. Below these you can add a longer text which will be shown next to each subcommand. This is demonstated by the example uftpd2.py:

./examples/uftpd2.py -h
usage: uftpd2.py [-h] {server} ...

positional arguments:
  {server}    commands
    server    No nonsense TFTP/FTP Server. add some long test below these
              three dashes

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