CMDSyntax: Bake-off

Home David CMDSyntax Bake-off
Updated: 2002-08-07

Introduction

In February 2002, Greg Ward posted to the Python getopt-sig mailing list announcing his comparison of option-parsing libraries in which three scripts were written to implement an example command line interface using each of the libraries under examination. This generated a certain amount of discussion on the mailing list concerning the design philosophy of each of the candidates presented in the comparison. However, some of the issues raised in the discussion were never resolved to the satisfaction of all parties involved, possibly due to the fundamental differences in opinion between contributors to the list.

In May 2002, Guido van Rossum suggested that the candidates should be updated to reflect some of the discussion which followed the earlier comparison.

Having written an option-parsing library of my own, I wrote a script to implement the example interface used in the comparison. However, my library did not scale well to large numbers of options and I had to perform more work on it before it could be publically released. I was confident enough in the library's capabilities to include a version of the example script in the library distribution, so I feel that it can now be compared with the other candidates.

Update: The library was not formally compared with the others because Optik was accepted into future releases of the Python standard library. Some of this work may yet be incorporated into other projects.

The submission

A version of the following example is included in the CMDSyntax library, but the latest version is available to be downloaded separately. I have annotated the example on this page to help explain the approach used by the library.

The script begins by importing the library and the sys module. The syntax accepted by the script is defined in a string which is, in this case, rather long. Note that square brackets are used to denote optional arguments and the vertical bar is used as an exclusive OR operator.


  #! /usr/bin/env python
  
  import cmdsyntax, sys
  
  # Define the syntax: note that the CMDSyntax library doesn't support the
  # usage of the -v option which allows -vv and -vvv, etc.
  
  syntax = """
    [-h | --help] | [--version] |
    (
      [
        (-v | --verbose=V) | (-q | --quiet)
      ]
      
      [( -d DEVICE) | (--device=DEVICE)]
      [(-b DIR) | --output-base=DIR]
    
      [(-t TRACKS) | --tracks=TRACKS]
    
      [
        (-p | --use-pipes | --synchronous)
        |
        (
          (-f | --use-files | --asynchronous)
          [
              (-k | --keep-tmp) | --no-keep-tmp
              [-R | --rip-only]
          ]
        )
      ]
      
      [-P | --playback]
      [-e | --eject]
      [(-A ARTIST) | --artist=ARTIST]
      [(-L ALBUM) | --album=ALBUM]
      [(-D DISC) | --disc=DISC]
      [(-y YEAR) | --year=YEAR]
      [--offset=OFFSET]
    )
  """
  

Although the example syntax contains lots of options which may be omitted, some of them are synonyms, and others are contradictory when used in conjunction. I have avoided the issue of aliased commands setting options by default to be overridden by options explicitly set by the user.

  
  if __name__ == "__main__":
  
      # Create a syntax object.
      syntax_obj = cmdsyntax.Syntax(syntax)
      

Inside the main namespace, an object is created which parses the syntax definition and constructs a tree structure for subsequent use.

      
      # Match the command line arguments against the syntax definition.
      matches = syntax_obj.get_args(sys.argv[1:])
      

The input from the command line is passed to the relevant method and compared with the syntax definition. A list of matches is returned; this will be empty if no valid matches were found.

This next part is optional, as it concerns the optional creation of a graphical form interface on supported systems in the case that no valid matches were returned.

      
      if matches == [] and cmdsyntax.use_GUI() != None:
          
          print "Using the GUI..."
          print
          
          form = cmdsyntax.Form(sys.argv[0], syntax_obj)
          
          match = form.get_args()
      
          # A valid matches was found.
          if match != {}:
          
              # Put the unambiguous match in a list so that it can be dealt
              # with in the same manner as matches from command line input.
              matches = [match]
      

If a valid match was returned from the form, it is placed inside a list for compatibility with the rest of the script.

      
      if matches == []:
      
          # No matches found: print the syntax definition and exit.
          
          print "Syntax: %s %s" % (sys.argv[0], syntax)
          sys.exit()
      

The availability of valid matches is checked and, if none are found, the syntax definition is printed on exit.

      
      # At this point, there may be a number of matches if the syntax
      # specification was ambiguous. Assuming that it wasn't, take the
      # first match.
      
      match = matches[0]
      

The first match presented is used.

      
      # Note that the CMDSyntax library does not coerce command line arguments
      # to types; this is up to the application. Using the source for the
      # Optik solution to ripoff, we can determine which arguments have
      # types other than the string type.
      
      def coerce_using(value, fn):
      
          try:
              return fn(value)
          
          except ValueError:
          
              print "Could not coerce %s using %s." % (value, fn)
              return value
      
      

The library does not coerce arguments from the command line to types automatically, so a function is defined to do this in a simple manner.

      
      # Integer types:
      
      if match.has_key("verbose"):
      
          match["verbose"] = coerce_using(match["verbose"], int)
      
      if match.has_key("disc"):
      
          match["disc"] = coerce_using(match["disc"], int)
      
      if match.has_key("DISC"):
      
          match["DISC"] = coerce_using(match["DISC"], int)
      
      if match.has_key("offset"):
      
          match["offset"] = coerce_using(match["offset"], int)
      
      

The relevant arguments are coerced to integers if possible. Note that the values are kept in a dictionary.

      
      print match
      
      sys.exit()
      

The match dictionary is printed and the script exits.

Contacting me

You can contact me or read the discussion at http://mail.python.org/pipermail/getopt-sig/.