348 lines
12 KiB
Python
348 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
click.parser
|
|
~~~~~~~~~~~~
|
|
|
|
This module started out as largely a copy paste from the stdlib's
|
|
optparse module with the features removed that we do not need from
|
|
optparse because we implement them in Click on a higher level (for
|
|
instance type handling, help formatting and a lot more).
|
|
|
|
The plan is to remove more and more from here over time.
|
|
|
|
The reason this is a different module and not optparse from the stdlib
|
|
is that there are differences in 2.x and 3.x about the error messages
|
|
generated and optparse in the stdlib uses gettext for no good reason
|
|
and might cause us issues.
|
|
"""
|
|
import re
|
|
from .exceptions import UsageError
|
|
from .utils import unpack_args
|
|
|
|
|
|
def split_opt(opt):
|
|
first = opt[:1]
|
|
if first.isalnum():
|
|
return '', opt
|
|
if opt[1:2] == first:
|
|
return opt[:2], opt[2:]
|
|
return first, opt[1:]
|
|
|
|
|
|
def normalize_opt(opt, ctx):
|
|
if ctx is None or ctx.token_normalize_func is None:
|
|
return opt
|
|
prefix, opt = split_opt(opt)
|
|
return prefix + ctx.token_normalize_func(opt)
|
|
|
|
|
|
def split_arg_string(string):
|
|
"""Given an argument string this attempts to split it into small parts."""
|
|
rv = []
|
|
for match in re.finditer(r"('([^'\\]*(?:\\.[^'\\]*)*)'"
|
|
r'|"([^"\\]*(?:\\.[^"\\]*)*)"'
|
|
r'|\S+)\s*', string, re.S):
|
|
arg = match.group().strip()
|
|
if arg[:1] == arg[-1:] and arg[:1] in '"\'':
|
|
arg = arg[1:-1].encode('ascii', 'backslashreplace') \
|
|
.decode('unicode-escape')
|
|
try:
|
|
arg = type(string)(arg)
|
|
except UnicodeError:
|
|
pass
|
|
rv.append(arg)
|
|
return rv
|
|
|
|
|
|
class Option(object):
|
|
|
|
def __init__(self, opts, dest, action=None, nargs=1, const=None, obj=None):
|
|
self._short_opts = []
|
|
self._long_opts = []
|
|
self.prefixes = set()
|
|
|
|
for opt in opts:
|
|
prefix, value = split_opt(opt)
|
|
if not prefix:
|
|
raise ValueError('Invalid start character for option (%s)'
|
|
% opt)
|
|
self.prefixes.add(prefix[0])
|
|
if len(prefix) == 1 and len(value) == 1:
|
|
self._short_opts.append(opt)
|
|
else:
|
|
self._long_opts.append(opt)
|
|
self.prefixes.add(prefix)
|
|
|
|
if action is None:
|
|
action = 'store'
|
|
|
|
self.dest = dest
|
|
self.action = action
|
|
self.nargs = nargs
|
|
self.const = const
|
|
self.obj = obj
|
|
|
|
@property
|
|
def takes_value(self):
|
|
return self.action in ('store', 'append')
|
|
|
|
def process(self, value, state):
|
|
if self.action == 'store':
|
|
state.opts[self.dest] = value
|
|
elif self.action == 'store_const':
|
|
state.opts[self.dest] = self.const
|
|
elif self.action == 'append':
|
|
state.opts.setdefault(self.dest, []).append(value)
|
|
elif self.action == 'append_const':
|
|
state.opts.setdefault(self.dest, []).append(self.const)
|
|
elif self.action == 'count':
|
|
state.opts[self.dest] = state.opts.get(self.dest, 0) + 1
|
|
else:
|
|
raise ValueError('unknown action %r' % self.action)
|
|
state.order.append(self.obj)
|
|
|
|
|
|
class Argument(object):
|
|
|
|
def __init__(self, dest, nargs=1, obj=None):
|
|
self.dest = dest
|
|
self.nargs = nargs
|
|
self.obj = obj
|
|
|
|
def process(self, value, state):
|
|
state.opts[self.dest] = value
|
|
state.order.append(self.obj)
|
|
|
|
|
|
class ParsingState(object):
|
|
|
|
def __init__(self, rargs):
|
|
self.opts = {}
|
|
self.largs = []
|
|
self.rargs = rargs
|
|
self.order = []
|
|
|
|
|
|
class OptionParser(object):
|
|
"""The option parser is an internal class that is ultimately used to
|
|
parse options and arguments. It's modelled after optparse and brings
|
|
a similar but vastly simplified API. It should generally not be used
|
|
directly as the high level Click classes wrap it for you.
|
|
|
|
It's not nearly as extensible as optparse or argparse as it does not
|
|
implement features that are implemented on a higher level (such as
|
|
types or defaults).
|
|
|
|
:param ctx: optionally the :class:`~click.Context` where this parser
|
|
should go with.
|
|
"""
|
|
|
|
def __init__(self, ctx=None):
|
|
#: The :class:`~click.Context` for this parser. This might be
|
|
#: `None` for some advanced use cases.
|
|
self.ctx = ctx
|
|
#: This controls how the parser deals with interspersed arguments.
|
|
#: If this is set to `False`, the parser will stop on the first
|
|
#: non-option. Click uses this to implement nested subcommands
|
|
#: safely.
|
|
self.allow_interspersed_args = True
|
|
self._short_opt = {}
|
|
self._long_opt = {}
|
|
self._opt_prefixes = set(['-', '--'])
|
|
self._args = []
|
|
|
|
def add_option(self, opts, dest, action=None, nargs=1, const=None,
|
|
obj=None):
|
|
"""Adds a new option named `dest` to the parser. The destination
|
|
is not inferred (unlike with optparse) and needs to be explicitly
|
|
provided. Action can be any of ``store``, ``store_const``,
|
|
``append``, ``appnd_const`` or ``count``.
|
|
|
|
The `obj` can be used to identify the option in the order list
|
|
that is returned from the parser.
|
|
"""
|
|
if obj is None:
|
|
obj = dest
|
|
opts = [normalize_opt(opt, self.ctx) for opt in opts]
|
|
option = Option(opts, dest, action=action, nargs=nargs,
|
|
const=const, obj=obj)
|
|
self._opt_prefixes.update(option.prefixes)
|
|
for opt in option._short_opts:
|
|
self._short_opt[opt] = option
|
|
for opt in option._long_opts:
|
|
self._long_opt[opt] = option
|
|
|
|
def add_argument(self, dest, nargs=1, obj=None):
|
|
"""Adds a positional argument named `dest` to the parser.
|
|
|
|
The `obj` can be used to identify the option in the order list
|
|
that is returned from the parser.
|
|
"""
|
|
if obj is None:
|
|
obj = dest
|
|
self._args.append(Argument(dest=dest, nargs=nargs, obj=obj))
|
|
|
|
def parse_args(self, args):
|
|
"""Parses positional arguments and returns ``(values, args, order)``
|
|
for the parsed options and arguments as well as the leftover
|
|
arguments if there are any. The order is a list of objects as they
|
|
appear on the command line. If arguments appear multiple times they
|
|
will be memorized multiple times as well.
|
|
"""
|
|
state = ParsingState(args)
|
|
try:
|
|
self._process_args_for_options(state)
|
|
self._process_args_for_args(state)
|
|
except UsageError:
|
|
if not self.ctx.resilient_parsing:
|
|
raise
|
|
return state.opts, state.largs, state.order
|
|
|
|
def _process_args_for_args(self, state):
|
|
pargs, args = unpack_args(state.largs + state.rargs,
|
|
[x.nargs for x in self._args])
|
|
|
|
for idx, arg in enumerate(self._args):
|
|
arg.process(pargs[idx], state)
|
|
|
|
state.largs = args
|
|
state.rargs = []
|
|
|
|
def _process_args_for_options(self, state):
|
|
while state.rargs:
|
|
arg = state.rargs.pop(0)
|
|
arglen = len(arg)
|
|
# Double dashes always handled explicitly regardless of what
|
|
# prefixes are valid.
|
|
if arg == '--':
|
|
return
|
|
elif arg[:1] in self._opt_prefixes and arglen > 1:
|
|
self._process_opts(arg, state)
|
|
elif self.allow_interspersed_args:
|
|
state.largs.append(arg)
|
|
else:
|
|
state.rargs.insert(0, arg)
|
|
return
|
|
|
|
# Say this is the original argument list:
|
|
# [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
|
|
# ^
|
|
# (we are about to process arg(i)).
|
|
#
|
|
# Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of
|
|
# [arg0, ..., arg(i-1)] (any options and their arguments will have
|
|
# been removed from largs).
|
|
#
|
|
# The while loop will usually consume 1 or more arguments per pass.
|
|
# If it consumes 1 (eg. arg is an option that takes no arguments),
|
|
# then after _process_arg() is done the situation is:
|
|
#
|
|
# largs = subset of [arg0, ..., arg(i)]
|
|
# rargs = [arg(i+1), ..., arg(N-1)]
|
|
#
|
|
# If allow_interspersed_args is false, largs will always be
|
|
# *empty* -- still a subset of [arg0, ..., arg(i-1)], but
|
|
# not a very interesting subset!
|
|
|
|
def _match_long_opt(self, opt):
|
|
# Is there an exact match?
|
|
if opt in self._long_opt:
|
|
return opt
|
|
|
|
# Isolate all words with s as a prefix.
|
|
possibilities = [word for word in self._long_opt
|
|
if word.startswith(opt)]
|
|
|
|
# No exact match, so there had better be just one possibility.
|
|
if not possibilities:
|
|
self._error('no such option: %s' % opt)
|
|
elif len(possibilities) == 1:
|
|
self._error('no such option: %s. Did you mean %s?' %
|
|
(opt, possibilities[0]))
|
|
return possibilities[0]
|
|
else:
|
|
# More than one possible completion: ambiguous prefix.
|
|
possibilities.sort()
|
|
self._error('no such option: %s. (Possible options: %s)'
|
|
% (opt, ', '.join(possibilities)))
|
|
|
|
def _process_long_opt(self, arg, state):
|
|
# Value explicitly attached to arg? Pretend it's the next argument.
|
|
if '=' in arg:
|
|
opt, next_arg = arg.split('=', 1)
|
|
state.rargs.insert(0, next_arg)
|
|
had_explicit_value = True
|
|
else:
|
|
opt = arg
|
|
had_explicit_value = False
|
|
|
|
opt = normalize_opt(opt, self.ctx)
|
|
|
|
opt = self._match_long_opt(opt)
|
|
option = self._long_opt[opt]
|
|
if option.takes_value:
|
|
nargs = option.nargs
|
|
if len(state.rargs) < nargs:
|
|
if nargs == 1:
|
|
self._error('%s option requires an argument' % opt)
|
|
else:
|
|
self._error('%s option requires %d arguments' % (opt, nargs))
|
|
elif nargs == 1:
|
|
value = state.rargs.pop(0)
|
|
else:
|
|
value = tuple(state.rargs[:nargs])
|
|
del state.rargs[:nargs]
|
|
|
|
elif had_explicit_value:
|
|
self._error('%s option does not take a value' % opt)
|
|
|
|
else:
|
|
value = None
|
|
|
|
option.process(value, state)
|
|
|
|
def _process_opts(self, arg, state):
|
|
if '=' in arg or normalize_opt(arg, self.ctx) in self._long_opt:
|
|
return self._process_long_opt(arg, state)
|
|
|
|
stop = False
|
|
i = 1
|
|
prefix = arg[0]
|
|
for ch in arg[1:]:
|
|
opt = normalize_opt(prefix + ch, self.ctx)
|
|
option = self._short_opt.get(opt)
|
|
i += 1
|
|
|
|
if not option:
|
|
self._error('no such option: %s' % (arg if arg.startswith('--') else opt))
|
|
if option.takes_value:
|
|
# Any characters left in arg? Pretend they're the
|
|
# next arg, and stop consuming characters of arg.
|
|
if i < len(arg):
|
|
state.rargs.insert(0, arg[i:])
|
|
stop = True
|
|
|
|
nargs = option.nargs
|
|
if len(state.rargs) < nargs:
|
|
if nargs == 1:
|
|
self._error('%s option requires an argument' % opt)
|
|
else:
|
|
self._error('%s option requires %d arguments' %
|
|
(opt, nargs))
|
|
elif nargs == 1:
|
|
value = state.rargs.pop(0)
|
|
else:
|
|
value = tuple(state.rargs[:nargs])
|
|
del state.rargs[:nargs]
|
|
|
|
else:
|
|
value = None
|
|
|
|
option.process(value, state)
|
|
|
|
if stop:
|
|
break
|
|
|
|
def _error(self, msg):
|
|
raise UsageError(msg, self.ctx)
|