Path: blob/main/test/lib/python3.9/site-packages/setuptools/_distutils/fancy_getopt.py
4799 views
"""distutils.fancy_getopt12Wrapper around the standard getopt module that provides the following3additional features:4* short and long options are tied together5* options have help strings, so fancy_getopt could potentially6create a complete usage summary7* options set attributes of a passed-in object8"""910import sys, string, re11import getopt12from distutils.errors import *1314# Much like command_re in distutils.core, this is close to but not quite15# the same as a Python NAME -- except, in the spirit of most GNU16# utilities, we use '-' in place of '_'. (The spirit of LISP lives on!)17# The similarities to NAME are again not a coincidence...18longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)'19longopt_re = re.compile(r'^%s$' % longopt_pat)2021# For recognizing "negative alias" options, eg. "quiet=!verbose"22neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat))2324# This is used to translate long options to legitimate Python identifiers25# (for use as attributes of some object).26longopt_xlate = str.maketrans('-', '_')2728class FancyGetopt:29"""Wrapper around the standard 'getopt()' module that provides some30handy extra functionality:31* short and long options are tied together32* options have help strings, and help text can be assembled33from them34* options set attributes of a passed-in object35* boolean options can have "negative aliases" -- eg. if36--quiet is the "negative alias" of --verbose, then "--quiet"37on the command line sets 'verbose' to false38"""3940def __init__(self, option_table=None):41# The option table is (currently) a list of tuples. The42# tuples may have 3 or four values:43# (long_option, short_option, help_string [, repeatable])44# if an option takes an argument, its long_option should have '='45# appended; short_option should just be a single character, no ':'46# in any case. If a long_option doesn't have a corresponding47# short_option, short_option should be None. All option tuples48# must have long options.49self.option_table = option_table5051# 'option_index' maps long option names to entries in the option52# table (ie. those 3-tuples).53self.option_index = {}54if self.option_table:55self._build_index()5657# 'alias' records (duh) alias options; {'foo': 'bar'} means58# --foo is an alias for --bar59self.alias = {}6061# 'negative_alias' keeps track of options that are the boolean62# opposite of some other option63self.negative_alias = {}6465# These keep track of the information in the option table. We66# don't actually populate these structures until we're ready to67# parse the command-line, since the 'option_table' passed in here68# isn't necessarily the final word.69self.short_opts = []70self.long_opts = []71self.short2long = {}72self.attr_name = {}73self.takes_arg = {}7475# And 'option_order' is filled up in 'getopt()'; it records the76# original order of options (and their values) on the command-line,77# but expands short options, converts aliases, etc.78self.option_order = []7980def _build_index(self):81self.option_index.clear()82for option in self.option_table:83self.option_index[option[0]] = option8485def set_option_table(self, option_table):86self.option_table = option_table87self._build_index()8889def add_option(self, long_option, short_option=None, help_string=None):90if long_option in self.option_index:91raise DistutilsGetoptError(92"option conflict: already an option '%s'" % long_option)93else:94option = (long_option, short_option, help_string)95self.option_table.append(option)96self.option_index[long_option] = option9798def has_option(self, long_option):99"""Return true if the option table for this parser has an100option with long name 'long_option'."""101return long_option in self.option_index102103def get_attr_name(self, long_option):104"""Translate long option name 'long_option' to the form it105has as an attribute of some object: ie., translate hyphens106to underscores."""107return long_option.translate(longopt_xlate)108109def _check_alias_dict(self, aliases, what):110assert isinstance(aliases, dict)111for (alias, opt) in aliases.items():112if alias not in self.option_index:113raise DistutilsGetoptError(("invalid %s '%s': "114"option '%s' not defined") % (what, alias, alias))115if opt not in self.option_index:116raise DistutilsGetoptError(("invalid %s '%s': "117"aliased option '%s' not defined") % (what, alias, opt))118119def set_aliases(self, alias):120"""Set the aliases for this option parser."""121self._check_alias_dict(alias, "alias")122self.alias = alias123124def set_negative_aliases(self, negative_alias):125"""Set the negative aliases for this option parser.126'negative_alias' should be a dictionary mapping option names to127option names, both the key and value must already be defined128in the option table."""129self._check_alias_dict(negative_alias, "negative alias")130self.negative_alias = negative_alias131132def _grok_option_table(self):133"""Populate the various data structures that keep tabs on the134option table. Called by 'getopt()' before it can do anything135worthwhile.136"""137self.long_opts = []138self.short_opts = []139self.short2long.clear()140self.repeat = {}141142for option in self.option_table:143if len(option) == 3:144long, short, help = option145repeat = 0146elif len(option) == 4:147long, short, help, repeat = option148else:149# the option table is part of the code, so simply150# assert that it is correct151raise ValueError("invalid option tuple: %r" % (option,))152153# Type- and value-check the option names154if not isinstance(long, str) or len(long) < 2:155raise DistutilsGetoptError(("invalid long option '%s': "156"must be a string of length >= 2") % long)157158if (not ((short is None) or159(isinstance(short, str) and len(short) == 1))):160raise DistutilsGetoptError("invalid short option '%s': "161"must a single character or None" % short)162163self.repeat[long] = repeat164self.long_opts.append(long)165166if long[-1] == '=': # option takes an argument?167if short: short = short + ':'168long = long[0:-1]169self.takes_arg[long] = 1170else:171# Is option is a "negative alias" for some other option (eg.172# "quiet" == "!verbose")?173alias_to = self.negative_alias.get(long)174if alias_to is not None:175if self.takes_arg[alias_to]:176raise DistutilsGetoptError(177"invalid negative alias '%s': "178"aliased option '%s' takes a value"179% (long, alias_to))180181self.long_opts[-1] = long # XXX redundant?!182self.takes_arg[long] = 0183184# If this is an alias option, make sure its "takes arg" flag is185# the same as the option it's aliased to.186alias_to = self.alias.get(long)187if alias_to is not None:188if self.takes_arg[long] != self.takes_arg[alias_to]:189raise DistutilsGetoptError(190"invalid alias '%s': inconsistent with "191"aliased option '%s' (one of them takes a value, "192"the other doesn't"193% (long, alias_to))194195# Now enforce some bondage on the long option name, so we can196# later translate it to an attribute name on some object. Have197# to do this a bit late to make sure we've removed any trailing198# '='.199if not longopt_re.match(long):200raise DistutilsGetoptError(201"invalid long option name '%s' "202"(must be letters, numbers, hyphens only" % long)203204self.attr_name[long] = self.get_attr_name(long)205if short:206self.short_opts.append(short)207self.short2long[short[0]] = long208209def getopt(self, args=None, object=None):210"""Parse command-line options in args. Store as attributes on object.211212If 'args' is None or not supplied, uses 'sys.argv[1:]'. If213'object' is None or not supplied, creates a new OptionDummy214object, stores option values there, and returns a tuple (args,215object). If 'object' is supplied, it is modified in place and216'getopt()' just returns 'args'; in both cases, the returned217'args' is a modified copy of the passed-in 'args' list, which218is left untouched.219"""220if args is None:221args = sys.argv[1:]222if object is None:223object = OptionDummy()224created_object = True225else:226created_object = False227228self._grok_option_table()229230short_opts = ' '.join(self.short_opts)231try:232opts, args = getopt.getopt(args, short_opts, self.long_opts)233except getopt.error as msg:234raise DistutilsArgError(msg)235236for opt, val in opts:237if len(opt) == 2 and opt[0] == '-': # it's a short option238opt = self.short2long[opt[1]]239else:240assert len(opt) > 2 and opt[:2] == '--'241opt = opt[2:]242243alias = self.alias.get(opt)244if alias:245opt = alias246247if not self.takes_arg[opt]: # boolean option?248assert val == '', "boolean option can't have value"249alias = self.negative_alias.get(opt)250if alias:251opt = alias252val = 0253else:254val = 1255256attr = self.attr_name[opt]257# The only repeating option at the moment is 'verbose'.258# It has a negative option -q quiet, which should set verbose = 0.259if val and self.repeat.get(attr) is not None:260val = getattr(object, attr, 0) + 1261setattr(object, attr, val)262self.option_order.append((opt, val))263264# for opts265if created_object:266return args, object267else:268return args269270def get_option_order(self):271"""Returns the list of (option, value) tuples processed by the272previous run of 'getopt()'. Raises RuntimeError if273'getopt()' hasn't been called yet.274"""275if self.option_order is None:276raise RuntimeError("'getopt()' hasn't been called yet")277else:278return self.option_order279280def generate_help(self, header=None):281"""Generate help text (a list of strings, one per suggested line of282output) from the option table for this FancyGetopt object.283"""284# Blithely assume the option table is good: probably wouldn't call285# 'generate_help()' unless you've already called 'getopt()'.286287# First pass: determine maximum length of long option names288max_opt = 0289for option in self.option_table:290long = option[0]291short = option[1]292l = len(long)293if long[-1] == '=':294l = l - 1295if short is not None:296l = l + 5 # " (-x)" where short == 'x'297if l > max_opt:298max_opt = l299300opt_width = max_opt + 2 + 2 + 2 # room for indent + dashes + gutter301302# Typical help block looks like this:303# --foo controls foonabulation304# Help block for longest option looks like this:305# --flimflam set the flim-flam level306# and with wrapped text:307# --flimflam set the flim-flam level (must be between308# 0 and 100, except on Tuesdays)309# Options with short names will have the short name shown (but310# it doesn't contribute to max_opt):311# --foo (-f) controls foonabulation312# If adding the short option would make the left column too wide,313# we push the explanation off to the next line314# --flimflam (-l)315# set the flim-flam level316# Important parameters:317# - 2 spaces before option block start lines318# - 2 dashes for each long option name319# - min. 2 spaces between option and explanation (gutter)320# - 5 characters (incl. space) for short option name321322# Now generate lines of help text. (If 80 columns were good enough323# for Jesus, then 78 columns are good enough for me!)324line_width = 78325text_width = line_width - opt_width326big_indent = ' ' * opt_width327if header:328lines = [header]329else:330lines = ['Option summary:']331332for option in self.option_table:333long, short, help = option[:3]334text = wrap_text(help, text_width)335if long[-1] == '=':336long = long[0:-1]337338# Case 1: no short option at all (makes life easy)339if short is None:340if text:341lines.append(" --%-*s %s" % (max_opt, long, text[0]))342else:343lines.append(" --%-*s " % (max_opt, long))344345# Case 2: we have a short option, so we have to include it346# just after the long option347else:348opt_names = "%s (-%s)" % (long, short)349if text:350lines.append(" --%-*s %s" %351(max_opt, opt_names, text[0]))352else:353lines.append(" --%-*s" % opt_names)354355for l in text[1:]:356lines.append(big_indent + l)357return lines358359def print_help(self, header=None, file=None):360if file is None:361file = sys.stdout362for line in self.generate_help(header):363file.write(line + "\n")364365366def fancy_getopt(options, negative_opt, object, args):367parser = FancyGetopt(options)368parser.set_negative_aliases(negative_opt)369return parser.getopt(args, object)370371372WS_TRANS = {ord(_wschar) : ' ' for _wschar in string.whitespace}373374def wrap_text(text, width):375"""wrap_text(text : string, width : int) -> [string]376377Split 'text' into multiple lines of no more than 'width' characters378each, and return the list of strings that results.379"""380if text is None:381return []382if len(text) <= width:383return [text]384385text = text.expandtabs()386text = text.translate(WS_TRANS)387chunks = re.split(r'( +|-+)', text)388chunks = [ch for ch in chunks if ch] # ' - ' results in empty strings389lines = []390391while chunks:392cur_line = [] # list of chunks (to-be-joined)393cur_len = 0 # length of current line394395while chunks:396l = len(chunks[0])397if cur_len + l <= width: # can squeeze (at least) this chunk in398cur_line.append(chunks[0])399del chunks[0]400cur_len = cur_len + l401else: # this line is full402# drop last chunk if all space403if cur_line and cur_line[-1][0] == ' ':404del cur_line[-1]405break406407if chunks: # any chunks left to process?408# if the current line is still empty, then we had a single409# chunk that's too big too fit on a line -- so we break410# down and break it up at the line width411if cur_len == 0:412cur_line.append(chunks[0][0:width])413chunks[0] = chunks[0][width:]414415# all-whitespace chunks at the end of a line can be discarded416# (and we know from the re.split above that if a chunk has417# *any* whitespace, it is *all* whitespace)418if chunks[0][0] == ' ':419del chunks[0]420421# and store this line in the list-of-all-lines -- as a single422# string, of course!423lines.append(''.join(cur_line))424425return lines426427428def translate_longopt(opt):429"""Convert a long option name to a valid Python identifier by430changing "-" to "_".431"""432return opt.translate(longopt_xlate)433434435class OptionDummy:436"""Dummy class just used as a place to hold command-line option437values as instance attributes."""438439def __init__(self, options=[]):440"""Create a new OptionDummy instance. The attributes listed in441'options' will be initialized to None."""442for opt in options:443setattr(self, opt, None)444445446if __name__ == "__main__":447text = """\448Tra-la-la, supercalifragilisticexpialidocious.449How *do* you spell that odd word, anyways?450(Someone ask Mary -- she'll know [or she'll451say, "How should I know?"].)"""452453for w in (10, 20, 30, 40):454print("width: %d" % w)455print("\n".join(wrap_text(text, w)))456print()457458459