b60fca88ee23ae2e8f71edc8296a6432c0b83ec8
"""distutils.fancy_getopt
Wrapper around the standard getopt module that provides the following
* short and long options are tied together
* options have help strings, so fancy_getopt could potentially
create a complete usage summary
* options set attributes of a passed-in object
# This module should be kept compatible with Python 2.1.
__revision__
= "$Id: fancy_getopt.py,v 1.30 2004/11/10 22:23:14 loewis Exp $"
from distutils
.errors
import *
# Much like command_re in distutils.core, this is close to but not quite
# the same as a Python NAME -- except, in the spirit of most GNU
# utilities, we use '-' in place of '_'. (The spirit of LISP lives on!)
# The similarities to NAME are again not a coincidence...
longopt_pat
= r
'[a-zA-Z](?:[a-zA-Z0-9-]*)'
longopt_re
= re
.compile(r
'^%s$' % longopt_pat
)
# For recognizing "negative alias" options, eg. "quiet=!verbose"
neg_alias_re
= re
.compile("^(%s)=!(%s)$" % (longopt_pat
, longopt_pat
))
# This is used to translate long options to legitimate Python identifiers
# (for use as attributes of some object).
longopt_xlate
= string
.maketrans('-', '_')
"""Wrapper around the standard 'getopt()' module that provides some
handy extra functionality:
* short and long options are tied together
* options have help strings, and help text can be assembled
* options set attributes of a passed-in object
* boolean options can have "negative aliases" -- eg. if
--quiet is the "negative alias" of --verbose, then "--quiet"
on the command line sets 'verbose' to false
def __init__ (self
, option_table
=None):
# The option table is (currently) a list of tuples. The
# tuples may have 3 or four values:
# (long_option, short_option, help_string [, repeatable])
# if an option takes an argument, its long_option should have '='
# appended; short_option should just be a single character, no ':'
# in any case. If a long_option doesn't have a corresponding
# short_option, short_option should be None. All option tuples
# must have long options.
self
.option_table
= option_table
# 'option_index' maps long option names to entries in the option
# table (ie. those 3-tuples).
# 'alias' records (duh) alias options; {'foo': 'bar'} means
# --foo is an alias for --bar
# 'negative_alias' keeps track of options that are the boolean
# opposite of some other option
# These keep track of the information in the option table. We
# don't actually populate these structures until we're ready to
# parse the command-line, since the 'option_table' passed in here
# isn't necessarily the final word.
# And 'option_order' is filled up in 'getopt()'; it records the
# original order of options (and their values) on the command-line,
# but expands short options, converts aliases, etc.
self
.option_index
.clear()
for option
in self
.option_table
:
self
.option_index
[option
[0]] = option
def set_option_table (self
, option_table
):
self
.option_table
= option_table
def add_option (self
, long_option
, short_option
=None, help_string
=None):
if self
.option_index
.has_key(long_option
):
raise DistutilsGetoptError
, \
"option conflict: already an option '%s'" % long_option
option
= (long_option
, short_option
, help_string
)
self
.option_table
.append(option
)
self
.option_index
[long_option
] = option
def has_option (self
, long_option
):
"""Return true if the option table for this parser has an
option with long name 'long_option'."""
return self
.option_index
.has_key(long_option
)
def get_attr_name (self
, long_option
):
"""Translate long option name 'long_option' to the form it
has as an attribute of some object: ie., translate hyphens
return string
.translate(long_option
, longopt_xlate
)
def _check_alias_dict (self
, aliases
, what
):
assert type(aliases
) is DictionaryType
for (alias
, opt
) in aliases
.items():
if not self
.option_index
.has_key(alias
):
raise DistutilsGetoptError
, \
"option '%s' not defined") % (what
, alias
, alias
)
if not self
.option_index
.has_key(opt
):
raise DistutilsGetoptError
, \
"aliased option '%s' not defined") % (what
, alias
, opt
)
def set_aliases (self
, alias
):
"""Set the aliases for this option parser."""
self
._check
_alias
_dict
(alias
, "alias")
def set_negative_aliases (self
, negative_alias
):
"""Set the negative aliases for this option parser.
'negative_alias' should be a dictionary mapping option names to
option names, both the key and value must already be defined
self
._check
_alias
_dict
(negative_alias
, "negative alias")
self
.negative_alias
= negative_alias
def _grok_option_table (self
):
"""Populate the various data structures that keep tabs on the
option table. Called by 'getopt()' before it can do anything
for option
in self
.option_table
:
long, short
, help = option
long, short
, help, repeat
= option
# the option table is part of the code, so simply
# assert that it is correct
raise ValueError, "invalid option tuple: %r" % (option
,)
# Type- and value-check the option names
if type(long) is not StringType
or len(long) < 2:
raise DistutilsGetoptError
, \
("invalid long option '%s': "
"must be a string of length >= 2") % long
if (not ((short
is None) or
(type(short
) is StringType
and len(short
) == 1))):
raise DistutilsGetoptError
, \
("invalid short option '%s': "
"must a single character or None") % short
self
.repeat
[long] = repeat
self
.long_opts
.append(long)
if long[-1] == '=': # option takes an argument?
if short
: short
= short
+ ':'
# Is option is a "negative alias" for some other option (eg.
# "quiet" == "!verbose")?
alias_to
= self
.negative_alias
.get(long)
if self
.takes_arg
[alias_to
]:
raise DistutilsGetoptError
, \
("invalid negative alias '%s': "
"aliased option '%s' takes a value") % \
self
.long_opts
[-1] = long # XXX redundant?!
# If this is an alias option, make sure its "takes arg" flag is
# the same as the option it's aliased to.
alias_to
= self
.alias
.get(long)
if self
.takes_arg
[long] != self
.takes_arg
[alias_to
]:
raise DistutilsGetoptError
, \
("invalid alias '%s': inconsistent with "
"aliased option '%s' (one of them takes a value, "
"the other doesn't") % (long, alias_to
)
# Now enforce some bondage on the long option name, so we can
# later translate it to an attribute name on some object. Have
# to do this a bit late to make sure we've removed any trailing
if not longopt_re
.match(long):
raise DistutilsGetoptError
, \
("invalid long option name '%s' " +
"(must be letters, numbers, hyphens only") % long
self
.attr_name
[long] = self
.get_attr_name(long)
self
.short_opts
.append(short
)
self
.short2long
[short
[0]] = long
def getopt (self
, args
=None, object=None):
"""Parse command-line options in args. Store as attributes on object.
If 'args' is None or not supplied, uses 'sys.argv[1:]'. If
'object' is None or not supplied, creates a new OptionDummy
object, stores option values there, and returns a tuple (args,
object). If 'object' is supplied, it is modified in place and
'getopt()' just returns 'args'; in both cases, the returned
'args' is a modified copy of the passed-in 'args' list, which
self
._grok
_option
_table
()
short_opts
= string
.join(self
.short_opts
)
opts
, args
= getopt
.getopt(args
, short_opts
, self
.long_opts
)
except getopt
.error
, msg
:
raise DistutilsArgError
, msg
if len(opt
) == 2 and opt
[0] == '-': # it's a short option
opt
= self
.short2long
[opt
[1]]
assert len(opt
) > 2 and opt
[:2] == '--'
alias
= self
.alias
.get(opt
)
if not self
.takes_arg
[opt
]: # boolean option?
assert val
== '', "boolean option can't have value"
alias
= self
.negative_alias
.get(opt
)
attr
= self
.attr_name
[opt
]
# The only repeating option at the moment is 'verbose'.
# It has a negative option -q quiet, which should set verbose = 0.
if val
and self
.repeat
.get(attr
) is not None:
val
= getattr(object, attr
, 0) + 1
setattr(object, attr
, val
)
self
.option_order
.append((opt
, val
))
def get_option_order (self
):
"""Returns the list of (option, value) tuples processed by the
previous run of 'getopt()'. Raises RuntimeError if
'getopt()' hasn't been called yet.
if self
.option_order
is None:
raise RuntimeError, "'getopt()' hasn't been called yet"
def generate_help (self
, header
=None):
"""Generate help text (a list of strings, one per suggested line of
output) from the option table for this FancyGetopt object.
# Blithely assume the option table is good: probably wouldn't call
# 'generate_help()' unless you've already called 'getopt()'.
# First pass: determine maximum length of long option names
for option
in self
.option_table
:
l
= l
+ 5 # " (-x)" where short == 'x'
opt_width
= max_opt
+ 2 + 2 + 2 # room for indent + dashes + gutter
# Typical help block looks like this:
# --foo controls foonabulation
# Help block for longest option looks like this:
# --flimflam set the flim-flam level
# --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):
# --foo (-f) controls foonabulation
# If adding the short option would make the left column too wide,
# we push the explanation off to the next line
# set the flim-flam level
# - 2 spaces before option block start lines
# - 2 dashes for each long option name
# - min. 2 spaces between option and explanation (gutter)
# - 5 characters (incl. space) for short option name
# Now generate lines of help text. (If 80 columns were good enough
# for Jesus, then 78 columns are good enough for me!)
text_width
= line_width
- opt_width
big_indent
= ' ' * opt_width
lines
= ['Option summary:']
for option
in self
.option_table
:
long, short
, help = option
[:3]
text
= wrap_text(help, text_width
)
# Case 1: no short option at all (makes life easy)
lines
.append(" --%-*s %s" % (max_opt
, long, text
[0]))
lines
.append(" --%-*s " % (max_opt
, long))
# Case 2: we have a short option, so we have to include it
# just after the long option
opt_names
= "%s (-%s)" % (long, short
)
lines
.append(" --%-*s %s" %
(max_opt
, opt_names
, text
[0]))
lines
.append(" --%-*s" % opt_names
)
lines
.append(big_indent
+ l
)
def print_help (self
, header
=None, file=None):
for line
in self
.generate_help(header
):
def fancy_getopt (options
, negative_opt
, object, args
):
parser
= FancyGetopt(options
)
parser
.set_negative_aliases(negative_opt
)
return parser
.getopt(args
, object)
WS_TRANS
= string
.maketrans(string
.whitespace
, ' ' * len(string
.whitespace
))
def wrap_text (text
, width
):
"""wrap_text(text : string, width : int) -> [string]
Split 'text' into multiple lines of no more than 'width' characters
each, and return the list of strings that results.
text
= string
.expandtabs(text
)
text
= string
.translate(text
, WS_TRANS
)
chunks
= re
.split(r
'( +|-+)', text
)
chunks
= filter(None, chunks
) # ' - ' results in empty strings
cur_line
= [] # list of chunks (to-be-joined)
cur_len
= 0 # length of current line
if cur_len
+ l
<= width
: # can squeeze (at least) this chunk in
cur_line
.append(chunks
[0])
else: # this line is full
# drop last chunk if all space
if cur_line
and cur_line
[-1][0] == ' ':
if chunks
: # any chunks left to process?
# if the current line is still empty, then we had a single
# chunk that's too big too fit on a line -- so we break
# down and break it up at the line width
cur_line
.append(chunks
[0][0:width
])
chunks
[0] = chunks
[0][width
:]
# all-whitespace chunks at the end of a line can be discarded
# (and we know from the re.split above that if a chunk has
# *any* whitespace, it is *all* whitespace)
# and store this line in the list-of-all-lines -- as a single
lines
.append(string
.join(cur_line
, ''))
def translate_longopt (opt
):
"""Convert a long option name to a valid Python identifier by
return string
.translate(opt
, longopt_xlate
)
"""Dummy class just used as a place to hold command-line option
values as instance attributes."""
def __init__ (self
, options
=[]):
"""Create a new OptionDummy instance. The attributes listed in
'options' will be initialized to None."""
if __name__
== "__main__":
Tra-la-la, supercalifragilisticexpialidocious.
How *do* you spell that odd word, anyways?
(Someone ask Mary -- she'll know [or she'll
say, "How should I know?"].)"""
for w
in (10, 20, 30, 40):
print string
.join(wrap_text(text
, w
), "\n")