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.