diff options
Diffstat (limited to 'tools/site_compare/command_line.py')
-rw-r--r-- | tools/site_compare/command_line.py | 292 |
1 files changed, 146 insertions, 146 deletions
diff --git a/tools/site_compare/command_line.py b/tools/site_compare/command_line.py index b474abf..2c87fb9 100644 --- a/tools/site_compare/command_line.py +++ b/tools/site_compare/command_line.py @@ -56,11 +56,11 @@ class Command(object): """Encapsulates an argument to a command.""" VALID_TYPES = ['string', 'readfile', 'int', 'flag', 'coords'] TYPES_WITH_VALUES = ['string', 'readfile', 'int', 'coords'] - + def __init__(self, names, helptext, type, metaname, required, default, positional): """Command-line argument to a command. - + Args: names: argument name, or list of synonyms helptext: brief description of the argument @@ -76,28 +76,28 @@ class Command(object): required: True if argument must be specified default: Default value if not specified positional: Argument specified by location, not name - + Raises: ValueError: the argument name is invalid for some reason """ if type not in Command.Argument.VALID_TYPES: raise ValueError("Invalid type: %r" % type) - + if required and default is not None: raise ValueError("required and default are mutually exclusive") - + if required and type == 'flag': raise ValueError("A required flag? Give me a break.") - + if metaname and type not in Command.Argument.TYPES_WITH_VALUES: raise ValueError("Type %r can't have a metaname" % type) - + # If no metaname is provided, infer it: use the alphabetical characters # of the last provided name if not metaname and type in Command.Argument.TYPES_WITH_VALUES: metaname = ( names[-1].lstrip(string.punctuation + string.whitespace).upper()) - + self.names = names self.helptext = helptext self.type = type @@ -105,31 +105,31 @@ class Command(object): self.default = default self.positional = positional self.metaname = metaname - + self.mutex = [] # arguments that are mutually exclusive with # this one self.depends = [] # arguments that must be present for this # one to be valid self.present = False # has this argument been specified? - + def AddDependency(self, arg): """Makes this argument dependent on another argument. - + Args: arg: name of the argument this one depends on """ if arg not in self.depends: self.depends.append(arg) - + def AddMutualExclusion(self, arg): """Makes this argument invalid if another is specified. - + Args: arg: name of the mutually exclusive argument. """ if arg not in self.mutex: self.mutex.append(arg) - + def GetUsageString(self): """Returns a brief string describing the argument's usage.""" if not self.positional: @@ -138,49 +138,49 @@ class Command(object): string += "="+self.metaname else: string = self.metaname - + if not self.required: string = "["+string+"]" - + return string - + def GetNames(self): """Returns a string containing a list of the arg's names.""" if self.positional: return self.metaname else: return ", ".join(self.names) - + def GetHelpString(self, width=80, indent=5, names_width=20, gutter=2): """Returns a help string including help for all the arguments.""" names = [" "*indent + line +" "*(names_width-len(line)) for line in textwrap.wrap(self.GetNames(), names_width)] - + helpstring = textwrap.wrap(self.helptext, width-indent-names_width-gutter) - + if len(names) < len(helpstring): names += [" "*(indent+names_width)]*(len(helpstring)-len(names)) - + if len(helpstring) < len(names): helpstring += [""]*(len(names)-len(helpstring)) - + return "\n".join([name_line + " "*gutter + help_line for name_line, help_line in zip(names, helpstring)]) - + def __repr__(self): if self.present: string = '= %r' % self.value else: string = "(absent)" - + return "Argument %s '%s'%s" % (self.type, self.names[0], string) - + # end of nested class Argument - + def AddArgument(self, names, helptext, type="string", metaname=None, required=False, default=None, positional=False): """Command-line argument to a command. - + Args: names: argument name, or list of synonyms helptext: brief description of the argument @@ -189,82 +189,82 @@ class Command(object): required: True if argument must be specified default: Default value if not specified positional: Argument specified by location, not name - + Raises: ValueError: the argument already exists or is invalid - + Returns: The newly-created argument """ if IsString(names): names = [names] - + names = [name.lower() for name in names] - + for name in names: if name in self.arg_dict: raise ValueError("%s is already an argument"%name) - + if (positional and required and [arg for arg in self.args if arg.positional] and not [arg for arg in self.args if arg.positional][-1].required): raise ValueError( "A required positional argument may not follow an optional one.") - + arg = Command.Argument(names, helptext, type, metaname, required, default, positional) - + self.args.append(arg) - + for name in names: self.arg_dict[name] = arg - + return arg - + def GetArgument(self, name): """Return an argument from a name.""" return self.arg_dict[name.lower()] - + def AddMutualExclusion(self, args): """Specifies that a list of arguments are mutually exclusive.""" if len(args) < 2: raise ValueError("At least two arguments must be specified.") - + args = [arg.lower() for arg in args] - + for index in xrange(len(args)-1): for index2 in xrange(index+1, len(args)): self.arg_dict[args[index]].AddMutualExclusion(self.arg_dict[args[index2]]) - + def AddDependency(self, dependent, depends_on): """Specifies that one argument may only be present if another is. - + Args: dependent: the name of the dependent argument depends_on: the name of the argument on which it depends """ self.arg_dict[dependent.lower()].AddDependency( self.arg_dict[depends_on.lower()]) - + def AddMutualDependency(self, args): """Specifies that a list of arguments are all mutually dependent.""" if len(args) < 2: raise ValueError("At least two arguments must be specified.") - + args = [arg.lower() for arg in args] - + for (arg1, arg2) in [(arg1, arg2) for arg1 in args for arg2 in args]: if arg1 == arg2: continue self.arg_dict[arg1].AddDependency(self.arg_dict[arg2]) - + def AddRequiredGroup(self, args): """Specifies that at least one of the named arguments must be present.""" if len(args) < 2: raise ValueError("At least two arguments must be in a required group.") - + args = [self.arg_dict[arg.lower()] for arg in args] - + self.required_groups.append(args) - + def ParseArguments(self): """Given a command line, parse and validate the arguments.""" @@ -272,70 +272,70 @@ class Command(object): for arg in self.args: arg.present = False arg.value = None - + self.parse_errors = [] - + # look for arguments remaining on the command line while len(self.cmdline.rargs): try: self.ParseNextArgument() except ParseError, e: self.parse_errors.append(e.args[0]) - + # after all the arguments are parsed, check for problems for arg in self.args: if not arg.present and arg.required: self.parse_errors.append("'%s': required parameter was missing" % arg.names[0]) - + if not arg.present and arg.default: arg.present = True arg.value = arg.default - + if arg.present: for mutex in arg.mutex: if mutex.present: self.parse_errors.append( "'%s', '%s': arguments are mutually exclusive" % (arg.argstr, mutex.argstr)) - + for depend in arg.depends: if not depend.present: self.parse_errors.append("'%s': '%s' must be specified as well" % (arg.argstr, depend.names[0])) - + # check for required groups for group in self.required_groups: if not [arg for arg in group if arg.present]: self.parse_errors.append("%s: at least one must be present" % (", ".join(["'%s'" % arg.names[-1] for arg in group]))) - + # if we have any validators, invoke them if not self.parse_errors and self.validator: try: self.validator(self) except ParseError, e: self.parse_errors.append(e.args[0]) - + # Helper methods so you can treat the command like a dict def __getitem__(self, key): arg = self.arg_dict[key.lower()] - + if arg.type == 'flag': return arg.present else: return arg.value - + def __iter__(self): return [arg for arg in self.args if arg.present].__iter__() - + def ArgumentPresent(self, key): """Tests if an argument exists and has been specified.""" return key.lower() in self.arg_dict and self.arg_dict[key.lower()].present - + def __contains__(self, key): return self.ArgumentPresent(key) - + def ParseNextArgument(self): """Find the next argument in the command line and parse it.""" arg = None @@ -348,26 +348,26 @@ class Command(object): if arg.type in Command.Argument.TYPES_WITH_VALUES: if len(self.cmdline.rargs): value = self.cmdline.rargs.pop(0) - + # Second check: is this of the form "arg=val" or "arg:val"? if arg is None: delimiter_pos = -1 - + for delimiter in [':', '=']: pos = argstr.find(delimiter) if pos >= 0: if delimiter_pos < 0 or pos < delimiter_pos: delimiter_pos = pos - + if delimiter_pos >= 0: testarg = argstr[:delimiter_pos] testval = argstr[delimiter_pos+1:] - + if testarg.lower() in self.arg_dict: arg = self.arg_dict[testarg.lower()] argstr = testarg value = testval - + # Third check: does this begin an argument? if arg is None: for key in self.arg_dict.iterkeys(): @@ -377,7 +377,7 @@ class Command(object): value = argstr[len(key):] argstr = argstr[:len(key)] arg = self.arg_dict[argstr] - + # Fourth check: do we have any positional arguments available? if arg is None: for positional_arg in [ @@ -391,40 +391,40 @@ class Command(object): # Push the retrieved argument/value onto the largs stack if argstr: self.cmdline.largs.append(argstr) if value: self.cmdline.largs.append(value) - + # If we've made it this far and haven't found an arg, give up if arg is None: raise ParseError("Unknown argument: '%s'" % argstr) - + # Convert the value, if necessary if arg.type in Command.Argument.TYPES_WITH_VALUES and value is None: raise ParseError("Argument '%s' requires a value" % argstr) - + if value is not None: value = self.StringToValue(value, arg.type, argstr) arg.argstr = argstr arg.value = value arg.present = True - + # end method ParseNextArgument - + def StringToValue(self, value, type, argstr): """Convert a string from the command line to a value type.""" try: if type == 'string': pass # leave it be - + elif type == 'int': try: value = int(value) except ValueError: raise ParseError - + elif type == 'readfile': if not os.path.isfile(value): raise ParseError("'%s': '%s' does not exist" % (argstr, value)) - + elif type == 'coords': try: value = [int(val) for val in @@ -432,10 +432,10 @@ class Command(object): groups()] except AttributeError: raise ParseError - + else: raise ValueError("Unknown type: '%s'" % type) - + except ParseError, e: # The bare exception is raised in the generic case; more specific errors # will arrive with arguments and should just be reraised @@ -443,23 +443,23 @@ class Command(object): e = ParseError("'%s': unable to convert '%s' to type '%s'" % (argstr, value, type)) raise e - + return value - + def SortArgs(self): """Returns a method that can be passed to sort() to sort arguments.""" - + def ArgSorter(arg1, arg2): """Helper for sorting arguments in the usage string. - + Positional arguments come first, then required arguments, then optional arguments. Pylint demands this trivial function have both Args: and Returns: sections, sigh. - + Args: arg1: the first argument to compare arg2: the second argument to compare - + Returns: -1 if arg1 should be sorted first, +1 if it should be sorted second, and 0 if arg1 and arg2 have the same sort level. @@ -467,56 +467,56 @@ class Command(object): return ((arg2.positional-arg1.positional)*2 + (arg2.required-arg1.required)) return ArgSorter - + def GetUsageString(self, width=80, name=None): """Gets a string describing how the command is used.""" if name is None: name = self.names[0] - + initial_indent = "Usage: %s %s " % (self.cmdline.prog, name) subsequent_indent = " " * len(initial_indent) - + sorted_args = self.args[:] sorted_args.sort(self.SortArgs()) - + return textwrap.fill( " ".join([arg.GetUsageString() for arg in sorted_args]), width, initial_indent=initial_indent, subsequent_indent=subsequent_indent) - + def GetHelpString(self, width=80): """Returns a list of help strings for all this command's arguments.""" sorted_args = self.args[:] sorted_args.sort(self.SortArgs()) - + return "\n".join([arg.GetHelpString(width) for arg in sorted_args]) - + # end class Command - - + + class CommandLine(object): """Parse a command line, extracting a command and its arguments.""" - + def __init__(self): self.commands = [] self.cmd_dict = {} - + # Add the help command to the parser help_cmd = self.AddCommand(["help", "--help", "-?", "-h"], "Displays help text for a command", ValidateHelpCommand, DoHelpCommand) - + help_cmd.AddArgument( "command", "Command to retrieve help for", positional=True) help_cmd.AddArgument( "--width", "Width of the output", type='int', default=80) - + self.Exit = sys.exit # override this if you don't want the script to halt # on error or on display of help - + self.out = sys.stdout # override these if you want to redirect self.err = sys.stderr # output or error messages - + def AddCommand(self, names, helptext, validator=None, impl=None): """Add a new command to the parser. @@ -525,56 +525,56 @@ class CommandLine(object): helptext: brief string description of the command validator: method to validate a command's arguments impl: callable to be invoked when command is called - + Raises: ValueError: raised if command already added - + Returns: The new command """ if IsString(names): names = [names] - + for name in names: if name in self.cmd_dict: raise ValueError("%s is already a command"%name) - + cmd = Command(names, helptext, validator, impl) cmd.cmdline = self - + self.commands.append(cmd) for name in names: self.cmd_dict[name.lower()] = cmd - + return cmd - + def GetUsageString(self): """Returns simple usage instructions.""" return "Type '%s help' for usage." % self.prog - + def ParseCommandLine(self, argv=None, prog=None, execute=True): """Does the work of parsing a command line. - + Args: argv: list of arguments, defaults to sys.args[1:] prog: name of the command, defaults to the base name of the script execute: if false, just parse, don't invoke the 'impl' member - + Returns: The command that was executed """ if argv is None: argv = sys.argv[1:] if prog is None: prog = os.path.basename(sys.argv[0]).split('.')[0] - + # Store off our parameters, we may need them someday self.argv = argv self.prog = prog - + # We shouldn't be invoked without arguments, that's just lame if not len(argv): self.out.writelines(self.GetUsageString()) self.Exit() return None # in case the client overrides Exit - + # Is it a valid command? self.command_string = argv[0].lower() if not self.command_string in self.cmd_dict: @@ -582,33 +582,33 @@ class CommandLine(object): self.out.write(self.GetUsageString()) self.Exit() return None # in case the client overrides Exit - + self.command = self.cmd_dict[self.command_string] - + # "rargs" = remaining (unparsed) arguments # "largs" = already parsed, "left" of the read head self.rargs = argv[1:] self.largs = [] - + # let the command object do the parsing self.command.ParseArguments() - + if self.command.parse_errors: # there were errors, output the usage string and exit self.err.write(self.command.GetUsageString()+"\n\n") self.err.write("\n".join(self.command.parse_errors)) self.err.write("\n\n") - + self.Exit() - + elif execute and self.command.impl: self.command.impl(self.command) - + return self.command - + def __getitem__(self, key): return self.cmd_dict[key] - + def __iter__(self): return self.cmd_dict.__iter__() @@ -618,25 +618,25 @@ def ValidateHelpCommand(command): if 'command' in command and command['command'] not in command.cmdline: raise ParseError("'%s': unknown command" % command['command']) - + def DoHelpCommand(command): """Executed when the command is 'help'.""" out = command.cmdline.out width = command['--width'] - + if 'command' not in command: out.write(command.GetUsageString()) out.write("\n\n") - + indent = 5 gutter = 2 - + command_width = ( max([len(cmd.names[0]) for cmd in command.cmdline.commands]) + gutter) - + for cmd in command.cmdline.commands: cmd_name = cmd.names[0] - + initial_indent = (" "*indent + cmd_name + " "* (command_width+gutter-len(cmd_name))) subsequent_indent = " "*(indent+command_width+gutter) @@ -645,9 +645,9 @@ def DoHelpCommand(command): initial_indent=initial_indent, subsequent_indent=subsequent_indent)) out.write("\n") - + out.write("\n") - + else: help_cmd = command.cmdline[command['command']] @@ -657,21 +657,21 @@ def DoHelpCommand(command): out.write("\n\n") out.write(help_cmd.GetHelpString(width=width)) out.write("\n") - + command.cmdline.Exit() - + if __name__ == "__main__": # If we're invoked rather than imported, run some tests cmdline = CommandLine() - + # Since we're testing, override Exit() def TestExit(): pass cmdline.Exit = TestExit - + # Actually, while we're at it, let's override error output too cmdline.err = open(os.path.devnull, "w") - + test = cmdline.AddCommand(["test", "testa", "testb"], "test command") test.AddArgument(["-i", "--int", "--integer", "--optint", "--optionalint"], "optional integer parameter", type='int') @@ -688,25 +688,25 @@ if __name__ == "__main__": test.AddArgument("--mutdep2", "mutually dependent parameter 2") test.AddArgument("--mutdep3", "mutually dependent parameter 3") test.AddMutualDependency(["--mutdep1", "--mutdep2", "--mutdep3"]) - + # mutually exclusive arguments test.AddArgument("--mutex1", "mutually exclusive parameter 1") test.AddArgument("--mutex2", "mutually exclusive parameter 2") test.AddArgument("--mutex3", "mutually exclusive parameter 3") test.AddMutualExclusion(["--mutex1", "--mutex2", "--mutex3"]) - + # dependent argument test.AddArgument("--dependent", "dependent argument") test.AddDependency("--dependent", "--int") - + # other argument types test.AddArgument("--file", "filename argument", type='readfile') test.AddArgument("--coords", "coordinate argument", type='coords') test.AddArgument("--flag", "flag argument", type='flag') - + test.AddArgument("--req1", "part of a required group", type='flag') test.AddArgument("--req2", "part 2 of a required group", type='flag') - + test.AddRequiredGroup(["--req1", "--req2"]) # a few failure cases @@ -742,7 +742,7 @@ if __name__ == "__main__": # Let's do some parsing! first, the minimal success line: MIN = "test --reqint 123 param1 --req1 " - + # tuples of (command line, expected error count) test_lines = [ ("test --int 3 foo --req1", 1), # missing required named parameter @@ -781,19 +781,19 @@ if __name__ == "__main__": (MIN+"--coords (123,456)", 0), # finally! ("test --int 123 --reqint=456 foo bar --coords(42,88) baz --req1", 0) ] - + badtests = 0 - + for (test, expected_failures) in test_lines: cmdline.ParseCommandLine([x.strip() for x in test.strip().split(" ")]) - + if not len(cmdline.command.parse_errors) == expected_failures: print "FAILED:\n issued: '%s'\n expected: %d\n received: %d\n\n" % ( test, expected_failures, len(cmdline.command.parse_errors)) badtests += 1 - + print "%d failed out of %d tests" % (badtests, len(test_lines)) - + cmdline.ParseCommandLine(["help", "test"]) |