########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Lib/CommandLine/Options.py,v 1.13 2005/04/13 23:41:04 jkloth Exp $
"""
Classes that support advanced option processing for command-line scripts
Copyright 2004 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""
from distutils.fancy_getopt import wrap_text
from Ft.Lib.CommandLine import CommandLineUtil,CONSOLE_WIDTH
class Options(list):
"""
A set of options that are available to be used in an invocation of a
command-line script, plus related functions.
"""
def __init__(self, options=None):
if options is None:
options = ()
list.__init__(self, options)
# Sanity check the supplied options
shorts = []
longs = []
for opt in options:
if not isinstance(opt, BaseOption):
raise TypeError("Option %s is not of BaseOption" % opt)
(short, long) = opt.getForGetOpt({}, {})
for s in short:
if s != ':' and s in shorts:
raise Exception('Duplicate short option in %s' % str(self))
shorts.append(s)
for l in long:
if l in longs:
raise Exception('Duplicate long option in %s' % str(self))
longs.append(l)
def findMaxOption(self, level=0):
max_opt = 0
for option in self:
l = option.displayLength()
# add two spaces for each level of sub-options
l = l + 2*level
if hasattr(option, 'subOptions'):
sublen = option.subOptions.findMaxOption(level+1)
if sublen > l:
l = sublen
if l > max_opt:
max_opt = l
return max_opt
def generate_help(self, level=1, max_opt=0):
"""Generate help text (a list of strings, one per suggested line of
output) from the option table for this FancyGetopt object.
"""
# If max_opt is > 0, this is help for a sub-option
# maximum width has already been determined.
if max_opt > 0:
# The indent for sub-options is included in option length
opt_width = max_opt - 2*(level-1)
else:
opt_width = max_opt = self.findMaxOption()
# room for indent + short option + dashes + longest option + gutter + add'l indent
col_width = 2*level + 4 + 2 + max_opt + 2 + 2
# Typical help block looks like this:
# --foo controls foonabulation
# Help block for longest option looks like this:
# --flimflam set the flim-flam level
# and with wrapped text:
# --flimflam set the flim-flam level (must be between
# 0 and 100, except on Tuesdays)
# Options with short names will have the short name shown (but
# it doesn't contribute to max_opt):
# -f, --foo controls foonabulation
# If adding the short option would make the left column too wide,
# we push the explanation off to the next line
# -l, --flimflam
# set the flim-flam level
# Important parameters:
# - 2 spaces before option block start lines
# - 2 dashes for each long option name
# - min. 2 spaces between option and explanation (gutter)
# Now generate lines of help text.
line_width = CONSOLE_WIDTH
text_width = line_width - col_width
indent = ' ' * level
big_indent = ' ' * (col_width)
lines = []
for option in self:
if isinstance(option, ExclusiveOptions):
lines.extend(option.choices.generate_help(level, max_opt))
continue
text = wrap_text(option.description, text_width)
short_opt = option.shortName
if option.takesArg:
long_opt = '%s=<%s>' % (option.longName, option.argName)
else:
long_opt = option.longName
if option.shortName:
short_part = '-%s' % (short_opt)
else:
short_part = ' '
if option.shortName and option.longName:
short_part += ', '
else:
short_part += ' '
long_part = "--%-*s" % (opt_width, long_opt)
if text:
lines.append('%s%s%s %s' % (indent, short_part, long_part, text[0]))
else:
lines.append('%s%s%s' % (indent, short_part, long_part))
# Add any description that didn't fit on the first line
for line in text[1:]:
lines.append(big_indent + line)
if isinstance(option, TypedOption):
for (val, desc) in option.allowed:
text = wrap_text(desc, text_width)
lines.append('%s %-*s%s' % (indent, opt_width, val, text[0]))
for line in text[1:]:
lines.append(big_indent + line)
if hasattr(option, 'subOptions'):
lines.extend(option.subOptions.generate_help(level + 1, max_opt))
return lines
class BaseOption:
"""
An option that is available to be used in an invocation of a
command-line script, plus related functions.
"""
multiple = False
def validate(self):
return
def getForGetOpt(self, short2long, takes_arg):
raise NotImplementedError('subclass must override')
def displayLength(self):
raise NotImplementedError('subclass %s must override' % self.__class__)
def gen_command_line(self):
raise NotImplementedError('subclass %s must override' % self.__class__)
def gen_description(self):
raise NotImplementedError('subclass %s must override' % self.__class__)
def apply_options(self, options):
raise NotImplementedError('subclass %s must override' % self.__class__)
def isApplied(self):
raise NotImplementedError('subclass %s must override' % self.__class__)
def getName(self):
raise NotImplementedError('subclass %s must override' % self.__class__)
def __str__(self):
return self.gen_command_line()
__repr__ = __str__
class Option(BaseOption):
def __init__(self, shortName, longName, description, subOptions=None,
multiple=False):
# Type- and value-check the option names
if len(longName) < 2:
raise SyntaxError('invalid long option: ' + longName)
if shortName is not None and len(shortName) != 1:
raise SyntaxError('invalid short option: ' + shortName)
self.shortName = shortName or ''
i = longName.find('=')
if i > 0:
self.takesArg = 1
argName = longName[i+1:]
longName = longName[:i]
self.argName = argName or longName
else:
self.takesArg = 0
self.longName = longName
self.description = description
if not isinstance(subOptions, Options):
subOptions = Options(subOptions)
self.subOptions = subOptions
self.multiple = multiple
def getForGetOpt(self, short2long, takes_arg):
short_opts = self.shortName
if self.takesArg:
if short_opts:
short_opts = short_opts + ':'
long_opts = [self.longName + '=']
else:
long_opts = [self.longName]
takes_arg[self.longName] = self.takesArg
if self.shortName:
short2long[self.shortName] = self.longName
# get getopt options for any sub-options
for option in self.subOptions:
(short, long) = option.getForGetOpt(short2long, takes_arg)
short_opts = short_opts + short
long_opts.extend(long)
return (short_opts, long_opts)
def displayLength(self):
l = len(self.longName)
if self.takesArg:
# add the "=argName" length
l = l + 1 + len(self.argName)
return l
def validate(self):
for option in self.subOptions:
option.validate()
for option in self.subOptions:
if option.isApplied() and not self.applied:
raise CommandLineUtil.ArgumentError("%s specified without %s" % (option.getName(),self.longName))
return
def apply_options(self, options):
self.applied = options.has_key(self.longName)
for option in self.subOptions:
option.apply_options(options)
return
def isApplied(self):
return self.applied
def getName(self):
return self.longName
def gen_command_line(self):
cl = '[--%s' % self.longName
if self.takesArg:
cl = cl + '=<%s>' % self.argName
if self.subOptions:
sub = map(lambda s: s.gen_command_line(), self.subOptions)
if len(sub) > 1:
cl = cl + ' [%s]' % ' '.join(sub)
else:
cl = cl + ' ' + sub[0]
return cl + ']'
class TypedOption(Option):
def __init__(self, shortName, longName, description, allowed, subOptions=None):
Option.__init__(self, shortName, longName, description, subOptions)
self.allowedValues = map(lambda (value, desc): value, allowed)
self.allowed = allowed
def apply_options(self, options):
self.applied = options.get(self.longName)
for option in self.subOptions:
option.apply_options(options)
return
def validate(self):
if self.applied and self.applied not in self.allowedValues:
expected = ', '.join(self.allowedValues)
raise SyntaxError('option %s: expected %s, got %s' %
(self.longName, expected, self.applied))
Option.validate(self)
return
def gen_command_line(self):
sub = ''
for option in self.subOptions:
sub = sub + '%s ' % option.gen_command_line()
av = '['
for a in self.allowedValues:
av = av + a
if a != self.allowedValues[-1]:
av = av + '|'
av =av + ']'
if sub:
return '[--%s=%s [%s]]' % (self.longName,av,sub)
else:
return '[--%s=%s]' % (self.longName,av)
class ExclusiveOptions(BaseOption):
def __init__(self, choices):
if not isinstance(choices, Options):
choices = Options(choices)
self.choices = choices
def getForGetOpt(self, short2long, takes_arg):
short_opts = ''
long_opts = []
# get getopt options for all choices
for option in self.choices:
(short, long) = option.getForGetOpt(short2long, takes_arg)
short_opts = short_opts + short
long_opts.extend(long)
return (short_opts, long_opts)
def displayLength(self):
return self.choices.findMaxOption()
def validate(self):
# make sure only one of our choices is in the options
applied = 0
for opt in self.choices:
if opt.isApplied():
if applied:
opts = ', '.join(map(lambda x: '--%s' % x.getName(), self.choices))
raise CommandLineUtil.ArgumentError("Only one of %s allowed" % opts)
applied = opt
if applied:
#NOTE, this allows some sub commands to sneak in
applied.validate()
else:
#Validate them all
for option in self.choices:
option.validate()
def apply_options(self, options):
for option in self.choices:
option.apply_options(options)
return
def isApplied(self):
for option in self.choices:
if option.isApplied(): return 1
return 0
def getName(self):
return '(%s)' % ', '.join(map(lambda x: '--%s' % x.getName(), self.choices))
def gen_command_line(self):
cl = '['
first = 1
for c in self.choices:
if not first:
cl = cl + ' | '
else:
first = 0
cl = cl + c.gen_command_line()
return cl + ']'
|