361 lines
15 KiB
Python
361 lines
15 KiB
Python
# Copyright (C) 2009 red-agent <hell.director@gmail.com>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
The module contains routines to parse command arguments and map them to the
|
|
command handler's positonal and keyword arguments.
|
|
|
|
Mapping is done in two stages: 1) parse arguments into positional arguments and
|
|
options; 2) adapt them to the specific command handler according to the command
|
|
properties.
|
|
"""
|
|
|
|
import re
|
|
from types import BooleanType, UnicodeType
|
|
from types import TupleType, ListType
|
|
from operator import itemgetter
|
|
|
|
from errors import DefinitionError, CommandError
|
|
|
|
# Quite complex piece of regular expression logic to parse options and
|
|
# arguments. Might need some tweaking along the way.
|
|
ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
|
|
OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?')
|
|
|
|
# Option keys needs to be encoded to a specific encoding as Python does not
|
|
# allow to expand dictionary with raw unicode strings as keys from a **kwargs.
|
|
KEY_ENCODING = 'UTF-8'
|
|
|
|
# Defines how complete representation of command usage (generated based on
|
|
# command handler argument specification) will be rendered.
|
|
USAGE_PATTERN = 'Usage: %s %s'
|
|
|
|
def parse_arguments(arguments):
|
|
"""
|
|
Simple yet effective and sufficient in most cases parser which parses
|
|
command arguments and returns them as two lists.
|
|
|
|
First list 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.
|
|
|
|
Options may be given in --long or -short format. As --option=value or
|
|
--option value or -option value. Keys without values will get None as value.
|
|
|
|
Arguments and option values that contain spaces may be given as 'one two
|
|
three' or "one two three"; that is between single or double quotes.
|
|
"""
|
|
args, opts = [], []
|
|
|
|
def intersects_opts((given_start, given_end)):
|
|
"""
|
|
Check if given span intersects with any of options.
|
|
"""
|
|
for key, value, (start, end) in opts:
|
|
if given_start >= start and given_end <= end:
|
|
return True
|
|
return False
|
|
|
|
def intersects_args((given_start, given_end)):
|
|
"""
|
|
Check if given span intersects with any of arguments.
|
|
"""
|
|
for arg, (start, end) in args:
|
|
if given_start >= start and given_end <= end:
|
|
return True
|
|
return False
|
|
|
|
for match in re.finditer(OPT_PATTERN, arguments):
|
|
if match:
|
|
key = match.group('key')
|
|
value = match.group('value') or None
|
|
position = match.span()
|
|
opts.append((key, value, position))
|
|
|
|
for match in re.finditer(ARG_PATTERN, arguments):
|
|
if match:
|
|
body = match.group('body')
|
|
position = match.span()
|
|
args.append((body, position))
|
|
|
|
# Primitive but sufficiently effective way of disposing of conflicted
|
|
# sectors. Remove any arguments that intersect with options.
|
|
for arg, position in args[:]:
|
|
if intersects_opts(position):
|
|
args.remove((arg, position))
|
|
|
|
# Primitive but sufficiently effective way of disposing of conflicted
|
|
# sectors. Remove any options that intersect with arguments.
|
|
for key, value, position in opts[:]:
|
|
if intersects_args(position):
|
|
opts.remove((key, value, position))
|
|
|
|
return args, opts
|
|
|
|
def adapt_arguments(command, arguments, args, opts):
|
|
"""
|
|
Adapt args and opts got from the parser to a specific handler by means of
|
|
arguments specified on command definition. That is transform them to *args
|
|
and **kwargs suitable for passing to a command handler.
|
|
|
|
Dashes (-) in the option names will be converted to underscores. So you can
|
|
map --one-more-option to a one_more_option=None.
|
|
|
|
If the 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. If a switch is followed by an argument -
|
|
then this argument will be treated just like a normal positional argument.
|
|
|
|
If the initial value of a keyword argument is a sequence, that is a tuple or
|
|
list - then a value of this option will be considered correct only if it is
|
|
present in the sequence.
|
|
"""
|
|
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
|
|
norm_kwargs = dict(spec_kwargs)
|
|
|
|
# Quite complex piece of neck-breaking logic to extract raw arguments if
|
|
# there is more, then one positional argument specified by the command. In
|
|
# case if it's just one argument which is the collector - this is fairly
|
|
# easy. But when it's more then one argument - the neck-breaking logic of
|
|
# how to retrieve residual arguments as a raw, all in one piece string,
|
|
# kicks in.
|
|
if command.raw:
|
|
if arguments:
|
|
spec_fix = 1 if command.source else 0
|
|
spec_len = len(spec_args) - spec_fix
|
|
arguments_end = len(arguments) - 1
|
|
|
|
# If there are any optional arguments given they should be either an
|
|
# unquoted postional argument or part of the raw argument. So we
|
|
# find all optional arguments that can possibly be unquoted argument
|
|
# and append them as is to the args.
|
|
for key, value, (start, end) in opts[:spec_len]:
|
|
if value:
|
|
end -= len(value) + 1
|
|
args.append((arguments[start:end], (start, end)))
|
|
args.append((value, (end, end + len(value) + 1)))
|
|
else:
|
|
args.append((arguments[start:end], (start, end)))
|
|
|
|
# We need in-place sort here because after manipulations with
|
|
# options order of arguments might be wrong and we just can't have
|
|
# more complex logic to not let that happen.
|
|
args.sort(key=itemgetter(1))
|
|
|
|
if spec_len > 1:
|
|
try:
|
|
stopper, (start, end) = args[spec_len - 2]
|
|
except IndexError:
|
|
raise CommandError("Missing arguments", command)
|
|
|
|
# The essential point of the whole play. After boundaries are
|
|
# being determined (supposingly correct) we separate raw part
|
|
# from the rest of arguments, which should be normally
|
|
# processed.
|
|
raw = arguments[end:]
|
|
raw = raw.strip() or None
|
|
|
|
if not raw and not command.empty:
|
|
raise CommandError("Missing arguments", command)
|
|
|
|
# Discard residual arguments and all of the options as raw
|
|
# command does not support options and if an option is given it
|
|
# is rather a part of a raw argument.
|
|
args = args[:spec_len - 1]
|
|
opts = []
|
|
|
|
args.append((raw, (end, arguments_end)))
|
|
else:
|
|
# Substitue all of the arguments with only one, which contain
|
|
# raw and unprocessed arguments as a string. And discard all the
|
|
# options, as raw command does not support them.
|
|
args = [(arguments, (0, arguments_end))]
|
|
opts = []
|
|
else:
|
|
if command.empty:
|
|
args.append((None, (0, 0)))
|
|
else:
|
|
raise CommandError("Missing arguments", command)
|
|
|
|
# The first stage of transforming options we have got to a format that can
|
|
# be used to associate them with declared keyword arguments. Substituting
|
|
# dashes (-) in their names with underscores (_).
|
|
for index, (key, value, position) in enumerate(opts):
|
|
if '-' in key:
|
|
opts[index] = (key.replace('-', '_'), value, position)
|
|
|
|
# The second stage of transforming options to an associatable state.
|
|
# Expanding short, one-letter options to a verbose ones, if corresponding
|
|
# optin has been given.
|
|
if command.expand_short:
|
|
expanded = []
|
|
for spec_key, spec_value in norm_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
|
|
|
|
# Detect switches and set their values accordingly. If any of them carries a
|
|
# value - append it to args.
|
|
for index, (key, value, position) in enumerate(opts):
|
|
if isinstance(norm_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 has extra option enabled - collect all extra arguments and pass
|
|
# them to a last positional argument command defines as a list.
|
|
if command.extra:
|
|
if not var_args:
|
|
spec_fix = 1 if not command.source else 2
|
|
spec_len = len(spec_args) - spec_fix
|
|
extra = args[spec_len:]
|
|
args = args[:spec_len]
|
|
args.append(extra)
|
|
else:
|
|
raise DefinitionError("Can not have both, extra and *args")
|
|
|
|
# Detect if positional arguments overlap keyword arguments. If so and this
|
|
# is allowed by command options - then map them directly to their options,
|
|
# so they can get propert further processings.
|
|
spec_fix = 1 if command.source else 0
|
|
spec_len = len(spec_args) - spec_fix
|
|
if len(args) > spec_len:
|
|
if command.overlap:
|
|
overlapped = args[spec_len:]
|
|
args = args[:spec_len]
|
|
for arg, (spec_key, spec_value) in zip(overlapped, spec_kwargs):
|
|
opts.append((spec_key, arg))
|
|
else:
|
|
raise CommandError("Excessive arguments", command)
|
|
|
|
# Detect every switch and ensure it will not receive any arguments.
|
|
# Normally this does not happen unless overlapping is enabled.
|
|
for key, value in opts:
|
|
initial = norm_kwargs.get(key)
|
|
if isinstance(initial, BooleanType):
|
|
if not isinstance(value, BooleanType):
|
|
raise CommandError("%s: Switch can not take an argument" % key, command)
|
|
|
|
# Detect every sequence constraint and ensure that if corresponding options
|
|
# are given - they contain proper values, within the constraint range.
|
|
for key, value in opts:
|
|
initial = norm_kwargs.get(key)
|
|
if isinstance(initial, (TupleType, ListType)):
|
|
if value not in initial:
|
|
raise CommandError("%s: Invalid argument" % key, command)
|
|
|
|
# If argument to an option constrained by a sequence was not given - then
|
|
# it's value should be set to None.
|
|
for spec_key, spec_value in spec_kwargs:
|
|
if isinstance(spec_value, (TupleType, ListType)):
|
|
for key, value in opts:
|
|
if spec_key == key:
|
|
break
|
|
else:
|
|
opts.append((spec_key, None))
|
|
|
|
# We need to encode every keyword argument to a simple string, not the
|
|
# unicode one, because ** expansion does not support it.
|
|
for index, (key, value) in enumerate(opts):
|
|
if isinstance(key, UnicodeType):
|
|
opts[index] = (key.encode(KEY_ENCODING), value)
|
|
|
|
# Inject the source arguments as a string as a first argument, if command
|
|
# has enabled the corresponding option.
|
|
if command.source:
|
|
args.insert(0, arguments)
|
|
|
|
# Return *args and **kwargs in the form suitable for passing to a command
|
|
# handler and being expanded.
|
|
return tuple(args), dict(opts)
|
|
|
|
def generate_usage(command, complete=True):
|
|
"""
|
|
Extract handler's arguments specification and wrap them in a human-readable
|
|
format usage information. If complete is given - then USAGE_PATTERN will be
|
|
used to render the specification completly.
|
|
"""
|
|
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
|
|
|
|
# Remove some special positional arguments from the specifiaction, but store
|
|
# their names so they can be used for usage info generation.
|
|
sp_source = spec_args.pop(0) if command.source else None
|
|
sp_extra = spec_args.pop() if command.extra else None
|
|
|
|
kwargs = []
|
|
letters = []
|
|
|
|
for key, value in spec_kwargs:
|
|
letter = key[0]
|
|
key = key.replace('_', '-')
|
|
|
|
if isinstance(value, BooleanType):
|
|
value = str()
|
|
elif isinstance(value, (TupleType, ListType)):
|
|
value = '={%s}' % ', '.join(value)
|
|
else:
|
|
value = '=%s' % value
|
|
|
|
if letter not in letters:
|
|
kwargs.append('-(-%s)%s%s' % (letter, key[1:], value))
|
|
letters.append(letter)
|
|
else:
|
|
kwargs.append('--%s%s' % (key, value))
|
|
|
|
usage = str()
|
|
args = str()
|
|
|
|
if command.raw:
|
|
spec_len = len(spec_args) - 1
|
|
if spec_len:
|
|
args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' '
|
|
args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1]
|
|
else:
|
|
if spec_args:
|
|
args += '<%s>' % ', '.join(spec_args)
|
|
if var_args or sp_extra:
|
|
args += (' ' if spec_args else str()) + '<<%s>>' % (var_args or sp_extra)
|
|
|
|
usage += args
|
|
|
|
if kwargs or var_kwargs:
|
|
if kwargs:
|
|
usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs)
|
|
if var_kwargs:
|
|
usage += (' ' if args else str()) + '[[%s]]' % var_kwargs
|
|
|
|
# Native name will be the first one if it is included. Otherwise, names will
|
|
# be in the order they were specified.
|
|
if len(command.names) > 1:
|
|
names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:]))
|
|
else:
|
|
names = command.first_name
|
|
|
|
return USAGE_PATTERN % (names, usage) if complete else usage
|