Enhanced command arguments parser/adapter
This commit is contained in:
parent
8a72870a6d
commit
10a0867e59
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue