summaryrefslogtreecommitdiffstats
path: root/tools/site_compare/command_line.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/site_compare/command_line.py')
-rw-r--r--tools/site_compare/command_line.py292
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"])