commands: emit types from partial parser, implement choice completion

This commit is contained in:
Aldo Cortesi 2017-12-15 11:34:53 +13:00
parent 8c0ba71fd8
commit 1c097813c1
3 changed files with 66 additions and 20 deletions

View File

@ -15,6 +15,14 @@ from mitmproxy import exceptions
from mitmproxy import flow from mitmproxy import flow
def lexer(s):
# mypy mis-identifies shlex.shlex as abstract
lex = shlex.shlex(s, punctuation_chars=True) # type: ignore
lex.whitespace_split = True
lex.commenters = ''
return lex
Cuts = typing.Sequence[ Cuts = typing.Sequence[
typing.Sequence[typing.Union[str, bytes]] typing.Sequence[typing.Union[str, bytes]]
] ]
@ -123,9 +131,10 @@ class Command:
return ret return ret
class ParseResult(typing.NamedTuple): ParseResult = typing.NamedTuple(
value: str "ParseResult",
type: typing.Type [("value", str), ("type", typing.Type)],
)
class CommandManager: class CommandManager:
@ -147,27 +156,37 @@ class CommandManager:
""" """
Parse a possibly partial command. Return a sequence of (part, type) tuples. Parse a possibly partial command. Return a sequence of (part, type) tuples.
""" """
parts: typing.List[ParseResult] = []
buf = io.StringIO(cmdstr) buf = io.StringIO(cmdstr)
# mypy mis-identifies shlex.shlex as abstract parts: typing.List[str] = []
lex = shlex.shlex(buf) # type: ignore lex = lexer(buf)
while 1: while 1:
remainder = cmdstr[buf.tell():] remainder = cmdstr[buf.tell():]
try: try:
t = lex.get_token() t = lex.get_token()
except ValueError: except ValueError:
parts.append(ParseResult(value = remainder, type = str)) parts.append(remainder)
break break
if not t: if not t:
break break
typ: type = str parts.append(t)
# First value is a special case: it has to be a command
if not parts: if not parts:
typ = Cmd parts = [""]
parts.append(ParseResult(value = t, type = typ)) elif cmdstr.endswith(" "):
if not parts: parts.append("")
return [ParseResult(value = "", type = Cmd)]
return parts parse: typing.List[ParseResult] = []
params: typing.List[type] = []
for i in range(len(parts)):
if i == 0:
params[:] = [Cmd]
if parts[i] in self.commands:
params.extend(self.commands[parts[i]].paramtypes)
if params:
typ = params.pop(0)
else:
typ = str
parse.append(ParseResult(value=parts[i], type=typ))
return parse
def call_args(self, path, args): def call_args(self, path, args):
""" """
@ -181,7 +200,7 @@ class CommandManager:
""" """
Call a command using a string. May raise CommandError. Call a command using a string. May raise CommandError.
""" """
parts = shlex.split(cmdstr) parts = list(lexer(cmdstr))
if not len(parts) >= 1: if not len(parts) >= 1:
raise exceptions.CommandError("Invalid command: %s" % cmdstr) raise exceptions.CommandError("Invalid command: %s" % cmdstr)
return self.call_args(parts[0], parts[1:]) return self.call_args(parts[0], parts[1:])
@ -208,8 +227,6 @@ def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any:
"Invalid choice: see %s for options" % cmd "Invalid choice: see %s for options" % cmd
) )
return spec return spec
if argtype in (Path, Cmd):
return spec
elif issubclass(argtype, str): elif issubclass(argtype, str):
return spec return spec
elif argtype == bool: elif argtype == bool:

View File

@ -72,7 +72,8 @@ class CommandBuffer():
def cycle_completion(self) -> None: def cycle_completion(self) -> None:
if not self.completion: if not self.completion:
parts = self.master.commands.parse_partial(self.buf[:self.cursor]) parts = self.master.commands.parse_partial(self.buf[:self.cursor])
if parts[-1].type == mitmproxy.command.Cmd: last = parts[-1]
if last.type == mitmproxy.command.Cmd:
self.completion = CompletionState( self.completion = CompletionState(
completer = ListCompleter( completer = ListCompleter(
parts[-1].value, parts[-1].value,
@ -80,6 +81,14 @@ class CommandBuffer():
), ),
parse = parts, parse = parts,
) )
elif isinstance(last.type, mitmproxy.command.Choice):
self.completion = CompletionState(
completer = ListCompleter(
parts[-1].value,
self.master.commands.call(last.type.options_command),
),
parse = parts,
)
if self.completion: if self.completion:
nxt = self.completion.completer.cycle() nxt = self.completion.completer.cycle()
buf = " ".join([i.value for i in self.completion.parse[:-1]]) + " " + nxt buf = " ".join([i.value for i in self.completion.parse[:-1]]) + " " + nxt

View File

@ -11,19 +11,24 @@ from mitmproxy.utils import typecheck
class TAddon: class TAddon:
@command.command("cmd1")
def cmd1(self, foo: str) -> str: def cmd1(self, foo: str) -> str:
"""cmd1 help""" """cmd1 help"""
return "ret " + foo return "ret " + foo
@command.command("cmd2")
def cmd2(self, foo: str) -> str: def cmd2(self, foo: str) -> str:
return 99 return 99
@command.command("cmd3")
def cmd3(self, foo: int) -> int: def cmd3(self, foo: int) -> int:
return foo return foo
@command.command("empty")
def empty(self) -> None: def empty(self) -> None:
pass pass
@command.command("varargs")
def varargs(self, one: str, *var: str) -> typing.Sequence[str]: def varargs(self, one: str, *var: str) -> typing.Sequence[str]:
return list(var) return list(var)
@ -34,6 +39,7 @@ class TAddon:
def choose(self, arg: str) -> typing.Sequence[str]: def choose(self, arg: str) -> typing.Sequence[str]:
return ["one", "two", "three"] return ["one", "two", "three"]
@command.command("path")
def path(self, arg: command.Path) -> None: def path(self, arg: command.Path) -> None:
pass pass
@ -82,11 +88,25 @@ class TestCommand:
], ],
["a", [command.ParseResult(value = "a", type = command.Cmd)]], ["a", [command.ParseResult(value = "a", type = command.Cmd)]],
["", [command.ParseResult(value = "", type = command.Cmd)]], ["", [command.ParseResult(value = "", type = command.Cmd)]],
[
"cmd3 1",
[
command.ParseResult(value = "cmd3", type = command.Cmd),
command.ParseResult(value = "1", type = int),
]
],
[
"cmd3 ",
[
command.ParseResult(value = "cmd3", type = command.Cmd),
command.ParseResult(value = "", type = int),
]
],
] ]
with taddons.context() as tctx: with taddons.context() as tctx:
cm = command.CommandManager(tctx.master) tctx.master.addons.add(TAddon())
for s, expected in tests: for s, expected in tests:
assert cm.parse_partial(s) == expected assert tctx.master.commands.parse_partial(s) == expected
def test_simple(): def test_simple():