Skip to content
This repository

Commando Documentation

Usage guide for Commando

Introduction

Commando is a Command Line Interface (Cli) Library for Python.

Commando was inspired by both Pythons built-in optparse module and the JewelCli Java library.

Commando eases the pain usually involved in trying to define and parse command line arguments to your Python application.

Installing Commando

Download the latest Commando source. Then just copy the Cli.py module into your Python site-packages folder.

Source Code

The source was built and tested using Python v2.6. The unit tests make use of the Python nosetools v1.0.0 library.

The code was developed on an iMac running Mac OS X v10.6.6 using Eclipse IDE (Build id: 20100218-1602) with following plugins:

Each unit test was run within Eclipse by selecting it and then picking Run As -> Python Run.

The Commando source comes with the following set of unit tests which are split by feature:

  • TestSimpleCli.py This represents the simplest usage
  • TestCliWithInheritance.py This shows how options can be inherited
  • TestCliWithDefault.py This shows how default values can be specified
  • TestCliWithMandatory.py This shows how to specify an option as being mandatory
  • TestCliWithMultiValued.py This shows how to specify an option with multiple values
  • TestCliWithShortName.py This shows how each option can be given a short name
  • TestCliWithDefaultHelp.py This shows to access the default help text
  • TestCliWithCustomisedHelp.py This shows how to enhance the help text
  • TestCliWithValueFormatter.py This shows how the option values can be formatted to suit your needs
  • TestCliWithPositional.py This shows how to specify positional arguments

Copyright & License

(c) Anjum Naseer open-cli@anjum.otherinbox.com 2nd January 2011. Licensed under the (GPL-compatible) MIT License

Core Concepts

Commando allows you to both define and access command line arguments via a user-defined Python interface. Python does not have the ability to define an interface in the same way as, say, Java does. So what you do instead is define a standard Python class with empty methods as follows:

class MyOptions(object):
   @option
   def getFilePath(self): pass

   @option
   def isRecursive(self): pass

Every option annotated with a Cli.option or Cli.positional decorator. The decorated methods must have a name that starts with either get or is. Methods that start with is represent boolean options (i.e options whose values can be either True or False). Every is method will return True if that option was specified on the command line, and will return False otherwise.

You obtain a reference to this interface by passing the interface to the Cli constructor and then calling its parseArguments() method as follows:

from Cli import Cli
from Cli import option

myOptions = Cli(MyOptions).parseArguments()
filePath = myOptions.getFilePath()

By default the command line arguments are picked up from sys.argv[1:]. You can override this behaviour by passing in your own list of strings instead as follows:

from Cli import Cli
from Cli import option

myOptions = Cli(MyOptions).parseArguments(['--filePath', 'C:/some/path', '--recursive'])
filePath = myOptions.getFilePath()

The name of the command line option that maps to each method is obtained by dropping the initial get or is and then turning the first letter of the remaining name to lowercase. Thus the command line option --filePath specifies the value C:/some/path that can be obtained by calling myOptions.getFilePath().

Each option can be further customised by supplying parameters to the Cli.option and Cli.positional decorators (see examples below).

Workflow Overview

  • Define a Python interface that defines each command line option
  • Construct a Cli instance using this interface
  • Call the Cli instances parseArguments method to parse the command line arguments supplied to your Python application. This will return an instance of your interface that can be used to retrieve each of the supplied command line arguments.
  • Use the returned instance of your interface to access each command line argument

Features

  • Ability to define an inheritance hierarchy of options (to aid re-use)
  • Ability define a short name for each option
  • Ability to define default value for each option
  • Ability to mark options as mandatory
  • Ability to mark an option as multi-valued (with optional min/max values)
  • Auto-generates help text based on the interface
  • Ability to define additional custom help text for each option
  • Ability to define custom value formatters for each option. Commando comes with the following pre-built formatters:
    • String (default)
    • Digits-only String
    • Numeric (allows decimal, hexadecimal, binary, octal integers)
  • Ability to specify positional arguments

Basic Usage

MyApp.py:

from Cli import Cli
from Cli import option

class MyOptions(object):
   @option
   def getFilePath(self): pass

   @option
   def isRecursive(self): pass

myOptions = Cli(MyOptions).parseArguments()
print('filePath:', myOptions.getFilePath())
print('isRecursive:', myOptions.isRecursive())

Invoking the above with various arguments produces the following results:

python MyApp.py
filePath: None
isRecursive: False

python MyApp.py --filePath C:/some/path
filePath: C:/some/path
isRecursive: False

python MyApp.py --filePath C:/some/path --recursive
filePath: C:/some/path
isRecursive: True

Inheriting Options

You can have a hierarchy of interfaces defining your command line arguments. This is useful where you want to group sets of options so that their definitions can be reused by other Python applications.

MyApp.py:

from Cli import Cli
from Cli import option

class FilePathOptions(object):
   @option
   def getFilePath(self): pass

class MyOptions(FilePathOptions):
   @option
   def isRecursive(self): pass

myOptions = Cli(MyOptions).parseArguments()
print('filePath:', myOptions.getFilePath())
print('isRecursive:', myOptions.isRecursive())

Invoking the above with various arguments produces the following results:

python MyApp.py
filePath: None
isRecursive: False

python MyApp.py --filePath C:/some/path
filePath: C:/some/path
isRecursive: False

python MyApp.py --filePath C:/some/path --recursive
filePath: C:/some/path
isRecursive: True

Defining Short Names

As well as supplying each argument using its long name (e.g. --filePath), you can also define a short name for each method that is accessed by prefixing the short name with a single dash '-'. Any method defining a short name can be accessed by either its long or short name.

MyApp.py:

from Cli import Cli
from Cli import option

class MyOptions(object):
   @option(shortName='f')
   def getFilePath(self): pass

   @option(shortName='r')
   def isRecursive(self): pass

myOptions = Cli(MyOptions).parseArguments()
print('filePath:', myOptions.getFilePath())
print('isRecursive:', myOptions.isRecursive())

Invoking the above with various arguments produces the following results:

python MyApp.py
filePath: None
isRecursive: False

python MyApp.py --filePath C:/some/path
filePath: C:/some/path
isRecursive: False

python MyApp.py -f C:/some/path
filePath: C:/some/path
isRecursive: False

python MyApp.py --filePath C:/some/path --recursive
filePath: C:/some/path
isRecursive: True

python MyApp.py -f C:/some/path -r
filePath: C:/some/path
isRecursive: True

Defining A Default Value

By default, a missing (non-boolean) option will return None. This behaviour can be overridden by defining a default value.

MyApp.py:

from Cli import Cli
from Cli import option

class MyOptions(object):
   @option(default='D:/default/path')
   def getFilePath(self): pass

   @option
   def isRecursive(self): pass

myOptions = Cli(MyOptions).parseArguments()
print('filePath:', myOptions.getFilePath())
print('isRecursive:', myOptions.isRecursive())

Invoking the above with various arguments produces the following results:

python MyApp.py
filePath: D:/default/path
isRecursive: False

python MyApp.py --filePath C:/some/path
filePath: C:/some/path
isRecursive: False

python MyApp.py --filePath C:/some/path --recursive
filePath: C:/some/path
isRecursive: True

Marking Options As Mandatory

You may want to mark some options as mandatory (i.e. they must be supplied).

MyApp.py:

from Cli import Cli
from Cli import option

class MyOptions(object):
   @option(mandatory=True)
   def getFilePath(self): pass

   @option
   def isRecursive(self): pass

myOptions = Cli(MyOptions).parseArguments()
print('filePath:', myOptions.getFilePath())
print('isRecursive:', myOptions.isRecursive())

Invoking the above without the mandatory option produces the following results:

python MyApp.py --recursive
Cli.CliParseError: Missing mandatory option --filePath

Marking Options As Multi-Valued

You may want to mark some options as multi-valued (i.e. they can have more than one value supplied). The value(s) of a multi-valued option are always returned as a list. By default multi-valued options can be omitted entirely or be supplied with an unlimited number of values. You can optionally define the min**imum and **max**imum number of values for each multi-valued option. Specifying a minimum number of values effectively makes the option **mandatory.

MyApp.py:

from Cli import Cli
from Cli import option

class MyOptions(object):
   @option(multivalued=True, min=1, max=2)
   def getFilePath(self): pass

   @option
   def isRecursive(self): pass

myOptions = Cli(MyOptions).parseArguments()
print('filePath:', myOptions.getFilePath())
print('isRecursive:', myOptions.isRecursive())

Invoking the above without the mandatory option produces the following results:

python MyApp.py
Cli.CliParseError: Multi-valued option --filePath must be given with at least 1 value(s)

python MyApp.py --filePath C:/some/path
filePath: ['C:/some/path']
isRecursive: False

python MyApp.py --filePath C:/some/path C:/some/other/path
filePath: ['C:/some/path', 'C:/some/other/path']
isRecursive: False

python MyApp.py --filePath C:/some/path C:/some/other/path C:/too/many/paths
Cli.CliParseError: Multi-valued option --filePath cannot have more than 2 values

Default Help Text

You may want to show some help text to the user of your application. Commando automatically generates some default help text from the supplied interface. The help text can be obtained by specifying the --help (or -?) command line argument, in which case the library will raise a CliHelpError exception that contains the help text. The usage text can also be obtained programatically.

MyApp.py:

from Cli import Cli
from Cli import option

class MyOptions(object):
   @option(multiValued=True, min=2, max=4, shortName='f')
   def getFilePath(self): pass

   @option(mandatory=True)
   def getUserName(self): pass

   @option(default='ReadMe.txt')
   def getReadMe(self): pass

   @option
   def isRecursive(self): pass

cli = Cli(MyOptions)
print('Help from cli:')
print(cli.helpText)
print('-'*10)
myOptions = cli.parseArguments()
print('Help from myOptions:')
print(myOptions.helpText)
print('-'*10)

Invoking the above without the mandatory option produces the following results:

python MyApp.py -f C:/some/path C:/some/other/path --userName joe
Help from cli:
Usage: MyApp.py --filePath, -f value1 ... [--readMe value] --userName value [--recursive]
where:
--filePath,  -f value1 valueMin2 ... valueMax4                                     
--readMe        value                          (default='ReadMe.txt')              
--userName      value                                                              
--recursive                                    (True if specified, otherwise False)
----------
Help from myOptions:
Usage: MyApp.py --filePath, -f value1 ... [--readMe value] --userName value [--recursive]
where:
--filePath,  -f value1 valueMin2 ... valueMax4                                     
--readMe        value                          (default='ReadMe.txt')              
--userName      value                                                              
--recursive                                    (True if specified, otherwise False)
----------

python MyApp.py --help
Help from cli:
Usage: MyApp.py --filePath, -f value1 ... [--readMe value] --userName value [--recursive]
where:
--filePath,  -f value1 valueMin2 ... valueMax4                                     
--readMe        value                          (default='ReadMe.txt')              
--userName      value                                                              
--recursive                                    (True if specified, otherwise False)
----------
Cli.CliHelpError: Usage: MyApp.py --filePath, -f value1 ... [--readMe value] --userName value [--recursive]
where:
--filePath,  -f value1 valueMin2 ... valueMax4                                     
--readMe        value                          (default='ReadMe.txt')              
--userName      value                                                              
--recursive                                    (True if specified, otherwise False)

Customised Help Text

You can customise the help text by supplying a doc comment to each method in your interface.

MyApp.py:

from Cli import Cli
from Cli import option

class MyOptions(object):
   @option(multiValued=True, min=2, max=4, shortName='f')
   def getFilePath(self):
      'list of file paths to process'
      pass

   @option(mandatory=True)
   def getUserName(self):
      'user name to log all operations against'
      pass

   @option(default='ReadMe.txt')
   def getReadMe(self):
      'name of read me text file'
      pass

   @option
   def isRecursive(self):
      'recurse into each file path when looking for read me text files'
      pass

cli = Cli(MyOptions)
print(cli.helpText)

Invoking the above produces the following results:

python MyApp.py
Usage: MyApp.py --filePath, -f value1 ... [--readMe value] --userName value [--recursive]
where:
--filePath,  -f value1 valueMin2 ... valueMax4                                      list of file paths to process
--readMe        value                          (default='ReadMe.txt')               name of read me text file
--userName      value                                                               user name to log all operations against
--recursive                                    (True if specified, otherwise False) recurse into each file path when looking for read me text files

Formatting Values

By default Commando formats and returns all the values specified by the command line arguments as strings. This behaviour can be customised by supplying your own Value Formatter for each option. A Value Formatter is basically a function that is called with the long name of the option whose value needs to be formatted as well as the value specified on the command line as a string. You should raise a CliParseError if the value does not satisfy your particular syntax. The value returned by the Value Formatter can be of any type (including an instance of a Python class). Commando comes with the following built-in value formatters:

  • Cli.STRING_VALUE_FORMATTER: This is the default Value Formatter and simply returns the value passed into it.
  • Cli.DIGIT_STRING_VALUE_FORMATTER: This ensures the value contains only the digits 0 to 9 (inclusive).
  • Cli.NUMERIC_VALUE_FORMATTER: This returns the value as an integer. It allows you to specify the integer as either a decimal, hexadecimal, binary or octal string.

MyApp.py:

import os.path
from Cli import Cli
from Cli import CliParseError
from Cli import option

def pathMustExistValueFormatter(optionName, value):
   'Ensures value contains an existing path.'
   if os.path.exists(value):
      return value
   else:
      raise CliParseError('The path "%s" specified for option %s does not exist' % (value, optionName))

class MyOptions(object):
   @option(shortName='f', valueFormatter=pathMustExistValueFormatter)
   def getFilePath(self): pass

   @option
   def isRecursive(self): pass

myOptions = Cli(MyOptions).parseArguments()
print('filePath:', myOptions.getFilePath())
print('isRecursive:', myOptions.isRecursive())

Invoking the above with various arguments produces the following results:

python MyApp.py
filePath: None
isRecursive: False

python MyApp.py -f C:/some/non/existent/path
Cli.CliParseError: The path "C:/some/non/existent/path" specified for option --filePath does not exist

Positional Arguments

All options defined in the interface are by default optional (except for options marked as mandatory or any multiValued options that also have a min specified). Positional arguments are described very well in the standard Python documentation for the optparse module. I have reproduced some of it here for convenience:

--- start of extract ---

Lots of people want their programs to have “required options”. Think about it. If it’s required, then it’s not optional! If there is a piece of information that your program absolutely requires in order to run successfully, that’s what positional arguments are for.

As an example of good command-line interface design, consider the humble cp utility, for copying files. It doesn’t make much sense to try to copy files without supplying a destination and at least one source. Hence, cp fails if you run it with no arguments. However, it has a flexible, useful syntax that does not require any options at all:

cp SOURCE DEST
cp SOURCE ... DEST-DIR

You can get pretty far with just that. Most cp implementations provide a bunch of options to tweak exactly how the files are copied: you can preserve mode and modification time, avoid following symlinks, ask before clobbering existing files, etc. But none of this distracts from the core mission of cp, which is to copy either one file to another, or several files to another directory.

Positional arguments are for those pieces of information that your program absolutely, positively requires to run.

A good user interface should have as few absolute requirements as possible. If your program requires 17 distinct pieces of information in order to run successfully, it doesn’t much matter how you get that information from the user—most people will give up and walk away before they successfully run the program. This applies whether the user interface is a command-line, a configuration file, or a GUI: if you make that many demands on your users, most of them will simply give up.

In short, try to minimize the amount of information that users are absolutely required to supply—use sensible defaults whenever possible. Of course, you also want to make your programs reasonably flexible. That’s what options are for. Again, it doesn’t matter if they are entries in a config file, widgets in the “Preferences” dialog of a GUI, or command-line options—the more options you implement, the more flexible your program is, and the more complicated its implementation becomes. Too much flexibility has drawbacks as well, of course; too many options can overwhelm users and make your code much harder to maintain.

--- end of extract ---

You specify positional arguments by using the Cli.positional decorator. This decorator has one mandatory parameter - a relative position for the argument (any positive or negative integer value) and one optional parameter - the valueFormatter as described above. The relative position is used to order the positional arguments specified in your interface. Boolean positional arguments can only be supplied with the values true or false (case insensitive).

MyApp.py:

from Cli import Cli
from Cli import option
from Cli import positional

class MyOptions(object):
   @option
   def getFilePath(self): pass

   @option
   def isRecursive(self): pass

   @positional(1)
   def getUserName(self): pass

   @positional(2)
   def getUserPassword(self): pass

   @positional(3)
   def isNewUser(self): pass

myOptions = Cli(MyOptions).parseArguments()
print('filePath:', myOptions.getFilePath())
print('isRecursive:', myOptions.isRecursive())
print('userName:',myOptions.getUserName())
print('userPassword:',myOptions.getUserPassword())
print('newUser:',myOptions.isNewUser())

Invoking the above with various arguments produces the following results:

python MyApp.py
Cli.CliParseError: Missing values for positional arguments: ['userName', 'userPassword', 'newUser']

python MyApp.py joe
Cli.CliParseError: Missing values for positional arguments: ['userPassword', 'newUser']

python MyApp.py joe secret
Cli.CliParseError: Missing value for last positional argument: ['newUser']

python MyApp.py joe secret wibble
Cli.CliParseError: Invalid boolean value "wibble" for positional argument "newUser". Must be "True" or "False" (case insensitive).

python MyApp.py joe secret true
filePath: None
isRecursive: False
userName: joe
userPassword: secret
newUser: True

python MyApp.py --filePath C:/some/path --recursive joe secret true
filePath: C:/some/path
isRecursive: True
userName: joe
userPassword: secret
newUser: True

Auxiliary Methods

In addition to defining options and positional arguments in a Python interface, Commando also allows you to define auxiliary methods that can be used to build any arbitrary logic. These auxiliary methods can also obtain the command line values by calling the overridden annotated methods.

MyApp.py:

import os

from Cli import Cli
from Cli import positional

class MyOptions(object):
   @positional(1)
   def getFilePath(self): pass

   def listFiles(self):
      for root, dirs, files in os.walk(self.getFilePath()):
         for file in files:
            print(os.path.join(root, file))

myOptions = Cli(MyOptions).parseArguments()
print(myOptions.listFiles())

Assuming C:/some/path contains:

C:/some/path
   |
   + file1
   + file2
   + subDir
       |
       + subFile1

Invoking the above produces the following results:

python MyApp.py --filePath C:/some/path
C:/some/path/file1
C:/some/path/file2
C:/some/path/subPath/subFile1
Something went wrong with that request. Please try again.