diff --git a/src/arclet/alconna/_internal/_analyser.py b/src/arclet/alconna/_internal/_analyser.py index 18608f12..7ade4865 100644 --- a/src/arclet/alconna/_internal/_analyser.py +++ b/src/arclet/alconna/_internal/_analyser.py @@ -12,7 +12,6 @@ from ..arparma import Arparma from ..base import Completion, Help, Option, Shortcut, Subcommand from ..completion import comp_ctx -from ..config import config from ..exceptions import ArgumentMissing, FuzzyMatchSuccess, InvalidParam, ParamsUnmatched, PauseTriggered, SpecialOptionTriggered from ..manager import command_manager from ..model import HeadResult, OptionResult, Sentence, SubcommandResult @@ -286,10 +285,13 @@ def shortcut( if self.command.meta.raise_exception: raise exc return self.export(argv, True, exc) - # if short.fuzzy and reg and len(trigger) > reg.span()[1]: - # argv.addon((trigger[reg.span()[1] :],)) argv.addon(short.args) data = _handle_shortcut_data(argv, data) + if not data and argv.raw_data and any(isinstance(i, str) and "{%0}" in i for i in argv.raw_data): + exc = ArgumentMissing(lang.require("analyser", "param_missing")) + if self.command.meta.raise_exception: + raise exc + return self.export(argv, True, exc) argv.bak_data = argv.raw_data.copy() argv.addon(data) if reg: @@ -323,22 +325,16 @@ def process(self, argv: Argv[TDC]) -> Arparma[TDC]: if self.command.meta.raise_exception: raise e return self.export(argv, True, e) - text = argv.separators[0].join([_next] + argv.release()) try: - short, mat = command_manager.find_shortcut(self.command, text) + rest, short, mat = command_manager.find_shortcut(self.command, [_next] + argv.release()) except ValueError as exc: if self.command.meta.raise_exception: raise e from exc return self.export(argv, True, e) else: - if mat and len(text) > mat.span()[1]: - data = text[mat.span()[1]:].lstrip(argv.separators[0]).split(argv.separators[0]) - else: - data = [] - # data = argv.release() self.reset() argv.reset() - return self.shortcut(argv, data, short, mat) + return self.shortcut(argv, rest, short, mat) except FuzzyMatchSuccess as Fuzzy: output_manager.send(self.command.name, lambda: str(Fuzzy)) diff --git a/src/arclet/alconna/_internal/_argv.py b/src/arclet/alconna/_internal/_argv.py index e47336cf..165676b7 100644 --- a/src/arclet/alconna/_internal/_argv.py +++ b/src/arclet/alconna/_internal/_argv.py @@ -240,7 +240,7 @@ def release(self, separate: tuple[str, ...] | None = None, recover: bool = False list[str | Any]: 剩余的数据. """ _result = [] - data = self.bak_data if recover else self.raw_data[self.current_index :] + data = self.bak_data if recover else self.raw_data[self.current_index:] for _data in data: if _data.__class__ is str: _result.extend(split(_data, separate or (" ",), self.filter_crlf)) diff --git a/src/arclet/alconna/_internal/_handlers.py b/src/arclet/alconna/_internal/_handlers.py index 558aca99..90084d9f 100644 --- a/src/arclet/alconna/_internal/_handlers.py +++ b/src/arclet/alconna/_internal/_handlers.py @@ -455,14 +455,14 @@ def analyse_header(header: Header, argv: Argv) -> HeadResult: elif content.__class__ is TPattern and (mat := content.fullmatch(head_text)): return HeadResult(head_text, head_text, True, mat.groupdict(), mapping) if header.compact and content.__class__ in (set, TPattern) and (mat := header.compact_pattern.match(head_text)): - argv.rollback(head_text[len(mat[0]) :], replace=True) + argv.rollback(head_text[len(mat[0]):], replace=True) return HeadResult(mat[0], mat[0], True, mat.groupdict(), mapping) if isinstance(content, BasePattern): if (val := content.exec(head_text, Empty)).success: return HeadResult(head_text, val.value, True, fixes=mapping) if header.compact and (val := header.compact_pattern.exec(head_text, Empty)).success: if _str: - argv.rollback(head_text[len(str(val.value)) :], replace=True) + argv.rollback(head_text[len(str(val.value)):], replace=True) return HeadResult(val.value, val.value, True, fixes=mapping) may_cmd, _m_str = argv.next() @@ -537,9 +537,9 @@ def handle_shortcut(analyser: Analyser, argv: Argv): elif opt_v["command"] == "_": msg = analyser.command.shortcut(opt_v["name"], None) elif opt_v["command"] == "$": - msg = analyser.command.shortcut(opt_v["name"], fuzzy=False) + msg = analyser.command.shortcut(opt_v["name"], fuzzy=True) else: - msg = analyser.command.shortcut(opt_v["name"], fuzzy=False, command=opt_v["command"]) + msg = analyser.command.shortcut(opt_v["name"], fuzzy=True, command=opt_v["command"]) output_manager.send(analyser.command.name, lambda: msg) except Exception as e: output_manager.send(analyser.command.name, lambda: str(e)) diff --git a/src/arclet/alconna/_internal/_header.py b/src/arclet/alconna/_internal/_header.py index 49a66beb..4617f16b 100644 --- a/src/arclet/alconna/_internal/_header.py +++ b/src/arclet/alconna/_internal/_header.py @@ -59,7 +59,7 @@ def _match(command: str, pbfn: Callable[..., ...], comp: bool): if command == self.pattern: return command, None if comp and command.startswith(self.pattern): - pbfn(command[len(self.pattern) :], replace=True) + pbfn(command[len(self.pattern):], replace=True) return self.pattern, None return None, None @@ -70,7 +70,7 @@ def _match(command: str, pbfn: Callable[..., ...], comp: bool): if mat := self.pattern.fullmatch(command): return command, mat if comp and (mat := self.pattern.match(command)): - pbfn(command[len(mat[0]) :], replace=True) + pbfn(command[len(mat[0]):], replace=True) return mat[0], mat return None, None @@ -150,7 +150,7 @@ def match0(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[... return (pf, cmd), (pf, val.value), True, None if comp and (val := self.comp_pattern.exec(cmd, Empty)).success: if c_str: - pbfn(cmd[len(str(val.value)) :], replace=True) + pbfn(cmd[len(str(val.value)):], replace=True) return (pf, cmd), (pf, cmd[: len(str(val.value))]), True, None return if (val := self.patterns.exec(pf, Empty)).success: @@ -158,7 +158,7 @@ def match0(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[... return (pf, cmd), (val.value, val2.value), True, None if comp and (val2 := self.comp_pattern.exec(cmd, Empty)).success: if c_str: - pbfn(cmd[len(str(val2.value)) :], replace=True) + pbfn(cmd[len(str(val2.value)):], replace=True) return (pf, cmd), (val.value, cmd[: len(str(val2.value))]), True, None return @@ -168,7 +168,7 @@ def match1(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[... if (val := self.patterns.exec(pf, Empty)).success and (mat := self.command.fullmatch(cmd)): return (pf, cmd), (val.value, cmd), True, mat.groupdict() if comp and (mat := self.comp_pattern.match(cmd)): - pbfn(cmd[len(mat[0]) :], replace=True) + pbfn(cmd[len(mat[0]):], replace=True) return (pf, cmd), (pf, mat[0]), True, mat.groupdict() def match(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[..., ...], comp: bool): @@ -184,21 +184,21 @@ def match(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[..., return pf, pf, True, mat.groupdict() if comp and (mat := self.comp_pattern.match(pf)): pbfn(cmd) - pbfn(pf[len(mat[0]) :], replace=True) + pbfn(pf[len(mat[0]):], replace=True) return mat[0], mat[0], True, mat.groupdict() if not c_str: return if mat := self.prefix.fullmatch((name := pf + cmd)): return name, name, True, mat.groupdict() if comp and (mat := self.comp_pattern.match(name)): - pbfn(name[len(mat[0]) :], replace=True) + pbfn(name[len(mat[0]):], replace=True) return mat[0], mat[0], True, mat.groupdict() return if (val := self.patterns.exec(pf, Empty)).success: if mat := self.command.fullmatch(cmd): return (pf, cmd), (val.value, cmd), True, mat.groupdict() if comp and (mat := self.command.match(cmd)): - pbfn(cmd[len(mat[0]) :], replace=True) + pbfn(cmd[len(mat[0]):], replace=True) return (pf, cmd), (val.value, mat[0]), True, mat.groupdict() diff --git a/src/arclet/alconna/completion.py b/src/arclet/alconna/completion.py index e7cc1e65..2fee1039 100644 --- a/src/arclet/alconna/completion.py +++ b/src/arclet/alconna/completion.py @@ -130,7 +130,7 @@ def enter(self, content: list | None = None) -> EnterResult: if isinstance(self.trigger, InvalidParam): argv.raw_data = argv.bak_data[: max(self.current_index, 1)] argv.addon(input_) - argv.raw_data.extend(self.raw_data[max(self.current_index, 1) :]) + argv.raw_data.extend(self.raw_data[max(self.current_index, 1):]) else: argv.raw_data = argv.bak_data.copy() argv.addon(input_) diff --git a/src/arclet/alconna/exceptions.py b/src/arclet/alconna/exceptions.py index 41a16fff..7db0dea3 100644 --- a/src/arclet/alconna/exceptions.py +++ b/src/arclet/alconna/exceptions.py @@ -4,9 +4,11 @@ class ParamsUnmatched(Exception): """一个传入参数没有被选项或Args匹配""" + class InvalidParam(Exception): """传入参数验证失败""" + class ArgumentMissing(Exception): """组件内的 Args 参数未能解析到任何内容""" diff --git a/src/arclet/alconna/formatter.py b/src/arclet/alconna/formatter.py index 7665c35d..73790a28 100644 --- a/src/arclet/alconna/formatter.py +++ b/src/arclet/alconna/formatter.py @@ -60,7 +60,6 @@ def ensure_node(targets: list[str], options: list[Option | Subcommand]): return ensure_node(targets, options) - @dataclass(eq=True) class Trace: """存放命令节点数据的结构 diff --git a/src/arclet/alconna/manager.py b/src/arclet/alconna/manager.py index d253c5c9..c7279b8c 100644 --- a/src/arclet/alconna/manager.py +++ b/src/arclet/alconna/manager.py @@ -237,31 +237,40 @@ def get_shortcut(self, target: Alconna[TDC]) -> dict[str, Union[Arparma[TDC], In return _shortcut def find_shortcut( - self, target: Alconna[TDC], query: str - ) -> tuple[Arparma[TDC] | InnerShortcutArgs, Match[str] | None]: + self, target: Alconna[TDC], data: list + ) -> tuple[list, Arparma[TDC] | InnerShortcutArgs, Match[str] | None]: """查找快捷命令 Args: - target (Alconna): 目标命令 - query (str): 快捷命令的名称. + target (Alconna): 目标命令对象 + data (list): 传入的命令数据 Returns: - tuple[Union[Arparma, InnerShortcutArgs], re.Match[str]]: 返回匹配的快捷命令 + tuple[list, Union[Arparma, InnerShortcutArgs], re.Match[str]]: 返回匹配的快捷命令 """ namespace, name = self._command_part(target.path) if not (_shortcut := self.__shortcuts.get(f"{namespace}.{name}")): raise ValueError(lang.require("manager", "undefined_command").format(target=f"{namespace}.{name}")) - try: - return _shortcut[query], None - except KeyError as e: + query: str = data.pop(0) + while True: + if query in _shortcut: + return data, _shortcut[query], None for key, args in _shortcut.items(): if isinstance(args, InnerShortcutArgs) and args.fuzzy and (mat := re.match(f"^{key}", query)): - return args, mat + if len(query) > mat.span()[1]: + data.insert(0, query[mat.span()[1]:]) + return data, args, mat elif mat := re.fullmatch(key, query): - return _shortcut[key], mat - raise ValueError( - lang.require("manager", "shortcut_parse_error").format(target=f"{namespace}.{name}", query=query) - ) from e + return data, _shortcut[key], mat + if not data: + break + next_data = data.pop(0) + if not isinstance(next_data, str): + break + query += f"{target.separators[0]}{next_data}" + raise ValueError( + lang.require("manager", "shortcut_parse_error").format(target=f"{namespace}.{name}", query=query) + ) def delete_shortcut(self, target: Alconna, key: str | None = None): """删除快捷命令""" @@ -351,10 +360,10 @@ def all_command_help( command_string = ( "\n".join( f" {str(index).rjust(len(str(page * max_length)), '0')} {slot[0]} : {slot[1]}" - for index, slot in enumerate(slots[(page - 1) * max_length : page * max_length], start=(page - 1) * max_length) # noqa: E501 + for index, slot in enumerate(slots[(page - 1) * max_length: page * max_length], start=(page - 1) * max_length) # noqa: E501 ) if show_index - else "\n".join(f" - {n} : {d}" for n, d in slots[(page - 1) * max_length : page * max_length]) + else "\n".join(f" - {n} : {d}" for n, d in slots[(page - 1) * max_length: page * max_length]) ) help_names = set() for i in cmds: diff --git a/tests/core_test.py b/tests/core_test.py index f831633c..e6d7f68f 100644 --- a/tests/core_test.py +++ b/tests/core_test.py @@ -429,20 +429,20 @@ def test_shortcut(): # 原始命令 alc16 = Alconna("core16", Args["foo", int], Option("bar", Args["baz", str])) assert alc16.parse("core16 123 bar abcd").matched is True - # 指令缩写传入, TEST1 -> core16 321 - alc16.parse("core16 --shortcut TEST1 'core16 321'") - res1 = alc16.parse("TEST1") - assert res1.foo == 321 - # 指令缩写传入的不允许后随参数 - alc16.parse("core16 --shortcut TEST2 core16") - res2 = alc16.parse("TEST2 442") - assert not res2.matched # 构造体缩写传入;{i} 将被可能的正则匹配替换 alc16.shortcut(r"TEST(\d+)(.+)", {"args": ["{0}", "bar {1}"]}) res = alc16.parse("TEST123aa") assert res.matched is True assert res.foo == 123 assert res.baz == "aa" + # 指令缩写传入, TEST1 -> core16 321 + alc16.parse("core16 --shortcut TEST1 'core16 321'") + res1 = alc16.parse("TEST1") + assert res1.foo == 321 + # 指令缩写传入的允许后随参数 + alc16.parse("core16 --shortcut TEST2 core16") + res2 = alc16.parse("TEST2 442") + assert res2.foo == 442 # 指令缩写也支持正则 alc16.parse(r"core16 --shortcut TESTa4(\d+) 'core16 {0}'") res3 = alc16.parse("TESTa4257") @@ -462,6 +462,7 @@ def test_shortcut(): assert not res7.matched res8 = alc16_1.parse("echo \\\\'123\\\\'") assert res8.content == "print('123')" + assert not alc16_1.parse("echo").matched alc16_2 = Alconna([1, 2, "3"], "core16_2", Args["foo", bool]) alc16_2.shortcut("test", {"command": [1, "core16_2 True"]}) # type: ignore @@ -507,6 +508,9 @@ def wrapper(slot, content): alc16_6.parse("testhelp") assert cap["output"] == "core16_6 \nUnknown" + alc16_7 = Alconna("core16_7", Args["bar", str]) + alc16_7.shortcut("test 123", {"args": ["abc"]}) + assert alc16_7.parse("test 123").bar == "abc" def test_help(): from arclet.alconna import output_manager