fancyopts.py
391 lines
| 11.3 KiB
| text/x-python
|
PythonLexer
/ mercurial / fancyopts.py
Martin Geisler
|
r8230 | # fancyopts.py - better command line parsing | ||
# | ||||
Raphaël Gomès
|
r47575 | # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others | ||
Martin Geisler
|
r8230 | # | ||
# This software may be used and distributed according to the terms of the | ||||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Martin Geisler
|
r8230 | |||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Gregory Szorc
|
r25947 | |||
Daniel Ploch
|
r36373 | import abc | ||
Yuya Nishihara
|
r35179 | import functools | ||
Gregory Szorc
|
r25947 | from .i18n import _ | ||
Pulkit Goyal
|
r30578 | from . import ( | ||
error, | ||||
pycompat, | ||||
) | ||||
mpm@selenic.com
|
r0 | |||
Augie Fackler
|
r29947 | # Set of flags to not apply boolean negation logic on | ||
Martin von Zweigbergk
|
r32291 | nevernegate = { | ||
Augie Fackler
|
r29947 | # avoid --no-noninteractive | ||
Augie Fackler
|
r43347 | b'noninteractive', | ||
Augie Fackler
|
r29947 | # These two flags are special because they cause hg to do one | ||
# thing and then exit, and so aren't suitable for use in things | ||||
# like aliases anyway. | ||||
Augie Fackler
|
r43347 | b'help', | ||
b'version', | ||||
Martin von Zweigbergk
|
r32291 | } | ||
Augie Fackler
|
r29947 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35179 | def _earlyoptarg(arg, shortlist, namelist): | ||
"""Check if the given arg is a valid unabbreviated option | ||||
Returns (flag_str, has_embedded_value?, embedded_value, takes_value?) | ||||
>>> def opt(arg): | ||||
... return _earlyoptarg(arg, b'R:q', [b'cwd=', b'debugger']) | ||||
long form: | ||||
>>> opt(b'--cwd') | ||||
('--cwd', False, '', True) | ||||
>>> opt(b'--cwd=') | ||||
('--cwd', True, '', True) | ||||
>>> opt(b'--cwd=foo') | ||||
('--cwd', True, 'foo', True) | ||||
>>> opt(b'--debugger') | ||||
('--debugger', False, '', False) | ||||
>>> opt(b'--debugger=') # invalid but parsable | ||||
('--debugger', True, '', False) | ||||
short form: | ||||
>>> opt(b'-R') | ||||
('-R', False, '', True) | ||||
>>> opt(b'-Rfoo') | ||||
('-R', True, 'foo', True) | ||||
>>> opt(b'-q') | ||||
('-q', False, '', False) | ||||
>>> opt(b'-qfoo') # invalid but parsable | ||||
('-q', True, 'foo', False) | ||||
unknown or invalid: | ||||
>>> opt(b'--unknown') | ||||
('', False, '', False) | ||||
>>> opt(b'-u') | ||||
('', False, '', False) | ||||
>>> opt(b'-ufoo') | ||||
('', False, '', False) | ||||
>>> opt(b'--') | ||||
('', False, '', False) | ||||
>>> opt(b'-') | ||||
('', False, '', False) | ||||
>>> opt(b'-:') | ||||
('', False, '', False) | ||||
>>> opt(b'-:foo') | ||||
('', False, '', False) | ||||
""" | ||||
Augie Fackler
|
r43347 | if arg.startswith(b'--'): | ||
flag, eq, val = arg.partition(b'=') | ||||
Yuya Nishihara
|
r35179 | if flag[2:] in namelist: | ||
return flag, bool(eq), val, False | ||||
Augie Fackler
|
r43347 | if flag[2:] + b'=' in namelist: | ||
Yuya Nishihara
|
r35179 | return flag, bool(eq), val, True | ||
Augie Fackler
|
r43347 | elif arg.startswith(b'-') and arg != b'-' and not arg.startswith(b'-:'): | ||
Yuya Nishihara
|
r35179 | flag, val = arg[:2], arg[2:] | ||
i = shortlist.find(flag[1:]) | ||||
if i >= 0: | ||||
Augie Fackler
|
r43347 | return flag, bool(val), val, shortlist.startswith(b':', i + 1) | ||
return b'', False, b'', False | ||||
Yuya Nishihara
|
r35179 | |||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35179 | def earlygetopt(args, shortlist, namelist, gnu=False, keepsep=False): | ||
"""Parse options like getopt, but ignores unknown options and abbreviated | ||||
forms | ||||
If gnu=False, this stops processing options as soon as a non/unknown-option | ||||
argument is encountered. Otherwise, option and non-option arguments may be | ||||
intermixed, and unknown-option arguments are taken as non-option. | ||||
If keepsep=True, '--' won't be removed from the list of arguments left. | ||||
This is useful for stripping early options from a full command arguments. | ||||
>>> def get(args, gnu=False, keepsep=False): | ||||
... return earlygetopt(args, b'R:q', [b'cwd=', b'debugger'], | ||||
... gnu=gnu, keepsep=keepsep) | ||||
default parsing rules for early options: | ||||
>>> get([b'x', b'--cwd', b'foo', b'-Rbar', b'-q', b'y'], gnu=True) | ||||
([('--cwd', 'foo'), ('-R', 'bar'), ('-q', '')], ['x', 'y']) | ||||
>>> get([b'x', b'--cwd=foo', b'y', b'-R', b'bar', b'--debugger'], gnu=True) | ||||
([('--cwd', 'foo'), ('-R', 'bar'), ('--debugger', '')], ['x', 'y']) | ||||
>>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=True) | ||||
([('--cwd', 'foo')], ['--unknown', '--debugger']) | ||||
restricted parsing rules (early options must come first): | ||||
>>> get([b'--cwd', b'foo', b'-Rbar', b'x', b'-q', b'y'], gnu=False) | ||||
([('--cwd', 'foo'), ('-R', 'bar')], ['x', '-q', 'y']) | ||||
>>> get([b'--cwd=foo', b'x', b'y', b'-R', b'bar', b'--debugger'], gnu=False) | ||||
([('--cwd', 'foo')], ['x', 'y', '-R', 'bar', '--debugger']) | ||||
>>> get([b'--unknown', b'--cwd=foo', b'--', '--debugger'], gnu=False) | ||||
Yuya Nishihara
|
r35227 | ([], ['--unknown', '--cwd=foo', '--', '--debugger']) | ||
Yuya Nishihara
|
r35179 | |||
stripping early options (without loosing '--'): | ||||
>>> get([b'x', b'-Rbar', b'--', '--debugger'], gnu=True, keepsep=True)[1] | ||||
['x', '--', '--debugger'] | ||||
last argument: | ||||
>>> get([b'--cwd']) | ||||
([], ['--cwd']) | ||||
>>> get([b'--cwd=foo']) | ||||
([('--cwd', 'foo')], []) | ||||
>>> get([b'-R']) | ||||
([], ['-R']) | ||||
>>> get([b'-Rbar']) | ||||
([('-R', 'bar')], []) | ||||
>>> get([b'-q']) | ||||
([('-q', '')], []) | ||||
>>> get([b'-q', b'--']) | ||||
([('-q', '')], []) | ||||
Yuya Nishihara
|
r35227 | '--' may be a value: | ||
>>> get([b'-R', b'--', b'x']) | ||||
([('-R', '--')], ['x']) | ||||
>>> get([b'--cwd', b'--', b'x']) | ||||
([('--cwd', '--')], ['x']) | ||||
Yuya Nishihara
|
r35179 | value passed to bool options: | ||
>>> get([b'--debugger=foo', b'x']) | ||||
([], ['--debugger=foo', 'x']) | ||||
>>> get([b'-qfoo', b'x']) | ||||
([], ['-qfoo', 'x']) | ||||
short option isn't separated with '=': | ||||
>>> get([b'-R=bar']) | ||||
([('-R', '=bar')], []) | ||||
':' may be in shortlist, but shouldn't be taken as an option letter: | ||||
>>> get([b'-:', b'y']) | ||||
([], ['-:', 'y']) | ||||
'-' is a valid non-option argument: | ||||
>>> get([b'-', b'y']) | ||||
([], ['-', 'y']) | ||||
""" | ||||
parsedopts = [] | ||||
parsedargs = [] | ||||
pos = 0 | ||||
Yuya Nishihara
|
r35227 | while pos < len(args): | ||
Yuya Nishihara
|
r35179 | arg = args[pos] | ||
Augie Fackler
|
r43347 | if arg == b'--': | ||
Yuya Nishihara
|
r35227 | pos += not keepsep | ||
break | ||||
Yuya Nishihara
|
r35179 | flag, hasval, val, takeval = _earlyoptarg(arg, shortlist, namelist) | ||
Yuya Nishihara
|
r35227 | if not hasval and takeval and pos + 1 >= len(args): | ||
Yuya Nishihara
|
r35179 | # missing last argument | ||
break | ||||
if not flag or hasval and not takeval: | ||||
# non-option argument or -b/--bool=INVALID_VALUE | ||||
if gnu: | ||||
parsedargs.append(arg) | ||||
pos += 1 | ||||
else: | ||||
break | ||||
elif hasval == takeval: | ||||
# -b/--bool or -s/--str=VALUE | ||||
parsedopts.append((flag, val)) | ||||
pos += 1 | ||||
else: | ||||
# -s/--str VALUE | ||||
parsedopts.append((flag, args[pos + 1])) | ||||
pos += 2 | ||||
Yuya Nishihara
|
r35227 | parsedargs.extend(args[pos:]) | ||
Yuya Nishihara
|
r35179 | return parsedopts, parsedargs | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r49801 | class customopt: # pytype: disable=ignored-metaclass | ||
Daniel Ploch
|
r36373 | """Manage defaults and mutations for any type of opt.""" | ||
__metaclass__ = abc.ABCMeta | ||||
def __init__(self, defaultvalue): | ||||
Daniel Ploch
|
r37110 | self._defaultvalue = defaultvalue | ||
Daniel Ploch
|
r36373 | |||
def _isboolopt(self): | ||||
return False | ||||
Daniel Ploch
|
r37110 | def getdefaultvalue(self): | ||
"""Returns the default value for this opt. | ||||
Subclasses should override this to return a new value if the value type | ||||
is mutable.""" | ||||
return self._defaultvalue | ||||
Daniel Ploch
|
r36373 | @abc.abstractmethod | ||
def newstate(self, oldstate, newparam, abort): | ||||
"""Adds newparam to oldstate and returns the new state. | ||||
On failure, abort can be called with a string error message.""" | ||||
Augie Fackler
|
r43346 | |||
Daniel Ploch
|
r36373 | class _simpleopt(customopt): | ||
def _isboolopt(self): | ||||
Daniel Ploch
|
r37110 | return isinstance(self._defaultvalue, (bool, type(None))) | ||
Daniel Ploch
|
r36373 | |||
def newstate(self, oldstate, newparam, abort): | ||||
return newparam | ||||
Augie Fackler
|
r43346 | |||
Daniel Ploch
|
r36373 | class _callableopt(customopt): | ||
def __init__(self, callablefn): | ||||
self.callablefn = callablefn | ||||
super(_callableopt, self).__init__(None) | ||||
def newstate(self, oldstate, newparam, abort): | ||||
return self.callablefn(newparam) | ||||
Augie Fackler
|
r43346 | |||
Daniel Ploch
|
r36373 | class _listopt(customopt): | ||
Daniel Ploch
|
r37110 | def getdefaultvalue(self): | ||
return self._defaultvalue[:] | ||||
Daniel Ploch
|
r36373 | def newstate(self, oldstate, newparam, abort): | ||
oldstate.append(newparam) | ||||
return oldstate | ||||
Augie Fackler
|
r43346 | |||
Daniel Ploch
|
r36373 | class _intopt(customopt): | ||
def newstate(self, oldstate, newparam, abort): | ||||
try: | ||||
return int(newparam) | ||||
except ValueError: | ||||
Augie Fackler
|
r43347 | abort(_(b'expected int')) | ||
Daniel Ploch
|
r36373 | |||
Augie Fackler
|
r43346 | |||
Daniel Ploch
|
r36373 | def _defaultopt(default): | ||
"""Returns a default opt implementation, given a default value.""" | ||||
if isinstance(default, customopt): | ||||
return default | ||||
elif callable(default): | ||||
return _callableopt(default) | ||||
elif isinstance(default, list): | ||||
return _listopt(default[:]) | ||||
elif type(default) is type(1): | ||||
return _intopt(default) | ||||
else: | ||||
return _simpleopt(default) | ||||
Augie Fackler
|
r43346 | |||
Yuya Nishihara
|
r35223 | def fancyopts(args, options, state, gnu=False, early=False, optaliases=None): | ||
Matt Mackall
|
r5638 | """ | ||
read args, parse options, and store options in state | ||||
each option is a tuple of: | ||||
short option or '' | ||||
long option | ||||
default value | ||||
description | ||||
FUJIWARA Katsunori
|
r11321 | option value label(optional) | ||
Matt Mackall
|
r5638 | |||
option types include: | ||||
boolean or none - option sets variable in state to true | ||||
string - parameter string is stored in state | ||||
list - parameter string is added to a list | ||||
integer - parameter strings is stored as int | ||||
function - call function with parameter | ||||
Daniel Ploch
|
r36373 | customopt - subclass of 'customopt' | ||
mpm@selenic.com
|
r0 | |||
Yuya Nishihara
|
r35223 | optaliases is a mapping from a canonical option name to a list of | ||
additional long options. This exists for preserving backward compatibility | ||||
of early options. If we want to use it extensively, please consider moving | ||||
the functionality to the options table (e.g separate long options by '|'.) | ||||
Matt Mackall
|
r5638 | non-option args are returned | ||
""" | ||||
Yuya Nishihara
|
r35223 | if optaliases is None: | ||
optaliases = {} | ||||
Matt Mackall
|
r5638 | namelist = [] | ||
Augie Fackler
|
r43347 | shortlist = b'' | ||
Matt Mackall
|
r5638 | argmap = {} | ||
defmap = {} | ||||
Augie Fackler
|
r29947 | negations = {} | ||
Augie Fackler
|
r44937 | alllong = {o[1] for o in options} | ||
Matt Mackall
|
r5638 | |||
FUJIWARA Katsunori
|
r11321 | for option in options: | ||
if len(option) == 5: | ||||
short, name, default, comment, dummy = option | ||||
else: | ||||
short, name, default, comment = option | ||||
Matt Mackall
|
r5638 | # convert opts to getopt format | ||
Yuya Nishihara
|
r35223 | onames = [name] | ||
onames.extend(optaliases.get(name, [])) | ||||
Augie Fackler
|
r43347 | name = name.replace(b'-', b'_') | ||
Matt Mackall
|
r5638 | |||
Augie Fackler
|
r43347 | argmap[b'-' + short] = name | ||
Yuya Nishihara
|
r35223 | for n in onames: | ||
Augie Fackler
|
r43347 | argmap[b'--' + n] = name | ||
Daniel Ploch
|
r36373 | defmap[name] = _defaultopt(default) | ||
Matt Mackall
|
r5638 | |||
# copy defaults to state | ||||
Daniel Ploch
|
r37110 | state[name] = defmap[name].getdefaultvalue() | ||
mpm@selenic.com
|
r0 | |||
Matt Mackall
|
r5638 | # does it take a parameter? | ||
Daniel Ploch
|
r36373 | if not defmap[name]._isboolopt(): | ||
Matt Mackall
|
r10282 | if short: | ||
Augie Fackler
|
r43347 | short += b':' | ||
onames = [n + b'=' for n in onames] | ||||
Yuya Nishihara
|
r35223 | elif name not in nevernegate: | ||
for n in onames: | ||||
Augie Fackler
|
r43347 | if n.startswith(b'no-'): | ||
Yuya Nishihara
|
r35223 | insert = n[3:] | ||
else: | ||||
Augie Fackler
|
r43347 | insert = b'no-' + n | ||
Yuya Nishihara
|
r35223 | # backout (as a practical example) has both --commit and | ||
# --no-commit options, so we don't want to allow the | ||||
# negations of those flags. | ||||
if insert not in alllong: | ||||
Augie Fackler
|
r43347 | assert (b'--' + n) not in negations | ||
negations[b'--' + insert] = b'--' + n | ||||
Yuya Nishihara
|
r35223 | namelist.append(insert) | ||
Matt Mackall
|
r5638 | if short: | ||
shortlist += short | ||||
if name: | ||||
Yuya Nishihara
|
r35223 | namelist.extend(onames) | ||
Matt Mackall
|
r5638 | |||
# parse arguments | ||||
Yuya Nishihara
|
r35179 | if early: | ||
parse = functools.partial(earlygetopt, gnu=gnu) | ||||
elif gnu: | ||||
Yuya Nishihara
|
r35226 | parse = pycompat.gnugetoptb | ||
Augie Fackler
|
r7772 | else: | ||
Pulkit Goyal
|
r30578 | parse = pycompat.getoptb | ||
Augie Fackler
|
r7772 | opts, args = parse(args, shortlist, namelist) | ||
mpm@selenic.com
|
r0 | |||
Matt Mackall
|
r5638 | # transfer result to state | ||
for opt, val in opts: | ||||
Augie Fackler
|
r29947 | boolval = True | ||
negation = negations.get(opt, False) | ||||
if negation: | ||||
opt = negation | ||||
boolval = False | ||||
Matt Mackall
|
r5638 | name = argmap[opt] | ||
introom
|
r25563 | obj = defmap[name] | ||
Daniel Ploch
|
r36373 | if obj._isboolopt(): | ||
Augie Fackler
|
r29947 | state[name] = boolval | ||
Daniel Ploch
|
r36373 | else: | ||
Augie Fackler
|
r43346 | |||
Daniel Ploch
|
r36373 | def abort(s): | ||
Martin von Zweigbergk
|
r46441 | raise error.InputError( | ||
Augie Fackler
|
r43347 | _(b'invalid value %r for option %s, %s') | ||
Augie Fackler
|
r43346 | % (pycompat.maybebytestr(val), opt, s) | ||
) | ||||
Daniel Ploch
|
r36373 | state[name] = defmap[name].newstate(state[name], val, abort) | ||
mpm@selenic.com
|
r209 | |||
Matt Mackall
|
r5638 | # return unparsed args | ||
mpm@selenic.com
|
r0 | return args | ||