The getopt module, like GNU getopt/getopt_long, separates short and long options, and makes no way to coordinate options with usage messages. This leads to an opportunity for usage info and the actual options to diverge, and makes it harder to see, at a glance, what short and long options correspond to one another. It also leaves handling of preset or default options up to ad-hoc solutions, often the same or similar code implemented time and time again. Xgetopt addresses all of these concerns in a simple, easy to use module.This code requires my word_wrap.py module, which is also posted to the Python Cookbook following this module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | #!/usr/bin/env python
#
# xgetopt.py
#
# Copyright (c) 2001 Alan Eldridge. All rights reserved.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the "Artistic License" which is
# distributed with the software in the file LICENSE.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Artistic
# License for more details.
#
# Alan Eldridge 2001-09-15 alane@wwweasel.geeksrus.net
#
# $Id: xgetopt.py,v 1.13 2001/10/14 04:57:58 alane Exp $
#
# 2001-09-15 alane@wwweasel.geeksrus.net
#
import os
import sys
import string
import getopt
import word_wrap
class parser:
"""Simple wrapper around getopt to specify short and long options
together by function, to create a usage message from the options.
"""
__usage_lwid=30
__usage_rwid=40
__usage_width=70
def __init__(self):
"""Set up empty tables for parser."""
self.__app = None
self.__args = None
self.__note = None
self.__s = [] # short opts for getopt
self.__l = [] # long opts for getopt
self.__olist = [] # list of opt def'ns
self.__odict = {} # opt def'ns indexed by opt
self.__usage = None # cached option help message
def set_app(self, app):
self.__app = app
def set_args(self, args):
self.__args = args
def set_note(self, note):
self.__note = note
def add_opt(self, opt, long, arg, key, usage, multi=0):
"""Add a new option to the parser's tables.
opt => short option, e.g., '-s', *not* 's' or 's:'.
long => long option, e.g., '--spam', *not* '--spam='.
arg => descriptive name for arg to option, if it has an
an arg, e.g., 'how-much-spam'.
key => a unique key to identify this option; see
xgetopt.getopt().
usage => usage text for this option; may contain references
to members of the opt_info dictionary such as %(opt)s
and %(arg)s; '\n' is a paragraph separator.
multi => if true, multiple values for the option are distinct;
if false, only the last value is significant. See
xgetopt.getopt() for detail.
"""
self.__usage = None
# Build opt def'n dict
opt_info = {}
opt_info['opt'] = opt
opt_info['long'] = long
opt_info['arg'] = arg
opt_info['key'] = key
opt_info['usage'] = usage
opt_info['multi'] = multi
# Setup for getopt
# Make index entries
if opt:
self.__odict[opt] = opt_info
if arg:
opt = opt + ':'
self.__s.append(opt[1:])
if long:
self.__odict[long] = opt_info
if arg:
long = long + '='
self.__l.append(long[0:])
# Split usage into paragraphs and substitute vars.
# This is gonna die a horrible death if the string
# has bad variable substitutions in it.
if usage:
opt_info['usage'] = string.split(usage % opt_info, '\n\n')
# Add to list of options
self.__olist.append(opt_info)
return opt_info
def getopt(self, args, app_opts = None):
"""Parse string 'args' using saved option info.
Returns a 3-tuple (opts, args, app_opts). Opts and args
are the conventional values returned by getopt.getopt().
App_opts is a dictionary mapping the 'key' given to add_opt()
for each option to the value present on the command line.
1. If the app_opts dictionary is passed in, this gives the
default values for options for the application.
2. Options not present in a passed-in app_opts, and not present
on the command line have a mapped value of None.
2. Options that have the 'multi' flag set have a mapped value
of a list containing all values for the option that were given
on the command line, in the order they were given.
"""
# set up dict for caller
if app_opts is None:
app_opts = {}
for oinfo in self.__olist:
if not app_opts.has_key(oinfo['key']):
app_opts[oinfo['key']] = None
# Do the getopt thing
opts, args = getopt.getopt(args,
string.join(self.__s, ''),
string.join(self.__l, ' '))
# now fill in caller dict
for k, v in opts:
app_key = self.__odict[k]['key']
app_multi = self.__odict[k]['multi']
if app_multi:
if not app_opts[app_key]:
app_opts[app_key] = []
app_opts[app_key].append(v)
else:
app_opts[app_key] = v
# return 3-tuple to caller
return opts, args, app_opts
def usage_msg(self):
"""Generate a help message using saved option info."""
if self.__usage:
return self.__usage
app = self.__app
if not app:
app = sys.argv[0].split(os.sep)[-1]
str = 'Usage: ' + app
if len(self.__olist):
str = str + ' [options]'
if self.__args:
str = str + ' ' + self.__args
self.__usage = map(lambda s: s + '\n', str.split('\n'))
if len(self.__olist):
self.__usage = self.__usage + ['\n', 'Options:\n']
# Loop over options
for oinfo in self.__olist:
lside = rside = ''
# Left side is -x,--xthing xarg
if oinfo['opt']:
lside = lside + oinfo['opt']
if oinfo['long']:
if oinfo['opt']:
lside = lside + ','
lside = lside + oinfo['long']
if oinfo['arg']:
lside = lside + ' ' + oinfo['arg']
lside = [ string.ljust(lside, self.__usage_lwid) ]
# Right side is usage, word wrapped to fit
rside = word_wrap.wrap_list(oinfo['usage'], self.__usage_rwid)
# Pad out extra blank lines on left side
for i in range(len(rside) - 1):
lside.append(string.ljust('', self.__usage_lwid))
# Tack 'em together
self.__usage = self.__usage + map(lambda l, r: l + r + '\n',
lside, rside)
if self.__note:
self.__usage.append('\n')
tmp = map(lambda s: s + '\n',
word_wrap.wrap_str(self.__note, self.__usage_width, '\n\n'))
self.__usage = self.__usage + tmp
return self.__usage
def usage(self, rc):
map(sys.stdout.write, self.usage_msg())
sys.exit(rc)
#
#EOF
##
self.__usage = self.__usage + tmp
|
Command line parsing is one of those necessary evils that we all dislike, and I imagine that most of us has tried to tackle the beast one or more times over our career. GNU's getopt and getopt_long, and the GNU C Library's argp represent some of the high profile solutions. The popt library, used by RedHat's RPM system, is another highly evolved system. Of course, all of these are targeted at the C/C++ programmer. In "scripting" languages, such as Perl and Python, we either use interfaces to these C facilities, or, as in this case, attempt to manage some of the larger issues of complexity in processing program invocation parameters by building on these implementation-language solutions.
I have tackled this problem in various languages over the 20 years of my career, and each new language I learn to use presents new "opportunities" in this area. Python's support for simple, yet extraordinarily powerful, data structures gave me a new look at an old problem.
Xgetopt is a simple solution, designed to address what I perceive as the most important issues facing the programmer in parsing the command line:
- Keeping track of differing ways (long and short options) of specifiying the same information.
- Keeping usage information updated, and in sync with the command line options the program actually accepts.
- Specifying default values for non-specified options.
- Minimizing the impact on other parts of the application by changes in the command line behavior of the application.
Xgetopt tries to follow the approach of doing the simplest thing that will work. I went through at least three major iterations of this module, each time having introduced complexity that was unwarranted. I had settled on only satisfying the first 2 of the 4 objectives I set out to satisfy. I added the associative array for looking up values as a means of addressing (4) above. I then saw that a 2 line change allowed me to hit the last target, (3), default values.
Xgetopt is released under the Artistic License, as found in the latest stable version of Perl (5.6.1). I hope it proves to be a useful addition to Python developers' toolboxes.
Modified 2001/10/14: Uses the new configurable separator in word_wrap to process help text and notes as formatted text with blank lines separating paragraphs.
The latter half of this code is missing. The last half of this code module has been cut off from the display. I am sorry for this. I did not know the interface would do this when actually posting the code. -- Alan Eldridge, author of xgetopt.py
In order to use xgetopt.py. 1. You must get the source using the "Text Source" link near the top of the listing. It contains the full source code.
EXAMPLE OF USE.
2 bugs prevent from using long options, fix included... Hi,
In method add_opt() line 94 change to:
self.__l.append(long[2:]) # in order to remove leading hypens --
In method getopt() line 137 change to:
self.__l # geoopt.getopt expect list of strings # not a single string
<p> Cheers