Enhanced command arguments parser/adapter

This commit is contained in:
red-agent 2009-09-15 22:57:01 +03:00
parent 8a72870a6d
commit 10a0867e59
1 changed files with 68 additions and 29 deletions

View File

@ -19,8 +19,9 @@ to implement commands in a streight and flexible, declarative way.
""" """
import re import re
from types import FunctionType, UnicodeType, TupleType, ListType from types import FunctionType, UnicodeType, TupleType, ListType, BooleanType
from inspect import getargspec from inspect import getargspec
from operator import itemgetter
class CommandInternalError(Exception): class CommandInternalError(Exception):
pass pass
@ -59,8 +60,13 @@ class Command(object):
try: try:
return self.handler(*args, **kwargs) return self.handler(*args, **kwargs)
except CommandError, exception: except CommandError, exception:
# Re-raise an excepttion with a proper command attribute set,
# unless it is already set by the one who raised an exception.
if not exception.command and not exception.name: if not exception.command and not exception.name:
raise CommandError(exception.message, self) raise CommandError(exception.message, self)
# This one is a little bit too wide, but as Python does not have
# anything more constrained - there is no other choice. Take a look here
# if command complains about invalid arguments while they are ok.
except TypeError: except TypeError:
raise CommandError("Command received invalid arguments", self) raise CommandError("Command received invalid arguments", self)
@ -136,13 +142,15 @@ class Command(object):
for key, value in spec_kwargs: for key, value in spec_kwargs:
letter = key[0] letter = key[0]
key = key.replace('_', '-') key = key.replace('_', '-')
value = ('=%s' % value) if not isinstance(value, BooleanType) else str()
if letter not in letters: if letter not in letters:
kwargs.append('-(-%s)%s=%s' % (letter, key[1:], value)) kwargs.append('-(-%s)%s%s' % (letter, key[1:], value))
letters.append(letter) letters.append(letter)
else: else:
kwargs.append('--%s=%s' % (key, value)) kwargs.append('--%s%s' % (key, value))
usage = str() usage = str()
args = str() args = str()
@ -371,9 +379,10 @@ class CommandProcessor(object):
def parse_command_arguments(cls, arguments): def parse_command_arguments(cls, arguments):
""" """
Simple yet effective and sufficient in most cases parser which parses Simple yet effective and sufficient in most cases parser which parses
command arguments and returns them as two lists, first representing command arguments and returns them as two lists. First represents
positional arguments, and second representing options as (key, value) positional arguments as (argument, position), and second representing
tuples. options as (key, value, position) tuples, where position is a (start,
end) span tuple of where it was found in the string.
The format of the input arguments should be: The format of the input arguments should be:
<arg1, arg2> <<extra>> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] <arg1, arg2> <<extra>> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]]
@ -386,27 +395,36 @@ class CommandProcessor(object):
""" """
args, opts = [], [] args, opts = [], []
# Need to store position of every option we have parsed in order to get def intersects_opts((given_start, given_end)):
# arguments to be parsed correct later. """
opt_positions = [] Check if something intersects with boundaries of any parsed option.
"""
for key, value, (start, end) in opts:
if given_start >= start and given_end <= end:
return True
return False
def intersects((given_start, given_end)): def intersects_args((given_start, given_end)):
""" """
Check if something intersects with boundaries of any parsed options. Check if something intersects with boundaries of any parsed argument.
""" """
for start, end in opt_positions: for arg, (start, end) in args:
if given_start == start or given_end == end: if given_start >= start and given_end <= end:
return True return True
return False return False
for match in re.finditer(cls.OPT_PATTERN, arguments): for match in re.finditer(cls.OPT_PATTERN, arguments):
if match: if match:
opt_positions.append(match.span()) key = match.group('key')
opts.append((match.group('key'), match.group('value') or True)) value = match.group('value') or None
position = match.span()
opts.append((key, value, position))
for match in re.finditer(cls.ARG_PATTERN, arguments): for match in re.finditer(cls.ARG_PATTERN, arguments):
if match and not intersects(match.span()): if match and not intersects_opts(match.span()):
args.append(match.group('body')) body = match.group('body')
position = match.span()
args.append((body, position))
return args, opts return args, opts
@ -423,17 +441,49 @@ class CommandProcessor(object):
Dashes (-) in the option names will be converted to underscores. So you Dashes (-) in the option names will be converted to underscores. So you
can map --one-more-option to a one_more_option=None. can map --one-more-option to a one_more_option=None.
If initial value of a keyword argument is a boolean (False in most
cases) then this option will be treated as a switch, that is an option
which does not take an argument. Argument preceded by a switch will be
treated just like a normal positional argument.
""" """
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec() spec_args, spec_kwargs, var_args, var_kwargs = command.extract_arg_spec()
spec_kwargs = dict(spec_kwargs) spec_kwargs = dict(spec_kwargs)
if command.raw: if command.raw:
if len(spec_args) == 1: if len(spec_args) == 1 and not spec_kwargs and not var_args and not var_kwargs:
if arguments or command.empty: if arguments or command.empty:
return (arguments,), {} return (arguments,), {}
raise CommandError("Can not be used without arguments", command) raise CommandError("Can not be used without arguments", command)
raise CommandInternalError("Raw command must define no more then one argument") raise CommandInternalError("Raw command must define no more then one argument")
if command.expand_short:
expanded = []
for spec_key, spec_value in spec_kwargs.iteritems():
letter = spec_key[0] if len(spec_key) > 1 else None
if letter and letter not in expanded:
for index, (key, value, position) in enumerate(opts):
if key == letter:
expanded.append(letter)
opts[index] = (spec_key, value, position)
break
for index, (key, value, position) in enumerate(opts):
if isinstance(spec_kwargs.get(key), BooleanType):
opts[index] = (key, True, position)
if value:
args.append((value, position))
# Sorting arguments and options (just to be sure) in regarding to their
# positions in the string.
args.sort(key=itemgetter(1))
opts.sort(key=itemgetter(2))
# Stripping down position information supplied with arguments and options as it
# won't be needed again.
args = map(lambda (arg, position): arg, args)
opts = map(lambda (key, value, position): (key, value), opts)
if command.extra: if command.extra:
if not var_args: if not var_args:
positional_len = len(spec_args) - (1 if not command.source else 2) positional_len = len(spec_args) - (1 if not command.source else 2)
@ -443,17 +493,6 @@ class CommandProcessor(object):
else: else:
raise CommandInternalError("Can not have both, extra and *args") raise CommandInternalError("Can not have both, extra and *args")
if command.expand_short:
expanded = []
for spec_key, spec_value in spec_kwargs.iteritems():
letter = spec_key[0] if len(spec_key) > 1 else None
if letter and letter not in expanded:
for index, (key, value) in enumerate(opts):
if key == letter:
expanded.append(letter)
opts[index] = (spec_key, value)
break
for index, (key, value) in enumerate(opts): for index, (key, value) in enumerate(opts):
if '-' in key: if '-' in key:
opts[index] = (key.replace('-', '_'), value) opts[index] = (key.replace('-', '_'), value)