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
8Supports the concept of command line commands, each with its own set
9of arguments. Supports dependent arguments and mutually exclusive arguments.
10Basically, a better optparse. I took heed of epg's WHINE() in gvn.cmdline
11and dumped optparse in favor of something better.
12"""
13
14import os.path
15import re
16import string
17import sys
18import textwrap
19import types
20
21
22def IsString(var):
23  """Little helper function to see if a variable is a string."""
24  return type(var) in types.StringTypes
25
26
27class ParseError(Exception):
28  """Encapsulates errors from parsing, string arg is description."""
29  pass
30
31
32class 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
496class 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
616def 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
622def 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
664def 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
801if __name__ == "__main__":
802  sys.exit(main())
803