In their day-to-day work, developers often have to perform the same task over and over, losing valuable time that could be spent on more productive things.

As good geeks/developers, the first thing that comes to mind is to automate those tasks, and writing small scripts or programs to perform them is the most common solution. However, it is easy to end up with a bunch of scripts which are difficult to maintain and share. To be more effective, we have written a little pluggable command line tool, to aggregate all these scripts and to provide an easy way to add and remove features. This post describes the core parts of this simple tool so anyone can build their own command line tool in seconds and complete it with their own plugins.

The command line tool has the following syntax: cli <plugin> <command> [<arguments>]

So for example, we could build a couple of plugins and invoke them as follows:

  • cli environment generate
  • cli image upload –target=<url>

Our tool will invoke the appropriate method of the referenced plugin, providing all the command line arguments, so the plugin has all it needs to run.

Ok, this seems cool. Let’s get into the code!

There are only three classes in this command line tool:
  • CLI: This is the main command line interface. It handles user input and delegates execution to the plugin manager.
  • PluginManager: Loads plugins and calls the appropriate plugin method when the user invokes the command line.
  • AbsPlugin: Defines common behavior for all plugins. Each plugin class must extend this one to be considered a valid plugin.

CLI

Responsible for invoking the plugin manager with the appropriate arguments or printing the help if the user’s input is invalid. The following function shows how:

[code lang=”python” light=”true”] def parse_input(self):
""" Validates user input and delegates to the plugin manager """
if len(sys.argv) &lt; 2:
print "Usage: cli []"
print "The following plugins are available:n"
self.__pluginmanager.help_all()
elif len(sys.argv) == 2:
print "Usage: cli []"
# Call the given plugin without command to print
# the help of the plugin
return self.__pluginmanager.call(sys.argv[1], None, None)
else:
# Call the command in the given plugin with the
# remaining arguments
return self.__pluginmanager.call(sys.argv[1],
sys.argv[2], sys.argv[3:])
[/code]

Basically it checks that at least two arguments are provided (the plugin name and the command to execute), and then delegates to the plugin. If additional arguments are provided, then it passes them to the plugin.

View entire file

PluginManager

This is the core class of the command line tool. It loads the plugins inside a plugins folder. That plugins folder is a python module that reads each file inside the folder upon initialization, to provide the list of available plugins. The following code shows the contents of the plugins/__init__.py file used to read the available plugins:

[code lang=”python” light=”true”] import os

# Automatically set the __all__ variable with all
# the available plugins.

plugin_dir = "plugins"

__all__ = [] for filename in os.listdir(plugin_dir):
filename = plugin_dir + "/" + filename
if os.path.isfile(filename):
basename = os.path.basename(filename)
base, extension = os.path.splitext(basename)
if extension == ".py" and not basename.startswith("_"):
__all__.append(base)
[/code]

With the list of available plugins dynamically computed, we can easily add files to the plugins folder and they will automatically become part of the command line tool. We can also remove files.
The following code snippet from the PluginManager class shows how the plugins are loaded:

[code lang=”python” light=”true”] def __init__(self):
""" Initialize the plugin list """
self.__plugins = {}

def load_plugin(self, plugin_name):
""" Loads a single plugin given its name """
if not plugin_name in __all__:
raise KeyError("Plugin " + plugin_name + " not found")
try:
plugin = self.__plugins[plugin_name] except KeyError:
# Load the plugin only if not loaded yet
module = __import__("plugins." + plugin_name, fromlist=["plugins"])
plugin = module.load()
self.__plugins[plugin_name] = plugin
return plugin
[/code]

Apart from automatically loading the plugins, the command line tool will also generate the documentation based on the plugin documentation strings. This way each plugin and command will be self-documented and a printable help could be generated for each one using the following function of the PluginManager class.

[code lang=”python” light=”true”] def help(self, plugin):
""" Prints the help for the given plugin """
commands = plugin._commands()
plugin_name = plugin.__module__.split(‘.’)[-1] print "%s" % plugin.__doc__
for command in sorted(commands.iterkeys()):
print " %s %st%s" % (plugin_name, command,
commands[command].__doc__)
[/code]

View entire file

AbsPlugin

This is the base class for all plugins. It simply reads all public methods from the plugin class and exposes them to the plugin manager as commands that can be invoked:

[code lang=”python” light=”true”] def _commands(self):
""" Get the list of commands for the current plugin.
By default all public methods in the plugin implementation
will be used as plugin commands. This method can be overriden
in subclasses to customize the available command list """
attrs = filter(lambda attr: not attr.startswith(‘_’), dir(self))
commands = {}
for attr in attrs:
method = getattr(self, attr)
commands[attr] = method
return commands
[/code]

View entire file

Putting it all together

These are the core classes of this simple command line tool. To put it all together just create a directory with the following structure and place the command line files there:

  • abstract.py
  • cli.py
  • pluginmanager.py
  • plugins/__init__.py

Building custom plugins

Adding new plugins to the command line is quite easy. Just follow these simple steps:

  1. Create the plugin file and place it under the plugins folder
  2. Write your plugin class making it extend AbsPlugin
  3. Add a load() method to your plugin file that returns an instance of your plugin

That’s it!

The following code can be used as a skeleton for new plugins.

[code lang=”python” light=”true”] #!/usr/bin/env python

# This is an example plugin that can be used as a
# skeleton for new plugins.
# The documentation string in the plugin class will be used to
# print the help of the plugin.

from abstract import AbsPlugin

class SkeletonPlugin(AbsPlugin):
""" An example plugin that prints dummy messages """
def __init__(self):
pass

# Public methods will be considered plugin commands.
# The name of the command will be the method name.
# The documentation string in command methods will be used to
# print the help of the command.
# The arguments are the options given to the command itself
def dummy(self, args):
""" Prints a dummy message """
print "This is the print_handler in the example plugin"

# Each plugin must provide a load method at module level that will be
# used to instantiate the plugin
def load():
""" Loads the current plugin """
return SkeletonPlugin()
[/code]

Just place it inside the plugins folder and invoke the cli to see how it works:

$ python cli.py
Usage: cli <plugin> <command> [<options>]
The following plugins are available:

 An example plugin that prints dummy messages
   skel dummy	 Prints a dummy message

Examples

  • Kahuna An Abiquo command line tool.
    We use kahuna to provision our development environments, deploy virtual machines such as hypervisors, storage cabinets, etc, and to manage the infrastructure and cloud resources. It uses jython and the Jclouds Abiquo provider.