Mohegan SkunkWorks

Sat, 29 Aug 2009 19:15:49 EST

Simplified command line processing with dyn-options.py

Am I the only one in the world who feels that using python's getopt is a bit of a struggle ? It involves a lot of boiler plate. Tedious refactoring is required each time you add or change an option. This is not specific to Python, as most languages have a similar facility to parse the command line, which is similarly annoying.

I decided to create an easier way to process command line options, by transforming the command line into an immutable (read-only) object. The result is dyn_options.

dyn_options considers every string on the command line which starts with either - or -- (i.e. a single or double dash) an option flag. The value of the option flag is a concatenation of everything that follows it, until the next flag is encountered. A simple option flag is one without explicit values and is considered a boolean flag, set to True. dyn_options creates a read-only object, with attributes and values set to the command line option flags and values respectively.

So, '--opt4 hello world' will be converted to an option flag called opt4, with a value of hello world. This makes dealing with spaces on the command line a lot easier.

Using dyn_options

Here is how an option object is created :

  import dyn_options   
 
  option = dyn_options.create_option(argv, option_defaults())  

If you have defaults, option_defaults() should return a dictionary of key-value pairs, with the key corresponding to the option, and its value to the desired default value.

An easy way to check whether an option is set, is to do something like :

  if option.some_flag :   
          do_something   
           .......   

A few examples

You can play around with example.py to test how various options are handled. Here's the source:

  #!/usr/bin/env python   
  import sys   
  import dyn_options   
 
  def option_defaults() :   
     return dict( [("opt1", "opt1_default"), ("help", False)])   
 
  def main(argv) :   
        option = dyn_options.create_option(argv, option_defaults())   
         print "using defaults :", option   
 
         option = dyn_options.create_option(argv)   
         print "no defaults :", option   
 
             if option.opt4 :   
                 print "opt4 is set :", option.opt4   
           else :   
                print "opt4 is not set"   
 
     if __name__ == '__main__':   
              sys.exit(main(sys.argv))   
 

I create two different option objects. The first one has defaults; The second one doesn't. The output for

    ./example.py --opt2 --opt4 hello world 

is this:

     using defaults : options :   
        #) help ==> False   
            #) program ==> ./example.py   
            #) opt4 ==> hello world   
            #) opt1 ==> opt1_default   
            #) opt2 ==> True   
 
   no defaults : options :   
           #) program ==> ./example.py   
           #) opt4 ==> hello world   
           #) opt2 ==> True   
   opt4 is set : hello world 

When option is initialized with the dictionary returned by option_defaults() ,
opt1 is set to the default value specified for it in the dictionary. In the second case, when no defaults are supplied, it's not set.

Here's the output for : ./example.py --opt1 new_value

  using defaults : options :   
       #) help ==> False   
       #) program ==> ./example.py   
       #) opt1 ==> new_value   
   no defaults : options :   
       #) program ==> ./example.py   
       #) opt1 ==> new_value   
   opt4 is not set 

As you can see, the value of opt1 is now the one provided on the command line, rather than the default.


Immutable/Read-Only

This is one of the tests in dynoptionstest.py . It verifies that the option remains unchanged, after it's been created.

  def test4() :   
  """   
            option is immutable   
     """   
 
     L=['./dyn_options.py', '--opt1', 'opt1_value', '-opt2', 'opt2_value', '-opt3']   
    option = dyn_options.create_option(L, option_defaults())   
    try :   
        assert option.opt1 == "opt1_value"   
        assert option.opt2 == "opt2_value"   
        assert option.opt3 == True   
        assert option.help == False   
 
        #Try to override...   
        option.help = True   
        assert option.help == False   
 
        option.opt2 == "new_opt2_value"   
        assert option.opt2 == "opt2_value"   
 
 
        #Try to add new attribute   
        option.opt55 = "opt55_value"   
        assert option.opt55 == False   
 
     except AssertionError :   
          traceback.print_exc()   
 
          print "Failed test4 : parsing ", str(L)   
          print "generated : ", option   
          print "internals : ", option.__repr__()   
          return -1   
 
    print "pass test4"   
    return 0 

You can't set additional attributes, nor override existing ones. This seems reasonable to me. Once your options are set, they should remain so. Notice that I don't throw an exception when try to override the value of option opt2.


Internals

How does all of this work ? Very simple : The command line is converted to a dictionary, which in turn is used to initialize the internal dictionary dict of the options object. I also override the getattr and setattr methods. Those are used to 'get' and 'set' elements of the internal dictionary, and need to be overridden to make the object read-only.

Enjoy.