The command tree builder ingredient is a part of the command recipe. It is responsible for instantiating all sub-commands and arranging them into a tree for other ingredients to work with (most notably the argument parser ingredient).
The secondary task is to add the spices requested by the top-level command to the bowl. This lets other ingredients act differently and effectively allows the top-level command to influence the runtime behavior of the whole recipe.
This ingredient is not influenced by any spices.
This ingredient adds two objects to the context:
In addition, this ingredient inspects the spieces required by the top-level command and adds them to the bowl.
This ingredient is not exposing any command line arguments.
Let’s create two examples below. One for a simple command and another for a hierarchical command. This example will not use the full command recipe, to focus on the side effects of just the command tree builder ingredient.
We’ll need a command object:
>>> from guacamole.recipes.cmd import Command
>>> class HelloWorld(Command):
... pass
Note that the tree builder is called with an instance of the command, not the class. This allows the top-level command to have a custom initializer, which might be helpful.
>>> from guacamole.core import Context
>>> from guacamole.ingredients import cmdtree
>>> ctx = Context()
>>> cmdtree.CommandTreeBuilder(HelloWorld()).added(ctx)
The context now has the cmd_toplevel object which is just the instance of the command we’ve used.
>>> ctx.cmd_toplevel
<HelloWorld>
Similarly, we’ll have a tree of all the commands and their names in cmd_tree:
>>> ctx.cmd_tree
cmd_tree_node(cmd_name=None, cmd_obj=<HelloWorld>, children=())
The first element of the tuple is the effective command name. This can be used to rename a sub-command. Note that typically the command.name attribute is used (see get_cmd_name()). The second element is the instance and the last element is a tuple of identical cmd_tree_node tuples, one for each of the sub-commands. We’ll see how that looks like in the next example.
We’ll need a few commands for this example. Let’s replicate the git, git commit, git stash, git stash pop and git stash list commands.
>>> from guacamole.recipes.cmd import Command
>>> class StashList(Command):
... pass
>>> class StashPop(Command):
... pass
>>> class Stash(Command):
... sub_commands = (('list', StashList), ('pop', StashPop))
>>> class Commit(Command):
... pass
>>> class Git(Command):
... sub_commands = (('commit', Commit), ('stash', Stash))
Now, let’s feed the Git class to the context.
>>> from guacamole.core import Context
>>> from guacamole.ingredients import cmdtree
>>> ctx = Context()
>>> cmdtree.CommandTreeBuilder(Git()).added(ctx)
The cmd_toplevel is as before (the Git instance). Let’s look at the more interesting command tree.
>>> ctx.cmd_tree
cmd_tree_node(cmd_name=None, cmd_obj=<Git>,
children=(cmd_tree_node(cmd_name='commit', cmd_obj=<Commit>,
children=()), cmd_tree_node(cmd_name='stash',
cmd_obj=<Stash>, children=(cmd_tree_node(cmd_name='list',
cmd_obj=<StashList>, children=()), cmd_tree_node(cmd_name='pop',
cmd_obj=<StashPop>, children=())))))
Blah, that’s mouthful. Let’s see particular fragments to understand it better.
>>> ctx.cmd_tree.children[0].cmd_name
'commit'
>>> ctx.cmd_tree.children[1].cmd_name
'stash'
>>> ctx.cmd_tree.children[1].children[0].cmd_name
'list'
>>> ctx.cmd_tree.children[1].children[1].cmd_name
'pop'
Most of the time you won’t have to use this data. Typically, it is consumed by the argument parser ingredient. Still, if you need it, here it is.
Ingredient for executing the invoked() methods of all the commands that were selected by the user on command line.
This ingredient is responsible for invoking commands. It works during the dispatch phase of the application life-cycle. Since earlier stages can be interrupted it is not aways reached. E.g. when the application is invoked with the --help argument.
The way this ingredient works is simple. It assumes that the argument parser creates a specific structure of references to command objects. The structure is stored in the argparse name-space object (which is available in ctx.args after the parsing phase. The structure is a sequence of attributes ctx.args.command0, ctx.args.command1, ctx.args.command2, etc. The first one, ctx.args.command0 is always present. Subsequent attributes are present if sub-commands are specified on the command line. For example, keeping our git sample in mind, the following command:
$ git stash
Will result in ctx.args.command0 instance of the Git command and ctx.args.command1 an instance of the GitStash command. The dispatcher ingredient will invoke the command0, look at the return value and then (most likely) proceed to command1 (N+1 in general).
The way return value is interpreted is interesting. In general, there are three cases:
This ingredient is not influenced by any spices.
This ingredient does not change the context. It does depend on the args object that is published by the argument parser ingredient.
This ingredient is not exposing any command line arguments.
Let’s see how command invocation works in the few specific examples below.
Let’s start with a hello-world command first:
>>> from guacamole.recipes.cmd import Command
>>> class HelloWorld(Command):
... def invoked(self, ctx):
... print("Hello World")
Let’s create the necessary infrastructure for using the dispatcher:
>>> import argparse
>>> from guacamole.core import Context
>>> from guacamole.ingredients import cmdtree
>>> ctx = Context()
>>> ctx.args = argparse.Namespace()
Now let’s run the HelloWorld command:
>>> ctx.args.command0 = HelloWorld()
>>> cmdtree.CommandTreeDispatcher().dispatch(ctx)
Hello World
Success! The print worked and we also got the exit code (None, which is not printed by the repl).
Next, let’s implement the classic UNIX false(1) command:
>>> class false(Command):
... def invoked(self, ctx):
... return 1
Now, let’s invoke it:
>>> ctx.args.command0 = false()
>>> cmdtree.CommandTreeDispatcher().dispatch(ctx)
1
One. Also good.
All command line tools return an exit code. If you actually run this command in the shell you can inspect the return code in several ways (depending on what is your shell). On Windows that is:
echo %ERRORLEVEL%
And on all other systems, that are mostly using Bash by default:
echo $?
In both cases, you should see 1 being printed by those echo statements.
Let’s expand the Git example to examine the context-manager-like behavior.
>>> class GitLibrary(object):
... def __enter__(self):
... print("Git initialized")
... return self
... def __exit__(self, *args):
... print("Git finalized")
... def commit(self):
... print("Using git to commit")
>>> class Commit(Command):
... def invoked(self, ctx):
... with GitLibrary() as git:
... git.commit()
>>> class Git(Command):
... sub_commands = (('commit', Commit),)
Now, let’s see what dispatch does here:
>>> ctx.args.command0 = Git()
>>> ctx.args.command1 = Commit()
>>> cmdtree.CommandTreeDispatcher().dispatch(ctx)
Git initialized
Using git to commit
Git finalized
If you have many commands that need to use some shared resource, you may be tempted to move the initialization to a shared code path. Guacamole allows you to do this by calling all the invoked() methods of all of the commands specified on command line.
Let’s modify the example to show this. The git library code will say as-is. The commit and git commands will be changed, to move the initialization code around.
>>> class Commit(Command):
... def invoked(self, ctx):
... ctx.git.commit()
>>> class Git(Command):
... sub_commands = (('commit', Commit),)
... def invoked(self, ctx):
... with GitLibrary() as git:
... ctx.git = git
... yield
Now, let’s see what dispatch does now:
>>> ctx.args.command0 = Git()
>>> ctx.args.command1 = Commit()
>>> cmdtree.CommandTreeDispatcher().dispatch(ctx)
Git initialized
Using git to commit
Git finalized
No change, that’s running exactly as before but now we can add more commands without duplicating the relevant code over and over.
Note
Here, the finalization will happen even if something bad happens (e.g. Commit raising an exception). It’s not useful often but it can be a way to use the context manager protocol with commands.