803 lines
27 KiB
Python
803 lines
27 KiB
Python
|
#!/usr/bin/env python
|
||
|
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
||
|
# Use of this source code is governed by a BSD-style license that can be
|
||
|
# found in the LICENSE file.
|
||
|
|
||
|
"""Parse a command line, retrieving a command and its arguments.
|
||
|
|
||
|
Supports the concept of command line commands, each with its own set
|
||
|
of arguments. Supports dependent arguments and mutually exclusive arguments.
|
||
|
Basically, a better optparse. I took heed of epg's WHINE() in gvn.cmdline
|
||
|
and dumped optparse in favor of something better.
|
||
|
"""
|
||
|
|
||
|
import os.path
|
||
|
import re
|
||
|
import string
|
||
|
import sys
|
||
|
import textwrap
|
||
|
import types
|
||
|
|
||
|
|
||
|
def IsString(var):
|
||
|
"""Little helper function to see if a variable is a string."""
|
||
|
return type(var) in types.StringTypes
|
||
|
|
||
|
|
||
|
class ParseError(Exception):
|
||
|
"""Encapsulates errors from parsing, string arg is description."""
|
||
|
pass
|
||
|
|
||
|
|
||
|
class Command(object):
|
||
|
"""Implements a single command."""
|
||
|
|
||
|
def __init__(self, names, helptext, validator=None, impl=None):
|
||
|
"""Initializes Command from names and helptext, plus optional callables.
|
||
|
|
||
|
Args:
|
||
|
names: command name, or list of synonyms
|
||
|
helptext: brief string description of the command
|
||
|
validator: callable for custom argument validation
|
||
|
Should raise ParseError if it wants
|
||
|
impl: callable to be invoked when command is called
|
||
|
"""
|
||
|
self.names = names
|
||
|
self.validator = validator
|
||
|
self.helptext = helptext
|
||
|
self.impl = impl
|
||
|
self.args = []
|
||
|
self.required_groups = []
|
||
|
self.arg_dict = {}
|
||
|
self.positional_args = []
|
||
|
self.cmdline = None
|
||
|
|
||
|
class Argument(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
|
||
|
type: type of the argument. Valid values include:
|
||
|
string - a string
|
||
|
readfile - a file which must exist and be available
|
||
|
for reading
|
||
|
int - an integer
|
||
|
flag - an optional flag (bool)
|
||
|
coords - (x,y) where x and y are ints
|
||
|
metaname: Name to display for value in help, inferred if not
|
||
|
specified
|
||
|
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
|
||
|
self.required = required
|
||
|
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:
|
||
|
string = self.names[0]
|
||
|
if self.type in Command.Argument.TYPES_WITH_VALUES:
|
||
|
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
|
||
|
type: type of the argument
|
||
|
metaname: Name to display for value in help, inferred if not
|
||
|
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."""
|
||
|
|
||
|
# reset all the arguments before we parse
|
||
|
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
|
||
|
value = None
|
||
|
argstr = self.cmdline.rargs.pop(0)
|
||
|
|
||
|
# First check: is this a literal argument?
|
||
|
if argstr.lower() in self.arg_dict:
|
||
|
arg = self.arg_dict[argstr.lower()]
|
||
|
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():
|
||
|
if (len(key) < len(argstr) and
|
||
|
self.arg_dict[key].type in Command.Argument.TYPES_WITH_VALUES and
|
||
|
argstr[:len(key)].lower() == key):
|
||
|
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 [
|
||
|
testarg for testarg in self.args if testarg.positional]:
|
||
|
if not positional_arg.present:
|
||
|
arg = positional_arg
|
||
|
value = argstr
|
||
|
argstr = positional_arg.names[0]
|
||
|
break
|
||
|
|
||
|
# 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
|
||
|
re.match("\(\s*(\d+)\s*\,\s*(\d+)\s*\)\s*\Z", value).
|
||
|
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
|
||
|
if not e.args:
|
||
|
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.
|
||
|
"""
|
||
|
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.
|
||
|
|
||
|
Args:
|
||
|
names: command name, or list of synonyms
|
||
|
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:
|
||
|
self.err.write("Unknown command: '%s'\n\n" % self.command_string)
|
||
|
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__()
|
||
|
|
||
|
|
||
|
def ValidateHelpCommand(command):
|
||
|
"""Checks to make sure an argument to 'help' is a valid 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)
|
||
|
|
||
|
out.write(textwrap.fill(cmd.helptext, width,
|
||
|
initial_indent=initial_indent,
|
||
|
subsequent_indent=subsequent_indent))
|
||
|
out.write("\n")
|
||
|
|
||
|
out.write("\n")
|
||
|
|
||
|
else:
|
||
|
help_cmd = command.cmdline[command['command']]
|
||
|
|
||
|
out.write(textwrap.fill(help_cmd.helptext, width))
|
||
|
out.write("\n\n")
|
||
|
out.write(help_cmd.GetUsageString(width=width))
|
||
|
out.write("\n\n")
|
||
|
out.write(help_cmd.GetHelpString(width=width))
|
||
|
out.write("\n")
|
||
|
|
||
|
command.cmdline.Exit()
|
||
|
|
||
|
|
||
|
def 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')
|
||
|
test.AddArgument("--reqint", "required integer parameter", type='int',
|
||
|
required=True)
|
||
|
test.AddArgument("pos1", "required positional argument", positional=True,
|
||
|
required=True)
|
||
|
test.AddArgument("pos2", "optional positional argument", positional=True)
|
||
|
test.AddArgument("pos3", "another optional positional arg",
|
||
|
positional=True)
|
||
|
|
||
|
# mutually dependent arguments
|
||
|
test.AddArgument("--mutdep1", "mutually dependent parameter 1")
|
||
|
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
|
||
|
exception_cases = """
|
||
|
test.AddArgument("failpos", "can't have req'd pos arg after opt",
|
||
|
positional=True, required=True)
|
||
|
+++
|
||
|
test.AddArgument("--int", "this argument already exists")
|
||
|
+++
|
||
|
test.AddDependency("--int", "--doesntexist")
|
||
|
+++
|
||
|
test.AddMutualDependency(["--doesntexist", "--mutdep2"])
|
||
|
+++
|
||
|
test.AddMutualExclusion(["--doesntexist", "--mutex2"])
|
||
|
+++
|
||
|
test.AddArgument("--reqflag", "required flag", required=True, type='flag')
|
||
|
+++
|
||
|
test.AddRequiredGroup(["--req1", "--doesntexist"])
|
||
|
"""
|
||
|
for exception_case in exception_cases.split("+++"):
|
||
|
try:
|
||
|
exception_case = exception_case.strip()
|
||
|
exec exception_case # yes, I'm using exec, it's just for a test.
|
||
|
except ValueError:
|
||
|
# this is expected
|
||
|
pass
|
||
|
except KeyError:
|
||
|
# ...and so is this
|
||
|
pass
|
||
|
else:
|
||
|
print ("FAILURE: expected an exception for '%s'"
|
||
|
" and didn't get it" % exception_case)
|
||
|
|
||
|
# 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
|
||
|
("test --reqint 3 --req1", 1), # missing required positional parameter
|
||
|
(MIN, 0), # success!
|
||
|
("test param1 --reqint 123 --req1", 0), # success, order shouldn't matter
|
||
|
("test param1 --reqint 123 --req2", 0), # success, any of required group ok
|
||
|
(MIN+"param2", 0), # another positional parameter is okay
|
||
|
(MIN+"param2 param3", 0), # and so are three
|
||
|
(MIN+"param2 param3 param4", 1), # but four are just too many
|
||
|
(MIN+"--int", 1), # where's the value?
|
||
|
(MIN+"--int 456", 0), # this is fine
|
||
|
(MIN+"--int456", 0), # as is this
|
||
|
(MIN+"--int:456", 0), # and this
|
||
|
(MIN+"--int=456", 0), # and this
|
||
|
(MIN+"--file c:\\windows\\system32\\kernel32.dll", 0), # yup
|
||
|
(MIN+"--file c:\\thisdoesntexist", 1), # nope
|
||
|
(MIN+"--mutdep1 a", 2), # no!
|
||
|
(MIN+"--mutdep2 b", 2), # also no!
|
||
|
(MIN+"--mutdep3 c", 2), # dream on!
|
||
|
(MIN+"--mutdep1 a --mutdep2 b", 2), # almost!
|
||
|
(MIN+"--mutdep1 a --mutdep2 b --mutdep3 c", 0), # yes
|
||
|
(MIN+"--mutex1 a", 0), # yes
|
||
|
(MIN+"--mutex2 b", 0), # yes
|
||
|
(MIN+"--mutex3 c", 0), # fine
|
||
|
(MIN+"--mutex1 a --mutex2 b", 1), # not fine
|
||
|
(MIN+"--mutex1 a --mutex2 b --mutex3 c", 3), # even worse
|
||
|
(MIN+"--dependent 1", 1), # no
|
||
|
(MIN+"--dependent 1 --int 2", 0), # ok
|
||
|
(MIN+"--int abc", 1), # bad type
|
||
|
(MIN+"--coords abc", 1), # also bad
|
||
|
(MIN+"--coords (abc)", 1), # getting warmer
|
||
|
(MIN+"--coords (abc,def)", 1), # missing something
|
||
|
(MIN+"--coords (123)", 1), # ooh, so close
|
||
|
(MIN+"--coords (123,def)", 1), # just a little farther
|
||
|
(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"])
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
sys.exit(main())
|