blog

Photo of pipes by Samuel Sianipar on Unsplash

Project Plumbing with Plumbum (Part III)

by

I’ve written before about Plumbum, first about it’s basic capabilities and then about it’s ability to do things remotely over a network. To paraphrase the great Bill Cosby, "I told you those stories to tell you this one". In addition to all it’s other features, Plumbum also provides a complete framework for building command line applications in a clean, declarative way.

Application object

The core of the Plumbum CLI framework is the Application class. Typically when creating a new command line app, you’ll create a subclass from Application and then override and add certain methods. Here’s a super-simple example:

from plumbum import cli
class HelloApp(cli.Application):
    """A simple app to say "Hello!""""
    PROGNAME = 'HelloApp'
    VERSION = '1.0'
    def main(self):
        print 'Hello CLI World!'
if __name__ == "__main__":
    HelloApp.run()

This code is simple enough to appear trivial, but we’ve already gained a few interesting things. First notice that we’ve sub-classed Application and overridden it’s main method, which serves as the meat of our application. We’ve also set a couple of constants representing the name of the application and it’s version, which we’ll be able to use later on. Then further down we call the class’s run method in order to start our command line app. (Note that this is a classmethod, so it’s a method of the class itself, rather than an instance of the class.)

Even with such a tiny bit of code, the Application base class gives us a couple of nifty things. Firstly, it implements a --version flag which will just display a quick version message:

$ python hello.py --version
HelloApp 1.0

Secondly, we have a built-in --help (or -h) switch which spits out nicely formatted help text that tells how to use our fancy new CLI app:

$ python hello.py --help
HelloApp 1.0
A simple app to say "Hello!"
Usage:
    HelloApp [SWITCHES]
Meta-switches:
    -h, --help     Prints this help message and quits
    -v, --version  Prints the program\'s version and quits

We can also add parameters to our main function, and these will automatically be populated by positional arguments provided by the user on the command line. If we want optional command line arguments, with a default, we can represent those using keyword arguments to the main function. Let’s change our main function to look like this:

    def main(self, who='CLI World'):
        print 'Hello {!s}!'.format(who)

and test it out:

$ python hello.py
Hello CLI World!
$ python hello.py Bob
Hello Bob!

We can also use our --help flag again to see that the help text has been updated to account for our changes:

$ python hello.py --help
HelloApp 1.0
A simple app to say "Hello!"
Usage:
    HelloApp [SWITCHES] [who='CLI World']
Meta-switches:
    -h, --help     Prints this help message and quits
    -v, --version  Prints the program's version and quits

Pretty sweet, eh?

Simple Switches

No hardcore command line user would be satisfied with a CLI tool that didn’t have a boatload of switches and options to play with, and Plumbum accommodates this quite well.

There are a number of common things that command line apps do with switches, and the simplest is probably a boolean flag, which can be either on or off. Plumbum uses the Flag descriptor to implement this. If we wanted a flag to capitalize our hello message, it might look something like this:

yell = cli.Flag(['-y', '--yell'],
                default=False,
                help="Capitalize the hello message.")

The next simplest use case is to count the number of uses of a given flag. This is often used for verbosity levels, "quality" levels, etc. Plumbum uses another descriptor called CountOf for this:

verbosity = cli.CountOf('-v', help="Verbosity Level")

(Note that in older versions of Plumbum, this was called CountingAttr.)

When you use this, the framework will tally up the number of times the user has provided the flag on the command line, and store it as an integer.

A slightly more complex use case is to use a command line switch to store some arbitrary value, like maybe a string or a filename. We’ll use SwitchAttr for this:

filename = cli.SwitchAttr(['-f', '--file'], str, default=None)

Notice that in the above examples, we can either specify the flag/switch in question as a string or as a list of strings. Also notice the str argument, which tells the SwitchAttr that it should only take strings as arguments. Plumbum accepts any callable here, and provides a couple of useful ones for accepting an ExistingFile, ExistingDirectory, or a NonexistentPath.

Switch Functions

If a switch needs to do more than simply store some kind of value, Plumbum also has a facility for connecting a switch on the command line to a Python function in your application. Just use the cli.switch decorator like so:

@cli.switch(["-d", "--do-a-thing"])
def do_a_thing(self):
    print "doing a thing...done"

cli.switch accepts a number of options, which allows validation of input, specifying mutually-exclusive flags, allowing repeatable flags, grouping flags for display in the help text and specifying dependencies between flags. I’m not going to go into much more detail here, but feel free to see the excellent documentation.

Combining all of the above, our hello command now looks something like this:

from plumbum import cli
class HelloApp(cli.Application):
    '''A simple app to say "Hello!"'''
    PROGNAME = 'HelloApp'
    VERSION = '1.0'
    yell = cli.Flag(['-y', '--yell'],
                    default=False,
                    help="Capitalize the hello message.")
    verbosity = cli.CountOf('-v', help="Verbosity Level")
    filename = cli.SwitchAttr(['-f', '--file'], str,
                              default=None,
                              help="Ouptut file.")
    @cli.switch(["-d", "--do-a-thing"])
    def do_a_thing(self):
        print "doing a thing...done"
    def main(self, who='CLI World'):
        if int(self.verbosity) >= 1:
            print "I'm about to say hello."
        msg = 'Hello {!s}!'.format(who)
        if self.yell:
            msg = msg.upper()
        print msg
        if self.verbosity >= 2:
            print "I just said hello."
if __name__ == "__main__":
    HelloApp.run()

Subcommands

Finally we come to my very favorite feature in all of Plumbum. I expect most of you reading this are familiar with various commands that take flags as normal, but also have sub-commands, which can each take their own sets of flags (most version control tools — svn, hg, bzr, git –come to mind). Plumbum makes creating these kinds of CLI apps far simpler than any other method I’ve invented or come across.

The recipe goes something like this:

  • Create a cli.Application object for the main command (e.g. hg).
  • Create a cli.Application object for each subcommand (e.g. init, clone, etc.). Create this like it was it’s own independent command, unrelated to any other command.
  • Wrap each subcommand up under the root command, using a decorator created by the root command’s subcommand method.

For example, if we wanted our HelloApp application above to be a subcommand of a root command called say, it might look something like this (simplified for brevity):

from plumbum import cli
class SayApp(cli.Application):
    def main(self):
        print 'starting to say something...'
@SayApp.subcommand('hello')
class HelloApp(cli.Application):
    '''Say "Hello!"'''
    def main(self, who='CLI World'):
        msg = 'Hello {!s}!'.format(who)
        print msg
if __name__ == "__main__":
    SayApp.run()

Running it looks like this:

$ python say.py
starting to say something...
$ python say.py hello
starting to say something...
Hello CLI World!

You can of course, use all the flag stuff we talked about above with either the root application or the subcommands, creating however complex a CLI app you might need. Try not to make it too complex though…remember the lesson of tar.

“There is too much, lemme sum up”

Well, this series has been quite a ride, so if you’ve made it this far, I congratulate you on putting up with me for this long. We’ve covered a huge amount of stuff, though, so bear with me just a moment while I recap.

Plumbum is a fantastic toolkit for creating all sorts of command line tools. It provides a ton of tools for replacing shell scripts on both Windows and Linux with nice, reusable Python. Additionally it gives you tools for scripting remote machines, using several different underlying SSH platforms. Finally, once you have your scripts doing the right thing, Plumbum gives you a clean toolkit for wrapping them up in a nice, friendly command-line interface.

If I haven’t convinced you by now, I don’t know what else to say. Go check out Plumbum and get rid of all those hacky shell scripts you’ve got lying around!

+ more