Home | History | Annotate | Download | only in site_compare
      1 #!/usr/bin/env python
      2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Parse a command line, retrieving a command and its arguments.
      7 
      8 Supports the concept of command line commands, each with its own set
      9 of arguments. Supports dependent arguments and mutually exclusive arguments.
     10 Basically, a better optparse. I took heed of epg's WHINE() in gvn.cmdline
     11 and dumped optparse in favor of something better.
     12 """
     13 
     14 import os.path
     15 import re
     16 import string
     17 import sys
     18 import textwrap
     19 import types
     20 
     21 
     22 def IsString(var):
     23   """Little helper function to see if a variable is a string."""
     24   return type(var) in types.StringTypes
     25 
     26 
     27 class ParseError(Exception):
     28   """Encapsulates errors from parsing, string arg is description."""
     29   pass
     30 
     31 
     32 class Command(object):
     33   """Implements a single command."""
     34 
     35   def __init__(self, names, helptext, validator=None, impl=None):
     36     """Initializes Command from names and helptext, plus optional callables.
     37 
     38     Args:
     39       names:       command name, or list of synonyms
     40       helptext:    brief string description of the command
     41       validator:   callable for custom argument validation
     42                    Should raise ParseError if it wants
     43       impl:        callable to be invoked when command is called
     44     """
     45     self.names = names
     46     self.validator = validator
     47     self.helptext = helptext
     48     self.impl = impl
     49     self.args = []
     50     self.required_groups = []
     51     self.arg_dict = {}
     52     self.positional_args = []
     53     self.cmdline = None
     54 
     55   class Argument(object):
     56     """Encapsulates an argument to a command."""
     57     VALID_TYPES = ['string', 'readfile', 'int', 'flag', 'coords']
     58     TYPES_WITH_VALUES = ['string', 'readfile', 'int', 'coords']
     59 
     60     def __init__(self, names, helptext, type, metaname,
     61                  required, default, positional):
     62       """Command-line argument to a command.
     63 
     64       Args:
     65         names:       argument name, or list of synonyms
     66         helptext:    brief description of the argument
     67         type:        type of the argument. Valid values include:
     68                           string - a string
     69                           readfile - a file which must exist and be available
     70                             for reading
     71                           int - an integer
     72                           flag - an optional flag (bool)
     73                           coords - (x,y) where x and y are ints
     74         metaname:    Name to display for value in help, inferred if not
     75                      specified
     76         required:    True if argument must be specified
     77         default:     Default value if not specified
     78         positional:  Argument specified by location, not name
     79 
     80       Raises:
     81         ValueError: the argument name is invalid for some reason
     82       """
     83       if type not in Command.Argument.VALID_TYPES:
     84         raise ValueError("Invalid type: %r" % type)
     85 
     86       if required and default is not None:
     87         raise ValueError("required and default are mutually exclusive")
     88 
     89       if required and type == 'flag':
     90         raise ValueError("A required flag? Give me a break.")
     91 
     92       if metaname and type not in Command.Argument.TYPES_WITH_VALUES:
     93         raise ValueError("Type %r can't have a metaname" % type)
     94 
     95       # If no metaname is provided, infer it: use the alphabetical characters
     96       # of the last provided name
     97       if not metaname and type in Command.Argument.TYPES_WITH_VALUES:
     98         metaname = (
     99           names[-1].lstrip(string.punctuation + string.whitespace).upper())
    100 
    101       self.names = names
    102       self.helptext = helptext
    103       self.type = type
    104       self.required = required
    105       self.default = default
    106       self.positional = positional
    107       self.metaname = metaname
    108 
    109       self.mutex = []          # arguments that are mutually exclusive with
    110                                # this one
    111       self.depends = []        # arguments that must be present for this
    112                                # one to be valid
    113       self.present = False     # has this argument been specified?
    114 
    115     def AddDependency(self, arg):
    116       """Makes this argument dependent on another argument.
    117 
    118       Args:
    119         arg: name of the argument this one depends on
    120       """
    121       if arg not in self.depends:
    122         self.depends.append(arg)
    123 
    124     def AddMutualExclusion(self, arg):
    125       """Makes this argument invalid if another is specified.
    126 
    127       Args:
    128         arg: name of the mutually exclusive argument.
    129       """
    130       if arg not in self.mutex:
    131         self.mutex.append(arg)
    132 
    133     def GetUsageString(self):
    134       """Returns a brief string describing the argument's usage."""
    135       if not self.positional:
    136         string = self.names[0]
    137         if self.type in Command.Argument.TYPES_WITH_VALUES:
    138           string += "="+self.metaname
    139       else:
    140         string = self.metaname
    141 
    142       if not self.required:
    143         string = "["+string+"]"
    144 
    145       return string
    146 
    147     def GetNames(self):
    148       """Returns a string containing a list of the arg's names."""
    149       if self.positional:
    150         return self.metaname
    151       else:
    152         return ", ".join(self.names)
    153 
    154     def GetHelpString(self, width=80, indent=5, names_width=20, gutter=2):
    155       """Returns a help string including help for all the arguments."""
    156       names = [" "*indent + line +" "*(names_width-len(line)) for line in
    157                textwrap.wrap(self.GetNames(), names_width)]
    158 
    159       helpstring = textwrap.wrap(self.helptext, width-indent-names_width-gutter)
    160 
    161       if len(names) < len(helpstring):
    162         names += [" "*(indent+names_width)]*(len(helpstring)-len(names))
    163 
    164       if len(helpstring) < len(names):
    165         helpstring += [""]*(len(names)-len(helpstring))
    166 
    167       return "\n".join([name_line + " "*gutter + help_line for
    168                         name_line, help_line in zip(names, helpstring)])
    169 
    170     def __repr__(self):
    171       if self.present:
    172         string = '= %r' % self.value
    173       else:
    174         string = "(absent)"
    175 
    176       return "Argument %s '%s'%s" % (self.type, self.names[0], string)
    177 
    178     # end of nested class Argument
    179 
    180   def AddArgument(self, names, helptext, type="string", metaname=None,
    181                   required=False, default=None, positional=False):
    182     """Command-line argument to a command.
    183 
    184     Args:
    185       names:      argument name, or list of synonyms
    186       helptext:   brief description of the argument
    187       type:       type of the argument
    188       metaname:   Name to display for value in help, inferred if not
    189       required:   True if argument must be specified
    190       default:    Default value if not specified
    191       positional: Argument specified by location, not name
    192 
    193     Raises:
    194       ValueError: the argument already exists or is invalid
    195 
    196     Returns:
    197       The newly-created argument
    198     """
    199     if IsString(names): names = [names]
    200 
    201     names = [name.lower() for name in names]
    202 
    203     for name in names:
    204       if name in self.arg_dict:
    205         raise ValueError("%s is already an argument"%name)
    206 
    207     if (positional and required and
    208         [arg for arg in self.args if arg.positional] and
    209         not [arg for arg in self.args if arg.positional][-1].required):
    210       raise ValueError(
    211         "A required positional argument may not follow an optional one.")
    212 
    213     arg = Command.Argument(names, helptext, type, metaname,
    214                            required, default, positional)
    215 
    216     self.args.append(arg)
    217 
    218     for name in names:
    219       self.arg_dict[name] = arg
    220 
    221     return arg
    222 
    223   def GetArgument(self, name):
    224     """Return an argument from a name."""
    225     return self.arg_dict[name.lower()]
    226 
    227   def AddMutualExclusion(self, args):
    228     """Specifies that a list of arguments are mutually exclusive."""
    229     if len(args) < 2:
    230       raise ValueError("At least two arguments must be specified.")
    231 
    232     args = [arg.lower() for arg in args]
    233 
    234     for index in xrange(len(args)-1):
    235       for index2 in xrange(index+1, len(args)):
    236         self.arg_dict[args[index]].AddMutualExclusion(self.arg_dict[args[index2]])
    237 
    238   def AddDependency(self, dependent, depends_on):
    239     """Specifies that one argument may only be present if another is.
    240 
    241     Args:
    242       dependent:  the name of the dependent argument
    243       depends_on: the name of the argument on which it depends
    244     """
    245     self.arg_dict[dependent.lower()].AddDependency(
    246       self.arg_dict[depends_on.lower()])
    247 
    248   def AddMutualDependency(self, args):
    249     """Specifies that a list of arguments are all mutually dependent."""
    250     if len(args) < 2:
    251       raise ValueError("At least two arguments must be specified.")
    252 
    253     args = [arg.lower() for arg in args]
    254 
    255     for (arg1, arg2) in [(arg1, arg2) for arg1 in args for arg2 in args]:
    256       if arg1 == arg2: continue
    257       self.arg_dict[arg1].AddDependency(self.arg_dict[arg2])
    258 
    259   def AddRequiredGroup(self, args):
    260     """Specifies that at least one of the named arguments must be present."""
    261     if len(args) < 2:
    262       raise ValueError("At least two arguments must be in a required group.")
    263 
    264     args = [self.arg_dict[arg.lower()] for arg in args]
    265 
    266     self.required_groups.append(args)
    267 
    268   def ParseArguments(self):
    269     """Given a command line, parse and validate the arguments."""
    270 
    271     # reset all the arguments before we parse
    272     for arg in self.args:
    273       arg.present = False
    274       arg.value = None
    275 
    276     self.parse_errors = []
    277 
    278     # look for arguments remaining on the command line
    279     while len(self.cmdline.rargs):
    280       try:
    281         self.ParseNextArgument()
    282       except ParseError, e:
    283         self.parse_errors.append(e.args[0])
    284 
    285     # after all the arguments are parsed, check for problems
    286     for arg in self.args:
    287       if not arg.present and arg.required:
    288         self.parse_errors.append("'%s': required parameter was missing"
    289                                  % arg.names[0])
    290 
    291       if not arg.present and arg.default:
    292         arg.present = True
    293         arg.value = arg.default
    294 
    295       if arg.present:
    296         for mutex in arg.mutex:
    297           if mutex.present:
    298             self.parse_errors.append(
    299               "'%s', '%s': arguments are mutually exclusive" %
    300               (arg.argstr, mutex.argstr))
    301 
    302         for depend in arg.depends:
    303           if not depend.present:
    304             self.parse_errors.append("'%s': '%s' must be specified as well" %
    305                                      (arg.argstr, depend.names[0]))
    306 
    307     # check for required groups
    308     for group in self.required_groups:
    309       if not [arg for arg in group if arg.present]:
    310         self.parse_errors.append("%s: at least one must be present" %
    311                          (", ".join(["'%s'" % arg.names[-1] for arg in group])))
    312 
    313     # if we have any validators, invoke them
    314     if not self.parse_errors and self.validator:
    315       try:
    316         self.validator(self)
    317       except ParseError, e:
    318         self.parse_errors.append(e.args[0])
    319 
    320   # Helper methods so you can treat the command like a dict
    321   def __getitem__(self, key):
    322     arg = self.arg_dict[key.lower()]
    323 
    324     if arg.type == 'flag':
    325       return arg.present
    326     else:
    327       return arg.value
    328 
    329   def __iter__(self):
    330     return [arg for arg in self.args if arg.present].__iter__()
    331 
    332   def ArgumentPresent(self, key):
    333     """Tests if an argument exists and has been specified."""
    334     return key.lower() in self.arg_dict and self.arg_dict[key.lower()].present
    335 
    336   def __contains__(self, key):
    337     return self.ArgumentPresent(key)
    338 
    339   def ParseNextArgument(self):
    340     """Find the next argument in the command line and parse it."""
    341     arg = None
    342     value = None
    343     argstr = self.cmdline.rargs.pop(0)
    344 
    345     # First check: is this a literal argument?
    346     if argstr.lower() in self.arg_dict:
    347       arg = self.arg_dict[argstr.lower()]
    348       if arg.type in Command.Argument.TYPES_WITH_VALUES:
    349         if len(self.cmdline.rargs):
    350           value = self.cmdline.rargs.pop(0)
    351 
    352     # Second check: is this of the form "arg=val" or "arg:val"?
    353     if arg is None:
    354       delimiter_pos = -1
    355 
    356       for delimiter in [':', '=']:
    357         pos = argstr.find(delimiter)
    358         if pos >= 0:
    359           if delimiter_pos < 0 or pos < delimiter_pos:
    360             delimiter_pos = pos
    361 
    362       if delimiter_pos >= 0:
    363         testarg = argstr[:delimiter_pos]
    364         testval = argstr[delimiter_pos+1:]
    365 
    366         if testarg.lower() in self.arg_dict:
    367           arg = self.arg_dict[testarg.lower()]
    368           argstr = testarg
    369           value = testval
    370 
    371     # Third check: does this begin an argument?
    372     if arg is None:
    373       for key in self.arg_dict.iterkeys():
    374         if (len(key) < len(argstr) and
    375             self.arg_dict[key].type in Command.Argument.TYPES_WITH_VALUES and
    376             argstr[:len(key)].lower() == key):
    377           value = argstr[len(key):]
    378           argstr = argstr[:len(key)]
    379           arg = self.arg_dict[argstr]
    380 
    381     # Fourth check: do we have any positional arguments available?
    382     if arg is None:
    383       for positional_arg in [
    384           testarg for testarg in self.args if testarg.positional]:
    385         if not positional_arg.present:
    386           arg = positional_arg
    387           value = argstr
    388           argstr = positional_arg.names[0]
    389           break
    390 
    391     # Push the retrieved argument/value onto the largs stack
    392     if argstr: self.cmdline.largs.append(argstr)
    393     if value:  self.cmdline.largs.append(value)
    394 
    395     # If we've made it this far and haven't found an arg, give up
    396     if arg is None:
    397       raise ParseError("Unknown argument: '%s'" % argstr)
    398 
    399     # Convert the value, if necessary
    400     if arg.type in Command.Argument.TYPES_WITH_VALUES and value is None:
    401       raise ParseError("Argument '%s' requires a value" % argstr)
    402 
    403     if value is not None:
    404       value = self.StringToValue(value, arg.type, argstr)
    405 
    406     arg.argstr = argstr
    407     arg.value = value
    408     arg.present = True
    409 
    410     # end method ParseNextArgument
    411 
    412   def StringToValue(self, value, type, argstr):
    413     """Convert a string from the command line to a value type."""
    414     try:
    415       if type == 'string':
    416         pass  # leave it be
    417 
    418       elif type == 'int':
    419         try:
    420           value = int(value)
    421         except ValueError:
    422           raise ParseError
    423 
    424       elif type == 'readfile':
    425         if not os.path.isfile(value):
    426           raise ParseError("'%s': '%s' does not exist" % (argstr, value))
    427 
    428       elif type == 'coords':
    429         try:
    430           value = [int(val) for val in
    431                    re.match("\(\s*(\d+)\s*\,\s*(\d+)\s*\)\s*\Z", value).
    432                    groups()]
    433         except AttributeError:
    434           raise ParseError
    435 
    436       else:
    437         raise ValueError("Unknown type: '%s'" % type)
    438 
    439     except ParseError, e:
    440       # The bare exception is raised in the generic case; more specific errors
    441       # will arrive with arguments and should just be reraised
    442       if not e.args:
    443         e = ParseError("'%s': unable to convert '%s' to type '%s'" %
    444                        (argstr, value, type))
    445       raise e
    446 
    447     return value
    448 
    449   def SortArgs(self):
    450     """Returns a method that can be passed to sort() to sort arguments."""
    451 
    452     def ArgSorter(arg1, arg2):
    453       """Helper for sorting arguments in the usage string.
    454 
    455       Positional arguments come first, then required arguments,
    456       then optional arguments. Pylint demands this trivial function
    457       have both Args: and Returns: sections, sigh.
    458 
    459       Args:
    460         arg1: the first argument to compare
    461         arg2: the second argument to compare
    462 
    463       Returns:
    464         -1 if arg1 should be sorted first, +1 if it should be sorted second,
    465         and 0 if arg1 and arg2 have the same sort level.
    466       """
    467       return ((arg2.positional-arg1.positional)*2 +
    468               (arg2.required-arg1.required))
    469     return ArgSorter
    470 
    471   def GetUsageString(self, width=80, name=None):
    472     """Gets a string describing how the command is used."""
    473     if name is None: name = self.names[0]
    474 
    475     initial_indent = "Usage: %s %s " % (self.cmdline.prog, name)
    476     subsequent_indent = " " * len(initial_indent)
    477 
    478     sorted_args = self.args[:]
    479     sorted_args.sort(self.SortArgs())
    480 
    481     return textwrap.fill(
    482       " ".join([arg.GetUsageString() for arg in sorted_args]), width,
    483       initial_indent=initial_indent,
    484       subsequent_indent=subsequent_indent)
    485 
    486   def GetHelpString(self, width=80):
    487     """Returns a list of help strings for all this command's arguments."""
    488     sorted_args = self.args[:]
    489     sorted_args.sort(self.SortArgs())
    490 
    491     return "\n".join([arg.GetHelpString(width) for arg in sorted_args])
    492 
    493   # end class Command
    494 
    495 
    496 class CommandLine(object):
    497   """Parse a command line, extracting a command and its arguments."""
    498 
    499   def __init__(self):
    500     self.commands = []
    501     self.cmd_dict = {}
    502 
    503     # Add the help command to the parser
    504     help_cmd = self.AddCommand(["help", "--help", "-?", "-h"],
    505                                "Displays help text for a command",
    506                                ValidateHelpCommand,
    507                                DoHelpCommand)
    508 
    509     help_cmd.AddArgument(
    510       "command", "Command to retrieve help for", positional=True)
    511     help_cmd.AddArgument(
    512       "--width", "Width of the output", type='int', default=80)
    513 
    514     self.Exit = sys.exit   # override this if you don't want the script to halt
    515                            # on error or on display of help
    516 
    517     self.out = sys.stdout  # override these if you want to redirect
    518     self.err = sys.stderr  # output or error messages
    519 
    520   def AddCommand(self, names, helptext, validator=None, impl=None):
    521     """Add a new command to the parser.
    522 
    523     Args:
    524       names:       command name, or list of synonyms
    525       helptext:    brief string description of the command
    526       validator:   method to validate a command's arguments
    527       impl:        callable to be invoked when command is called
    528 
    529     Raises:
    530       ValueError: raised if command already added
    531 
    532     Returns:
    533       The new command
    534     """
    535     if IsString(names): names = [names]
    536 
    537     for name in names:
    538       if name in self.cmd_dict:
    539         raise ValueError("%s is already a command"%name)
    540 
    541     cmd = Command(names, helptext, validator, impl)
    542     cmd.cmdline = self
    543 
    544     self.commands.append(cmd)
    545     for name in names:
    546       self.cmd_dict[name.lower()] = cmd
    547 
    548     return cmd
    549 
    550   def GetUsageString(self):
    551     """Returns simple usage instructions."""
    552     return "Type '%s help' for usage." % self.prog
    553 
    554   def ParseCommandLine(self, argv=None, prog=None, execute=True):
    555     """Does the work of parsing a command line.
    556 
    557     Args:
    558       argv:     list of arguments, defaults to sys.args[1:]
    559       prog:     name of the command, defaults to the base name of the script
    560       execute:  if false, just parse, don't invoke the 'impl' member
    561 
    562     Returns:
    563       The command that was executed
    564     """
    565     if argv is None: argv = sys.argv[1:]
    566     if prog is None: prog = os.path.basename(sys.argv[0]).split('.')[0]
    567 
    568     # Store off our parameters, we may need them someday
    569     self.argv = argv
    570     self.prog = prog
    571 
    572     # We shouldn't be invoked without arguments, that's just lame
    573     if not len(argv):
    574       self.out.writelines(self.GetUsageString())
    575       self.Exit()
    576       return None   # in case the client overrides Exit
    577 
    578     # Is it a valid command?
    579     self.command_string = argv[0].lower()
    580     if not self.command_string in self.cmd_dict:
    581       self.err.write("Unknown command: '%s'\n\n" % self.command_string)
    582       self.out.write(self.GetUsageString())
    583       self.Exit()
    584       return None   # in case the client overrides Exit
    585 
    586     self.command = self.cmd_dict[self.command_string]
    587 
    588     # "rargs" = remaining (unparsed) arguments
    589     # "largs" = already parsed, "left" of the read head
    590     self.rargs = argv[1:]
    591     self.largs = []
    592 
    593     # let the command object do the parsing
    594     self.command.ParseArguments()
    595 
    596     if self.command.parse_errors:
    597       # there were errors, output the usage string and exit
    598       self.err.write(self.command.GetUsageString()+"\n\n")
    599       self.err.write("\n".join(self.command.parse_errors))
    600       self.err.write("\n\n")
    601 
    602       self.Exit()
    603 
    604     elif execute and self.command.impl:
    605       self.command.impl(self.command)
    606 
    607     return self.command
    608 
    609   def __getitem__(self, key):
    610     return self.cmd_dict[key]
    611 
    612   def __iter__(self):
    613     return self.cmd_dict.__iter__()
    614 
    615 
    616 def ValidateHelpCommand(command):
    617   """Checks to make sure an argument to 'help' is a valid command."""
    618   if 'command' in command and command['command'] not in command.cmdline:
    619     raise ParseError("'%s': unknown command" % command['command'])
    620 
    621 
    622 def DoHelpCommand(command):
    623   """Executed when the command is 'help'."""
    624   out = command.cmdline.out
    625   width = command['--width']
    626 
    627   if 'command' not in command:
    628     out.write(command.GetUsageString())
    629     out.write("\n\n")
    630 
    631     indent = 5
    632     gutter = 2
    633 
    634     command_width = (
    635       max([len(cmd.names[0]) for cmd in command.cmdline.commands]) + gutter)
    636 
    637     for cmd in command.cmdline.commands:
    638       cmd_name = cmd.names[0]
    639 
    640       initial_indent = (" "*indent + cmd_name + " "*
    641                         (command_width+gutter-len(cmd_name)))
    642       subsequent_indent = " "*(indent+command_width+gutter)
    643 
    644       out.write(textwrap.fill(cmd.helptext, width,
    645                               initial_indent=initial_indent,
    646                               subsequent_indent=subsequent_indent))
    647       out.write("\n")
    648 
    649     out.write("\n")
    650 
    651   else:
    652     help_cmd = command.cmdline[command['command']]
    653 
    654     out.write(textwrap.fill(help_cmd.helptext, width))
    655     out.write("\n\n")
    656     out.write(help_cmd.GetUsageString(width=width))
    657     out.write("\n\n")
    658     out.write(help_cmd.GetHelpString(width=width))
    659     out.write("\n")
    660 
    661     command.cmdline.Exit()
    662 
    663 
    664 def main():
    665   # If we're invoked rather than imported, run some tests
    666   cmdline = CommandLine()
    667 
    668   # Since we're testing, override Exit()
    669   def TestExit():
    670     pass
    671   cmdline.Exit = TestExit
    672 
    673   # Actually, while we're at it, let's override error output too
    674   cmdline.err = open(os.path.devnull, "w")
    675 
    676   test = cmdline.AddCommand(["test", "testa", "testb"], "test command")
    677   test.AddArgument(["-i", "--int", "--integer", "--optint", "--optionalint"],
    678                    "optional integer parameter", type='int')
    679   test.AddArgument("--reqint", "required integer parameter", type='int',
    680                    required=True)
    681   test.AddArgument("pos1", "required positional argument", positional=True,
    682                    required=True)
    683   test.AddArgument("pos2", "optional positional argument", positional=True)
    684   test.AddArgument("pos3", "another optional positional arg",
    685                    positional=True)
    686 
    687   # mutually dependent arguments
    688   test.AddArgument("--mutdep1", "mutually dependent parameter 1")
    689   test.AddArgument("--mutdep2", "mutually dependent parameter 2")
    690   test.AddArgument("--mutdep3", "mutually dependent parameter 3")
    691   test.AddMutualDependency(["--mutdep1", "--mutdep2", "--mutdep3"])
    692 
    693   # mutually exclusive arguments
    694   test.AddArgument("--mutex1", "mutually exclusive parameter 1")
    695   test.AddArgument("--mutex2", "mutually exclusive parameter 2")
    696   test.AddArgument("--mutex3", "mutually exclusive parameter 3")
    697   test.AddMutualExclusion(["--mutex1", "--mutex2", "--mutex3"])
    698 
    699   # dependent argument
    700   test.AddArgument("--dependent", "dependent argument")
    701   test.AddDependency("--dependent", "--int")
    702 
    703   # other argument types
    704   test.AddArgument("--file", "filename argument", type='readfile')
    705   test.AddArgument("--coords", "coordinate argument", type='coords')
    706   test.AddArgument("--flag", "flag argument", type='flag')
    707 
    708   test.AddArgument("--req1", "part of a required group", type='flag')
    709   test.AddArgument("--req2", "part 2 of a required group", type='flag')
    710 
    711   test.AddRequiredGroup(["--req1", "--req2"])
    712 
    713   # a few failure cases
    714   exception_cases = """
    715     test.AddArgument("failpos", "can't have req'd pos arg after opt",
    716        positional=True, required=True)
    717 +++
    718     test.AddArgument("--int", "this argument already exists")
    719 +++
    720     test.AddDependency("--int", "--doesntexist")
    721 +++
    722     test.AddMutualDependency(["--doesntexist", "--mutdep2"])
    723 +++
    724     test.AddMutualExclusion(["--doesntexist", "--mutex2"])
    725 +++
    726     test.AddArgument("--reqflag", "required flag", required=True, type='flag')
    727 +++
    728     test.AddRequiredGroup(["--req1", "--doesntexist"])
    729 """
    730   for exception_case in exception_cases.split("+++"):
    731     try:
    732       exception_case = exception_case.strip()
    733       exec exception_case     # yes, I'm using exec, it's just for a test.
    734     except ValueError:
    735       # this is expected
    736       pass
    737     except KeyError:
    738       # ...and so is this
    739       pass
    740     else:
    741       print ("FAILURE: expected an exception for '%s'"
    742              " and didn't get it" % exception_case)
    743 
    744   # Let's do some parsing! first, the minimal success line:
    745   MIN = "test --reqint 123 param1 --req1 "
    746 
    747   # tuples of (command line, expected error count)
    748   test_lines = [
    749     ("test --int 3 foo --req1", 1),   # missing required named parameter
    750     ("test --reqint 3 --req1", 1),    # missing required positional parameter
    751     (MIN, 0),                         # success!
    752     ("test param1 --reqint 123 --req1", 0),  # success, order shouldn't matter
    753     ("test param1 --reqint 123 --req2", 0),  # success, any of required group ok
    754     (MIN+"param2", 0),                # another positional parameter is okay
    755     (MIN+"param2 param3", 0),         # and so are three
    756     (MIN+"param2 param3 param4", 1),  # but four are just too many
    757     (MIN+"--int", 1),                 # where's the value?
    758     (MIN+"--int 456", 0),             # this is fine
    759     (MIN+"--int456", 0),              # as is this
    760     (MIN+"--int:456", 0),             # and this
    761     (MIN+"--int=456", 0),             # and this
    762     (MIN+"--file c:\\windows\\system32\\kernel32.dll", 0),  # yup
    763     (MIN+"--file c:\\thisdoesntexist", 1),           # nope
    764     (MIN+"--mutdep1 a", 2),                          # no!
    765     (MIN+"--mutdep2 b", 2),                          # also no!
    766     (MIN+"--mutdep3 c", 2),                          # dream on!
    767     (MIN+"--mutdep1 a --mutdep2 b", 2),              # almost!
    768     (MIN+"--mutdep1 a --mutdep2 b --mutdep3 c", 0),  # yes
    769     (MIN+"--mutex1 a", 0),                           # yes
    770     (MIN+"--mutex2 b", 0),                           # yes
    771     (MIN+"--mutex3 c", 0),                           # fine
    772     (MIN+"--mutex1 a --mutex2 b", 1),                # not fine
    773     (MIN+"--mutex1 a --mutex2 b --mutex3 c", 3),     # even worse
    774     (MIN+"--dependent 1", 1),                        # no
    775     (MIN+"--dependent 1 --int 2", 0),                # ok
    776     (MIN+"--int abc", 1),                            # bad type
    777     (MIN+"--coords abc", 1),                         # also bad
    778     (MIN+"--coords (abc)", 1),                       # getting warmer
    779     (MIN+"--coords (abc,def)", 1),                   # missing something
    780     (MIN+"--coords (123)", 1),                       # ooh, so close
    781     (MIN+"--coords (123,def)", 1),                   # just a little farther
    782     (MIN+"--coords (123,456)", 0),                   # finally!
    783     ("test --int 123 --reqint=456 foo bar --coords(42,88) baz --req1", 0)
    784     ]
    785 
    786   badtests = 0
    787 
    788   for (test, expected_failures) in test_lines:
    789     cmdline.ParseCommandLine([x.strip() for x in test.strip().split(" ")])
    790 
    791     if not len(cmdline.command.parse_errors) == expected_failures:
    792       print "FAILED:\n  issued: '%s'\n  expected: %d\n  received: %d\n\n" % (
    793         test, expected_failures, len(cmdline.command.parse_errors))
    794       badtests += 1
    795 
    796   print "%d failed out of %d tests" % (badtests, len(test_lines))
    797 
    798   cmdline.ParseCommandLine(["help", "test"])
    799 
    800 
    801 if __name__ == "__main__":
    802   sys.exit(main())
    803