##
## altopt.py
##
## Alternative command line options parsing library.
##
## by Michael J. Fromberger
## Copyright (C) 2003 Michael J. Fromberger
##
## Permission is hereby granted, free of charge, to any person
## obtaining a copy of this software and associated documentation
## files (the "Software"), to deal in the Software without
## restriction, including without limitation the rights to use, copy,
## modify, merge, publish, distribute, sublicense, and/or sell copies
## of the Software, and to permit persons to whom the Software is
## furnished to do so, subject to the following conditions:
##
## The above copyright notice and this permission notice shall be
## included in all copies or substantial portions of the Software.
##
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
## IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
## CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
## TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
## SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
##
## $Id: altopt.py,v 1.10 2003/08/27 19:53:44 sting Exp $
##
## The option parsing strategy used here is based on the format used
## by the AFS (Andrew File System) tools, although I have not tried to
## make them identical by any means.
##
## There are two types of options:
## -single : an option which is selected by being present, and
## consumes no additional command line arguments
##
## -multiple : an option which consumes one or more additional
## arguments.
##
## ---- Basic usage:
##
## 1. Create an Options object
##
## 2. Add options to the object using its methods:
## .add_option(), .add_single(), .add_multiple()
## [optionally] Set help text using .set_help()
## [optionally] Set a free-argument map using .set_free_map()
## [optionally] Set min/max numbers of free args using .set_min_free()
## and .set_max_free()
##
## 3. Parse a command line using the .parse() method.
##
## ---- Alternate usage:
##
## 1. Call create_from_strings() with a list of option specs.
## This returns a new Options object.
##
## 2. Parse a command line using the .parse() method.
##
## ---- Other Things to Know:
##
## Each Options object has a string that signals the end of option
## processing when it is encountered. By default, it is `--', but you
## can set it using the .set_end() method. The terminating string
## MUST begin with a dash (-); one will be added if it doesn't.
##
## Each Options object has a string that "escapes" the following
## argument when it is encountered. By default, it is `-esc', but you
## can set it using the .set_escape() method. The escape string tells
## the parser to take the following argument "as is" even if it
## otherwise looks like an option selector.
##
## Any unique prefix of an option name suffices to match that option
## when parsing. So, for instance, if there are options named "aaron"
## and "aardvark", -aard would be a valid match for "aardvark" (as
## would -aardv, -aardva, and -aardvar) while -aaro would be a valid
## match for "aaron". But -a, -aa, and -aar are ambiguous.
##
## The string `-' is special, and is not treated as an option (i.e.,
## when encountered, it is treated as a plain argument).
##
## In case of errors, an OptError is thrown.
##
import sre as _sre
__version__ = "$Revision: 1.10 $".split(' ')[1]
class AltOptError ( Exception ):
"""The superclass for this library's exceptions."""
class OptError ( AltOptError ):
"""This class represents errors in option processing."""
class CommandError ( AltOptError ):
"""This class represents errors in command processing."""
class Options:
"""This class represents a collection of command line options.
After creating an instance of the object, use the .add_single(),
.add_multiple(), and .add_option() methods to add options to
the collection. See also the create_from_strings() function.
If the vald argument is true, then only valid (recognized) options
will signal the end of an argument list for a multiple option. If
vald is false, anything that "looks like" an option (i.e., begins
with a dash and is not the escape- or option-ending string) will
signal the end of the options."""
def __init__(self, vald = False):
"""Create a new empty collection of options."""
self.__opts = {} # Primary option information
self.__nmap = None # Prefix mapping
self.__eopt = '--' # Marker for end of options
self.__esc = '-esc' # Escapes the next argument
self.__vald = vald # Validate option-like strings?
self.__fmap = None # Maps free arguments to multiple options
# Other option information...
self.__info = { 'min_free': 0, 'max_free': -1 }
def __str__(self):
"""The string form of an Options object can be used as part of
a help message, as it gives a summary of each of the options
currently known, along with their help messages (if any)."""
if not self.__opts:
return ''
max_len = max([ len(name) for name in self.__opts.keys() ]) + 1
singles = filter(lambda n: self.__opts[n]['type'] == 'single',
self.__opts.keys())
others = filter(lambda n: self.__opts[n]['type'] <> 'single',
self.__opts.keys())
singles.sort()
others.sort()
return str.join('\n',
[ self.__fmt_opt(max_len, name)
for name in singles ] +
[ self.__fmt_opt(max_len, name)
for name in others ])
def __fmt_opt(self, width, name):
info = self.__opts[name]
single_fmt = " %%-%ds : %%s" % width
multiple_fmt = " %%-%ds\t%%s\n%s : %%s" % \
(width, ' ' * (width + 2))
if info.has_key('help') and info['help']:
help_str = info['help']
else:
help_str = ''
if info['type'] == 'single':
return single_fmt % ('-' + info['name'], help_str)
if info.has_key('argkeys'):
arg_keys = info['argkeys']
else:
arg_keys = [ 'arg' ]
args = []
arg_num = 0
while arg_num < info['min_args']:
if arg_num >= len(arg_keys):
key_string = arg_keys[-1] + \
str(arg_num + 2 - len(arg_keys))
else:
key_string = arg_keys[arg_num]
arg_num += 1
args.append('<' + key_string + '>')
opt_args = []
if info['max_args'] >= 0:
while arg_num < info['max_args']:
if arg_num >= len(arg_keys):
key_string = arg_keys[-1] + \
str(arg_num + 2 - len(arg_keys))
else:
key_string = arg_keys[arg_num]
arg_num += 1
opt_args.append('<' + key_string + '>')
else:
if info['min_args'] == 0:
args.append('[%s]' % arg_keys[0])
args.append('...')
base = str.join(' ', args)
if opt_args:
for arg in opt_args:
base += ' [' + arg
base += ']' * len(opt_args)
if info['required']: help_str = '[required] ' + help_str
return multiple_fmt % ('-' + info['name'], base, help_str)
def __build_map(self):
self.__nmap = _make_prefix_tab(self.__opts.keys())
def set_escape(self, esc):
"""Set the option escape string to esc. Default is `-esc'."""
self.__esc = esc
def set_end(self, end):
"""Set the option terminating string to end. Default is `--'."""
if end[0] <> '-':
self.__eopt = '-' + end
else:
self.__eopt = end
def set_min_free(self, min):
"""Set the minimum number of required free arguments (0 means
no free arguments are required, and is the default)."""
self.__info['min_free'] = min
def set_max_free(self, max):
"""Set the maximum number of allowable free arguments (-1
means no upper bound, and is the default)."""
self.__info['max_free'] = max
def set_free_map(self, map):
"""Maps free arguments to option names. The 'map' argument
must be a list of strings, corresponding to the names of
multiple-type options (which need not exist a priori). If the
last element of map begins with a '-', all remaining
unconsumed free arguments will be assigned to that option.
When the free map is set, options corresponding to the free
arguments will be appended to the parsed options returned by
.parse(), and those free arguments will NOT appear in the free
arguments list.
The free map is processed BEFORE minimum and maximum numbers
of free arguments are checked."""
assert(type(map) == list and len(map) > 0)
self.__fmap = map[:]
def clear_free_map(self):
"""Get rid of the free map."""
self.__fmap = None
def add_single(self, name, help = None, max_count = 0):
"""Add a zero-argument option with the given name. If help is
not None, the help text is set. If max_count > 0, the
argument may be repeated multiple times, up to the given
maximum; if max_count is zero (the default) it may be repeated
as many times as desired."""
name = name.lower()
self.__opts[name] = { 'name': name,
'type': 'single',
'max_count': max_count }
if help:
self.set_help(name, help)
self.__build_map()
def add_multiple(self, name, min_args = 1, max_args = 1,
help = None, arg_keys = None,
required = False, max_count = 0):
"""Add a multiple-argument option with the given name. At least
min_args arguments are required, and at most max_args arguments
will be consumed. If max_args < 0, then as many arguments as are
available will be consumed (up to another valid option).
If required is True, then this option MUST be present at least
one time. If max_count > 0, it will only be accepted at most
that number of times (by default, max_count = 0, and the
option may be repeated any number of times)."""
name = name.lower()
self.__opts[name] = { 'name': name,
'type': 'multiple',
'min_args': min_args,
'max_args': max_args,
'required': required,
'max_count': max_count }
if help:
self.set_help(name, help)
if arg_keys:
self.set_arg_keys(name, arg_keys)
self.__build_map()
def set_help(self, name, help):
"""Set the help string for the given option name."""
if not self.__opts.has_key(name):
raise OptError("No such option: %s" % name)
self.__opts[name]['help'] = help
def set_arg_keys(self, name, keys):
"""Set the argument key(s) for the given option name."""
if not self.__opts.has_key(name):
raise OptError("No such option: %s" % name)
elif self.__opts[name]['type'] <> 'multiple':
raise OptError("Cannot set argument keys of single option: %s" %
name)
if type(keys) == list:
self.__opts[name]['argkeys'] = keys
else:
self.__opts[name]['argkeys'] = [ keys ]
def add_option(self, info):
"""Add an option by specifing a dictionary of the relevant
information. At minimum, you must supply the 'name' and
'type' keys; if the type is 'multiple', you must also provide
'min_args', 'max_args', 'required', and 'max_count'. You may
also give help text ('help') and other data (e.g., 'argkeys')
as you see fit."""
if type(info) <> dict:
raise TypeError("Incorrect type for 'info' parameter "
"(wanted dict)")
if not (info.has_key('name') and info.has_key('type')):
raise OptError("Incorrect format for option specification")
if info['type'] == 'multiple' and \
not (info.has_key('min_args') and info.has_key('max_args') and \
info.has_key('required') and info.has_key('max_count')):
raise OptError("Incorrect format for multiple option")
self.__opts[info['name'].lower()] = info
self.__build_map()
def test_option(self, key):
"""Test whether the given string uniquely identifies some
option. If so, the option info for the matching option is
returned (as a dictionary). If the string matches more than
one option, a list of the matching option names is returned.
Otherwise, False is returned."""
if not key or not self.__nmap:
return False
name = key
if name[0] == '-':
name = name[1:]
ambig = {}
for (pfx, opt) in self.__nmap.items():
if name.find(pfx) == 0 and \
opt.find(name) == 0:
return self.__opts[opt]
elif pfx.find(name) == 0 and \
opt.find(name) == 0:
ambig[opt] = True
if ambig:
return ambig.keys()
else:
return False
def parse(self, args):
"""Given a list of strings, attempt to parse command line
options. Returns a tuple of (parsed_opts, free_args). The
parsed_opts is a list of options and their arguments;
free_args is a list of the strings that did not belong to
any other option, in the same order they occured in the
input list.
Each element of parsed_opts is a pair of (name, args). For
single options, args is None; for multiple options, args is
a list of the arguments associated with that option. The
list may be empty."""
free_args = []
parsed_opts = []
esc = False
while args:
if esc or args[0] == '-' or args[0][0] <> '-':
free_args.append(args.pop(0))
esc = False
continue
# If this is the escape string, get rid of it and set the escape
# flag for the next argument...
if args[0] == self.__esc:
esc = True
args.pop(0)
continue
# If this is the end-of-options string, stop processing here
if args[0] == self.__eopt:
args.pop(0)
break
match = self.test_option(args[0])
if not match:
raise OptError("Option '%s' not understood" % args[0])
elif type(match) == list:
raise OptError("Ambiguous option '%s' (%s)" %
(args[0], str.join(', ', match)))
if match['type'] == 'single':
parsed_opts.append((match['name'], None))
args.pop(0)
elif match['type'] == 'multiple':
args.pop(0)
min_args = match['min_args']; max_args = match['max_args']
opt_args = []
while args and \
(max_args < 0 or len(opt_args) < max_args):
if esc or args[0] == '-' or args[0][0] <> '-':
opt_args.append(args.pop(0))
esc = False
elif args[0] == self.__esc:
esc = True
args.pop(0)
else:
# If the next thing is a valid option, then
# stop gathering arguments; otherwise, grab
# it and put it into the collection
if args[0] == self.__eopt or \
type(self.test_option(args[0])) == dict or \
not self.__vald:
break
else:
opt_args.append(args.pop(0))
if min_args >= 0 and len(opt_args) < min_args:
raise OptError("Insufficient arguments for '%s' "
"(need at least %d)" %
(match['name'], min_args))
parsed_opts.append((match['name'], opt_args))
else:
raise OptError("Option type '%s' not understood for %s" %
(match['type'], match['name']))
if args:
free_args.extend(args)
# If there is a free map, do the right thing
if self.__fmap is not None:
(parsed_opts, free_args) = self.map_free(parsed_opts,
free_args)
# This function does not return in case of errors.
self.check_results(parsed_opts, free_args)
return (parsed_opts, free_args)
def map_free(self, parsed_opts, free_args):
map = zip(self.__fmap, free_args)
if len(map) == 0:
return (parsed_opts, free_args)
free_args = free_args[len(map):]
# Append the regular items to the parsed options...
for (key, arg) in map[:-1]:
parsed_opts.append((key, [ arg ] ))
# If the last element of the free map has a leading dash,
# that option consumes all the remaining free arguments, if any
if(map[-1][0][0] == '-'):
map[-1] = (map[-1][0][1:],
[ map[-1][1] ] + free_args)
free_args = []
parsed_opts.append(map[-1])
else:
parsed_opts.append((map[-1][0], [ map[-1][1] ]))
return (parsed_opts, free_args)
def check_results(self, parsed_opts, free_args):
# If there's a minimum number of free arguments, enforce it.
if self.__info['min_free'] > 0 and \
len(free_args) < self.__info['min_free']:
raise OptError("Insufficient free arguments "
"(need at least %d, got only %d)" %
(self.__info['min_free'], len(free_args)))
# If there's a maximum number of free arguments, enforce it.
if self.__info['max_free'] >= 0 and\
len(free_args) > self.__info['max_free']:
raise OptError("Too many free arguments "
"(want at most %d, got %d)" %
(self.__info['max_free'], len(free_args)))
# Check each option to make sure it's okay (present if
# required, not repeated too many times, etc.)
for info in self.__opts.values():
found = find_all_option(info['name'], parsed_opts, merge = False)
if info['type'] == 'multiple':
if info['required'] and not found:
raise OptError("Required option '%s' is missing" %
info['name'])
if found and info['max_count'] > 0 and \
len(found) > info['max_count']:
if info['max_count'] == 1:
plur = ''
else:
plur = 's'
raise OptError("Option '%s' may occur at most %d "
"time%s (not %d)" %
(info['name'],
info['max_count'],
plur,
len(found)))
# Falling off the end of this function is sufficient to assure
# that the checks passed...
class Commands:
"""This class represents a set of subcommands, each of which has
its own set of distinct options. Create an instance of this class
and add commands to it using .add_command(), use the .get_options()
method to fetch the Options object associated with a particular
command.
Use the .parse() method to parse a sequence of strings."""
def __init__(self, *cmds):
self.__cmds = {}
if cmds:
for cmd in [ c.lower() for c in cmds ]:
self.__cmds[cmd] = { 'name': cmd, 'options': Options() }
self.__build_map()
def __str__(self):
names = self.__cmds.keys()
names.sort()
max_len = max([ len(c) for c in names ]) + 1
return str.join('\n', [ self.__fmt_cmd(max_len, c)
for c in [ self.__cmds[n] for n in names ] ])
def __fmt_cmd(self, width, cmd):
out_fmt = " %%-%ds : %%s" % width
if cmd.has_key('help'):
help_str = cmd['help']
else:
help_str = ""
return out_fmt % (cmd['name'], help_str)
def __build_map(self):
self.__nmap = _make_prefix_tab(self.__cmds.keys())
def add_command(self, cmd, opts = None, help = None):
cmd = cmd.lower()
if not self.__cmds.has_key(cmd):
self.__cmds[cmd] = { 'name': cmd }
if not opts:
self.__cmds[cmd]['options'] = Options()
else:
self.set_options(cmd, opts)
if help:
self.set_help(cmd, help)
self.__build_map()
def get_options(self, cmd):
return self.__cmds[cmd]['options']
def set_options(self, cmd, opts):
if isinstance(opts, Options):
self.__cmds[cmd]['options'] = opts
elif type(opts) == list:
self.__cmds[cmd]['options'] = create_from_strings(opts)
else:
raise TypeError("invalid type for opts argument")
def set_help(self, cmd, help):
self.__cmds[cmd]['help'] = help
def get_help_line(self, cmd):
"""Return a descriptive help line for the named command."""
match = self.test_command(cmd)
if type(match) == list:
raise CommandError("ambiguous command '%s' "
"(matches %s)" %
(cmd, str.join(', ', match)))
elif not match:
raise CommandError("no such command: %s" % cmd)
max_width = max([ len(c) for c in self.__cmds.keys() ])
return self.__fmt_cmd(max_width, match)
def test_command(self, cmd):
"""Test whether the given string uniquely identifies a
command. If so, its record is returned. If the command is
ambiguous, a list of matching command names are returned;
otherwise, False."""
ambig = {}
for (pfx, name) in self.__nmap.items():
if cmd.find(pfx) == 0 and \
name.find(cmd) == 0:
return self.__cmds[name]
elif pfx.find(cmd) == 0 and \
name.find(cmd) == 0:
ambig[name] = 1
if ambig:
return ambig.keys()
else:
return False
def get_command_names(self):
return self.__cmds.keys()
def parse(self, args):
if not args:
cmd = ''
else:
cmd = args.pop(0)
match = self.test_command(cmd)
if not match:
raise CommandError("no such command: %s" % cmd)
elif type(match) == list:
raise CommandError("ambiguous command '%s' (matches %s)" %
(cmd, str.join(', ', match)))
(parsed_opts, other_args) = match['options'].parse(args)
return {'command': match['name'],
'options': parsed_opts,
'other': other_args }
def find_option(key, opts):
"""Find the given option in a parsed list of options; returns
either the first instance found as a tuple of (name, value), or
False if the option was not found. The name is a string, the
value is either None (in the case of single options) or a list of
values (in the case of multiple options)."""
for opt in opts:
if key == opt[0]:
return opt
return False
def find_all_option(key, opts, merge = False):
"""Like find_option(), but returns a list of tuples containing all
the occurrences of the given option. If merge is true, then the
arguments for repeated instances of multiple options will all be
folded into a single list, and returned as (name, list); while
repeated occurrences of single options will be counted up and
returned as a single tuple of the form (name, count).
In the event the option is not found, if merge is True, this
function returns None; otherwise the empty list is returned."""
out = []
for opt in opts:
if key == opt[0]:
out.append(opt)
if merge and out:
single = (out[0][1] == None)
if single:
out = (out[0][0], len(out))
else:
accum = []
for opt in out:
accum.extend(opt[1])
out = (out[0][0], accum)
elif merge and not out:
return None
return out
def _make_prefix_tab(names):
# Given a list of names, return a dictionary in which each key is
# a unique prefix of exactly one string from the original set of
# names. Returns None if not all the keys are unique. Only the
# shortest and matching prefixes are stored in the dictionary; so
# for instance, if the strings were ['allen', 'alien'], the keys
# of the result would be ['all', 'allen', 'ali', 'alien'].
if not names:
return None
def any_prefix(name, whole):
for key in names:
if key <> whole and key.find(name) == 0:
return True
return False
out = {}
for name in names:
if out.has_key(name):
return None # Key conflict
out[name] = name
last = None
pfx = name[:-1]
while pfx and not any_prefix(pfx, name):
last = pfx
pfx = pfx[:-1]
if last:
out[last] = name
return out
# Try not to stare at these too hard, you might go blind
_spec_s = _sre.compile('-(\w+)(?:\*(\d+))?(?:\?(.+))?') # Single options
_spec_m = _sre.compile('([+!])(\w+)(?:\*(\d+))?' # +name*n
'=(\d+)(?:\.(\d+))?' # =min.max
'(?:\?([^@]+))?' # ?help text [opt]
'(?:\@(\w+(?:,\w+)*))?' # @arg1,arg2 [opt]
'$')
def parse_option_spec(spec):
match = _spec_s.match(spec)
if match:
opt = { 'type': 'single' }
opt['name'] = match.group(1)
if match.group(2):
opt['max_count'] = int(match.group(2))
else:
opt['max_count'] = 0
if match.group(3):
opt['help'] = match.group(3)
return opt
match = _spec_m.match(spec)
if not match:
raise OptError("incorrect format for option specifier: %s" % spec)
opt = { 'type': 'multiple' }
opt['required'] = (match.group(1) == '!')
opt['name'] = match.group(2)
if match.group(3):
opt['max_count'] = int(match.group(3))
else:
opt['max_count'] = 0
opt['min_args'] = int(match.group(4))
opt['max_args'] = -1
if match.group(5):
opt['max_args'] = int(match.group(5))
if match.group(6):
opt['help'] = match.group(6)
if match.group(7):
opt['argkeys'] = match.group(7).split(',')
return opt
def create_from_strings(opts):
"""Create an options object from a list of strings specifying the
options to be included. The formats for these strings may be:
-name -- a single option, no arguments, any number of times
-name*n -- a single option, no arguments, at most n repeats.
+name=x
+name=x.y -- a multiple option with at least x arguments and at
most y arguments (no limit if y is omitted).
To specify that the option is REQUIRED, replace the
leading '+' with a '!'. To specify that it may be
repeated at most n times, write *n after the name
and before the '=' sign.
To either of these forms you may append help text by writing a
question mark (?) followed by the text, e.g.:
-name?Description of the option
+name=1?Description of the option
For multiple options, you can specify the argument names to be
displayed in the help listing by appending an at-sign (@) followed
by a comma-separated list of names, e.g.:
+set=2.2?Set a parameter value.@name,value
The return value is an Options object."""
out = Options()
for opt in opts:
out.add_option(parse_option_spec(opt))
return out
# Here there be dragons