From 10a0867e596a68e90492d28167c0cbb4f296e91d Mon Sep 17 00:00:00 2001 From: red-agent Date: Tue, 15 Sep 2009 22:57:01 +0300 Subject: [PATCH] Enhanced command arguments parser/adapter --- src/commands/framework.py | 97 +++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 29 deletions(-) diff --git a/src/commands/framework.py b/src/commands/framework.py index 866d8ff23..15daa250f 100644 --- a/src/commands/framework.py +++ b/src/commands/framework.py @@ -19,8 +19,9 @@ to implement commands in a streight and flexible, declarative way. """ import re -from types import FunctionType, UnicodeType, TupleType, ListType +from types import FunctionType, UnicodeType, TupleType, ListType, BooleanType from inspect import getargspec +from operator import itemgetter class CommandInternalError(Exception): pass @@ -59,8 +60,13 @@ class Command(object): try: return self.handler(*args, **kwargs) 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: 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: raise CommandError("Command received invalid arguments", self) @@ -136,13 +142,15 @@ class Command(object): for key, value in spec_kwargs: letter = key[0] + key = key.replace('_', '-') + value = ('=%s' % value) if not isinstance(value, BooleanType) else str() 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) else: - kwargs.append('--%s=%s' % (key, value)) + kwargs.append('--%s%s' % (key, value)) usage = str() args = str() @@ -371,9 +379,10 @@ class CommandProcessor(object): def parse_command_arguments(cls, arguments): """ Simple yet effective and sufficient in most cases parser which parses - command arguments and returns them as two lists, first representing - positional arguments, and second representing options as (key, value) - tuples. + command arguments and returns them as two lists. First represents + positional arguments as (argument, position), and second representing + 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: <> [-(-o)ption=value1, -(-a)nother=value2] [[extra_options]] @@ -386,27 +395,36 @@ class CommandProcessor(object): """ args, opts = [], [] - # Need to store position of every option we have parsed in order to get - # arguments to be parsed correct later. - opt_positions = [] + def intersects_opts((given_start, given_end)): + """ + 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: - if given_start == start or given_end == end: + for arg, (start, end) in args: + if given_start >= start and given_end <= end: return True return False for match in re.finditer(cls.OPT_PATTERN, arguments): if match: - opt_positions.append(match.span()) - opts.append((match.group('key'), match.group('value') or True)) + key = match.group('key') + value = match.group('value') or None + position = match.span() + opts.append((key, value, position)) for match in re.finditer(cls.ARG_PATTERN, arguments): - if match and not intersects(match.span()): - args.append(match.group('body')) + if match and not intersects_opts(match.span()): + body = match.group('body') + position = match.span() + args.append((body, position)) return args, opts @@ -423,17 +441,49 @@ class CommandProcessor(object): Dashes (-) in the option names will be converted to underscores. So you 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_kwargs = dict(spec_kwargs) 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: return (arguments,), {} raise CommandError("Can not be used without arguments", command) 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 not var_args: positional_len = len(spec_args) - (1 if not command.source else 2) @@ -443,17 +493,6 @@ class CommandProcessor(object): else: 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): if '-' in key: opts[index] = (key.replace('-', '_'), value)