Using Stock Recipes

The Command Recipe

The command recipe contains the distilled, correct behavior for command line applications. The main face of the command recipe is the Command class.

Note

Guacamole values conventions. Instead of overriding many of the methods that comprise the Command class, you can just define a variable that will take priority. This leads to shorter and more readable code.

Defining commands

Let’s build a simple hello-world example again:

>>> class HelloWorld(guacamole.Command):
...     def invoked(self, ctx):
...         print("Hello World!")

The central entry point of each command is the invoked() method. The method is called once the command is ready to be dispatched. This is what you would put inside your main() function, after the boiler-plate code that Guacamole handles for you. What you do here is up to you.

For now, let’s just run our simple example with the convenience method main(). Note that here we’re passing extra arguments to control how the tool executes, normally you would just call main without any arguments and it will do the right thing.

>>> HelloWorld().main([], exit=False)
Hello World!
0

For now let’s ignore the argument ctx. It is extremely handy, as we will see shortly, but we don’t need it yet.

Note

This little example is available in the examples/ directory in the source distribution. The version of Guacaomle packaged in Debian has them in the directory /usr/share/doc/python-guacamole-doc/examples. As the directory name implies, you have to install the python-guacamole-doc package to get them.

Do use the example and play around with it, see how it behaves if you run it with various arguments. The idea is that Guacamole is supposed to create good command line applications. Good applications do the right stuff internally. The hello-world example is trivial but we’ll see more of what is going on internally soon.

Working with arguments & The Context

Commands typically take arguments. To say which arguments are understood by our command we need to implement the second method register_arguments(). This method is called with the familiar argparse.ArgumentParser instance. You’ve seen this code over and over, here you should just focus on configuring the arguments and options. Guacamole handles the parser for you.

>>> class HelloWorld(guacamole.Command):
...     def register_arguments(self, parser):
...         parser.add_argument('name')
...     def invoked(self, ctx):
...         print("Hello {0}!".format(ctx.args.name))

As you can see, the context is how you reach the command line arguments parsed by argparse. What else is there you might ask? The answer is everything.

The context is how ingredients can expose useful capabilities to commands. The command recipe is comprised of several ingredients, as you will later see. One of those ingredients parsers command line arguments and adds the results to the context as the args object.

Note

When reading documentation about particular ingredients make sure to see how they interact with the context. Each ingredient documents that clearly.

Let’s run our improved command and see what happens:

>>> HelloWorld().main(["Guacamole"], exit=False)
Hello Guacamole!
0

No surprises there. We can see that the command printed the hello message and then returned the exit code 0. The exit code is normally passed to the system so that your application can be scripted.

Note

Guacamole will return 0 for you if you don’t return anything. If you do return a value we’ll just preserve it for you. You can also raise SystemExit with any value and we’ll do the right thing yet again.

This should be all quite familiar to everyone so we won’t spend more time on arguments now. You can read the argparse-tutorial if you want.

A small digression, why argparse?

By default, all command line parsing is handled by argparse.

Guacamole doesn’t force you to use argparse (nothing really is wired to depend on it in the core) but the stock set of ingredients do use it. Argparse is familiar to many developers and by having it by default you can quickly convert your application code over to guacamole without learning two new things at a time.

Nesting Commands

Many common tools expose everything from a top-level command, e.g. git commit. Here, git gets invoked, looks at the command line arguments and delegates the dispatching to the git-commit command.

All Guacamole commands can be nested. Let’s build a quick git-like command to see how to do that.

>>> class git_commit(guacamole.Command):
...     name = 'commit'
...     def invoked(self, ctx):
...         print("commit invoked")

>>> class git_log(guacamole.Command):
...     def invoked(self, ctx):
...         print("log invoked")

>>> class git(guacamole.Command):
...     name = 'git'
...     sub_commands = (
...         (None, git_commit),
...         ('log', git_log),
...     )

As you see it’s all based on declarations. Each command now cares about the name it is using. Names can be assigned in the sub_commands list or individually in each class, by defining the name attribute.

The name listed in sub_commands takes precedence over the name defined in the class. Here, the git_log command doesn’t define a name so we provide one explicitly as the first element of the pair, as sequence of which is stored in sub_commands.

Note

Behind the scenes Guacamole actually calls a number of methods for everything. See get_sub_commands() and get_cmd_name() for the two used here. There are many more methods though.

Let’s invoke our fake git to see how that works now:

>>> git().main(["commit"], exit=False)
commit invoked
0

>>> git().main(["log"], exit=False)
log invoked
0

So far everything behaves as expected. Let’s see what happens if we run something that we’ve not coded:

>>> git().main(["status"], exit=False)
2

This won’t fit the doctest above (it’s printed on stderr) but in reality the application will also say something like this:

usage: git [-h] {commit,log} ...
setup.py: error: invalid choice: 'status' (choose from 'commit', 'log')

Note

Technically the Command class has numerous methods. Most of those methods are of no interest to most of the developers. Feel free to read the API reference later if you are interested.