Skip to content

Commit 46a257a

Browse files
committed
✨ support shortcut key include separator
1 parent 0f8407c commit 46a257a

File tree

9 files changed

+59
-49
lines changed

9 files changed

+59
-49
lines changed

src/arclet/alconna/_internal/_analyser.py

+7-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from ..arparma import Arparma
1313
from ..base import Completion, Help, Option, Shortcut, Subcommand
1414
from ..completion import comp_ctx
15-
from ..config import config
1615
from ..exceptions import ArgumentMissing, FuzzyMatchSuccess, InvalidParam, ParamsUnmatched, PauseTriggered, SpecialOptionTriggered
1716
from ..manager import command_manager
1817
from ..model import HeadResult, OptionResult, Sentence, SubcommandResult
@@ -286,10 +285,13 @@ def shortcut(
286285
if self.command.meta.raise_exception:
287286
raise exc
288287
return self.export(argv, True, exc)
289-
# if short.fuzzy and reg and len(trigger) > reg.span()[1]:
290-
# argv.addon((trigger[reg.span()[1] :],))
291288
argv.addon(short.args)
292289
data = _handle_shortcut_data(argv, data)
290+
if not data and argv.raw_data and any(isinstance(i, str) and "{%0}" in i for i in argv.raw_data):
291+
exc = ArgumentMissing(lang.require("analyser", "param_missing"))
292+
if self.command.meta.raise_exception:
293+
raise exc
294+
return self.export(argv, True, exc)
293295
argv.bak_data = argv.raw_data.copy()
294296
argv.addon(data)
295297
if reg:
@@ -323,22 +325,16 @@ def process(self, argv: Argv[TDC]) -> Arparma[TDC]:
323325
if self.command.meta.raise_exception:
324326
raise e
325327
return self.export(argv, True, e)
326-
text = argv.separators[0].join([_next] + argv.release())
327328
try:
328-
short, mat = command_manager.find_shortcut(self.command, text)
329+
rest, short, mat = command_manager.find_shortcut(self.command, [_next] + argv.release())
329330
except ValueError as exc:
330331
if self.command.meta.raise_exception:
331332
raise e from exc
332333
return self.export(argv, True, e)
333334
else:
334-
if mat and len(text) > mat.span()[1]:
335-
data = text[mat.span()[1]:].lstrip(argv.separators[0]).split(argv.separators[0])
336-
else:
337-
data = []
338-
# data = argv.release()
339335
self.reset()
340336
argv.reset()
341-
return self.shortcut(argv, data, short, mat)
337+
return self.shortcut(argv, rest, short, mat)
342338

343339
except FuzzyMatchSuccess as Fuzzy:
344340
output_manager.send(self.command.name, lambda: str(Fuzzy))

src/arclet/alconna/_internal/_argv.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def release(self, separate: tuple[str, ...] | None = None, recover: bool = False
240240
list[str | Any]: 剩余的数据.
241241
"""
242242
_result = []
243-
data = self.bak_data if recover else self.raw_data[self.current_index :]
243+
data = self.bak_data if recover else self.raw_data[self.current_index:]
244244
for _data in data:
245245
if _data.__class__ is str:
246246
_result.extend(split(_data, separate or (" ",), self.filter_crlf))

src/arclet/alconna/_internal/_handlers.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -455,14 +455,14 @@ def analyse_header(header: Header, argv: Argv) -> HeadResult:
455455
elif content.__class__ is TPattern and (mat := content.fullmatch(head_text)):
456456
return HeadResult(head_text, head_text, True, mat.groupdict(), mapping)
457457
if header.compact and content.__class__ in (set, TPattern) and (mat := header.compact_pattern.match(head_text)):
458-
argv.rollback(head_text[len(mat[0]) :], replace=True)
458+
argv.rollback(head_text[len(mat[0]):], replace=True)
459459
return HeadResult(mat[0], mat[0], True, mat.groupdict(), mapping)
460460
if isinstance(content, BasePattern):
461461
if (val := content.exec(head_text, Empty)).success:
462462
return HeadResult(head_text, val.value, True, fixes=mapping)
463463
if header.compact and (val := header.compact_pattern.exec(head_text, Empty)).success:
464464
if _str:
465-
argv.rollback(head_text[len(str(val.value)) :], replace=True)
465+
argv.rollback(head_text[len(str(val.value)):], replace=True)
466466
return HeadResult(val.value, val.value, True, fixes=mapping)
467467

468468
may_cmd, _m_str = argv.next()
@@ -537,9 +537,9 @@ def handle_shortcut(analyser: Analyser, argv: Argv):
537537
elif opt_v["command"] == "_":
538538
msg = analyser.command.shortcut(opt_v["name"], None)
539539
elif opt_v["command"] == "$":
540-
msg = analyser.command.shortcut(opt_v["name"], fuzzy=False)
540+
msg = analyser.command.shortcut(opt_v["name"], fuzzy=True)
541541
else:
542-
msg = analyser.command.shortcut(opt_v["name"], fuzzy=False, command=opt_v["command"])
542+
msg = analyser.command.shortcut(opt_v["name"], fuzzy=True, command=opt_v["command"])
543543
output_manager.send(analyser.command.name, lambda: msg)
544544
except Exception as e:
545545
output_manager.send(analyser.command.name, lambda: str(e))

src/arclet/alconna/_internal/_header.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def _match(command: str, pbfn: Callable[..., ...], comp: bool):
5959
if command == self.pattern:
6060
return command, None
6161
if comp and command.startswith(self.pattern):
62-
pbfn(command[len(self.pattern) :], replace=True)
62+
pbfn(command[len(self.pattern):], replace=True)
6363
return self.pattern, None
6464
return None, None
6565

@@ -70,7 +70,7 @@ def _match(command: str, pbfn: Callable[..., ...], comp: bool):
7070
if mat := self.pattern.fullmatch(command):
7171
return command, mat
7272
if comp and (mat := self.pattern.match(command)):
73-
pbfn(command[len(mat[0]) :], replace=True)
73+
pbfn(command[len(mat[0]):], replace=True)
7474
return mat[0], mat
7575
return None, None
7676

@@ -150,15 +150,15 @@ def match0(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[...
150150
return (pf, cmd), (pf, val.value), True, None
151151
if comp and (val := self.comp_pattern.exec(cmd, Empty)).success:
152152
if c_str:
153-
pbfn(cmd[len(str(val.value)) :], replace=True)
153+
pbfn(cmd[len(str(val.value)):], replace=True)
154154
return (pf, cmd), (pf, cmd[: len(str(val.value))]), True, None
155155
return
156156
if (val := self.patterns.exec(pf, Empty)).success:
157157
if (val2 := self.command.exec(cmd, Empty)).success:
158158
return (pf, cmd), (val.value, val2.value), True, None
159159
if comp and (val2 := self.comp_pattern.exec(cmd, Empty)).success:
160160
if c_str:
161-
pbfn(cmd[len(str(val2.value)) :], replace=True)
161+
pbfn(cmd[len(str(val2.value)):], replace=True)
162162
return (pf, cmd), (val.value, cmd[: len(str(val2.value))]), True, None
163163
return
164164

@@ -168,7 +168,7 @@ def match1(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[...
168168
if (val := self.patterns.exec(pf, Empty)).success and (mat := self.command.fullmatch(cmd)):
169169
return (pf, cmd), (val.value, cmd), True, mat.groupdict()
170170
if comp and (mat := self.comp_pattern.match(cmd)):
171-
pbfn(cmd[len(mat[0]) :], replace=True)
171+
pbfn(cmd[len(mat[0]):], replace=True)
172172
return (pf, cmd), (pf, mat[0]), True, mat.groupdict()
173173

174174
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[...,
184184
return pf, pf, True, mat.groupdict()
185185
if comp and (mat := self.comp_pattern.match(pf)):
186186
pbfn(cmd)
187-
pbfn(pf[len(mat[0]) :], replace=True)
187+
pbfn(pf[len(mat[0]):], replace=True)
188188
return mat[0], mat[0], True, mat.groupdict()
189189
if not c_str:
190190
return
191191
if mat := self.prefix.fullmatch((name := pf + cmd)):
192192
return name, name, True, mat.groupdict()
193193
if comp and (mat := self.comp_pattern.match(name)):
194-
pbfn(name[len(mat[0]) :], replace=True)
194+
pbfn(name[len(mat[0]):], replace=True)
195195
return mat[0], mat[0], True, mat.groupdict()
196196
return
197197
if (val := self.patterns.exec(pf, Empty)).success:
198198
if mat := self.command.fullmatch(cmd):
199199
return (pf, cmd), (val.value, cmd), True, mat.groupdict()
200200
if comp and (mat := self.command.match(cmd)):
201-
pbfn(cmd[len(mat[0]) :], replace=True)
201+
pbfn(cmd[len(mat[0]):], replace=True)
202202
return (pf, cmd), (val.value, mat[0]), True, mat.groupdict()
203203

204204

src/arclet/alconna/completion.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def enter(self, content: list | None = None) -> EnterResult:
130130
if isinstance(self.trigger, InvalidParam):
131131
argv.raw_data = argv.bak_data[: max(self.current_index, 1)]
132132
argv.addon(input_)
133-
argv.raw_data.extend(self.raw_data[max(self.current_index, 1) :])
133+
argv.raw_data.extend(self.raw_data[max(self.current_index, 1):])
134134
else:
135135
argv.raw_data = argv.bak_data.copy()
136136
argv.addon(input_)

src/arclet/alconna/exceptions.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
class ParamsUnmatched(Exception):
55
"""一个传入参数没有被选项或Args匹配"""
66

7+
78
class InvalidParam(Exception):
89
"""传入参数验证失败"""
910

11+
1012
class ArgumentMissing(Exception):
1113
"""组件内的 Args 参数未能解析到任何内容"""
1214

src/arclet/alconna/formatter.py

-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ def ensure_node(targets: list[str], options: list[Option | Subcommand]):
6060
return ensure_node(targets, options)
6161

6262

63-
6463
@dataclass(eq=True)
6564
class Trace:
6665
"""存放命令节点数据的结构

src/arclet/alconna/manager.py

+24-15
Original file line numberDiff line numberDiff line change
@@ -237,31 +237,40 @@ def get_shortcut(self, target: Alconna[TDC]) -> dict[str, Union[Arparma[TDC], In
237237
return _shortcut
238238

239239
def find_shortcut(
240-
self, target: Alconna[TDC], query: str
241-
) -> tuple[Arparma[TDC] | InnerShortcutArgs, Match[str] | None]:
240+
self, target: Alconna[TDC], data: list
241+
) -> tuple[list, Arparma[TDC] | InnerShortcutArgs, Match[str] | None]:
242242
"""查找快捷命令
243243
244244
Args:
245-
target (Alconna): 目标命令
246-
query (str): 快捷命令的名称.
245+
target (Alconna): 目标命令对象
246+
data (list): 传入的命令数据
247247
248248
Returns:
249-
tuple[Union[Arparma, InnerShortcutArgs], re.Match[str]]: 返回匹配的快捷命令
249+
tuple[list, Union[Arparma, InnerShortcutArgs], re.Match[str]]: 返回匹配的快捷命令
250250
"""
251251
namespace, name = self._command_part(target.path)
252252
if not (_shortcut := self.__shortcuts.get(f"{namespace}.{name}")):
253253
raise ValueError(lang.require("manager", "undefined_command").format(target=f"{namespace}.{name}"))
254-
try:
255-
return _shortcut[query], None
256-
except KeyError as e:
254+
query: str = data.pop(0)
255+
while True:
256+
if query in _shortcut:
257+
return data, _shortcut[query], None
257258
for key, args in _shortcut.items():
258259
if isinstance(args, InnerShortcutArgs) and args.fuzzy and (mat := re.match(f"^{key}", query)):
259-
return args, mat
260+
if len(query) > mat.span()[1]:
261+
data.insert(0, query[mat.span()[1]:])
262+
return data, args, mat
260263
elif mat := re.fullmatch(key, query):
261-
return _shortcut[key], mat
262-
raise ValueError(
263-
lang.require("manager", "shortcut_parse_error").format(target=f"{namespace}.{name}", query=query)
264-
) from e
264+
return data, _shortcut[key], mat
265+
if not data:
266+
break
267+
next_data = data.pop(0)
268+
if not isinstance(next_data, str):
269+
break
270+
query += f"{target.separators[0]}{next_data}"
271+
raise ValueError(
272+
lang.require("manager", "shortcut_parse_error").format(target=f"{namespace}.{name}", query=query)
273+
)
265274

266275
def delete_shortcut(self, target: Alconna, key: str | None = None):
267276
"""删除快捷命令"""
@@ -351,10 +360,10 @@ def all_command_help(
351360
command_string = (
352361
"\n".join(
353362
f" {str(index).rjust(len(str(page * max_length)), '0')} {slot[0]} : {slot[1]}"
354-
for index, slot in enumerate(slots[(page - 1) * max_length : page * max_length], start=(page - 1) * max_length) # noqa: E501
363+
for index, slot in enumerate(slots[(page - 1) * max_length: page * max_length], start=(page - 1) * max_length) # noqa: E501
355364
)
356365
if show_index
357-
else "\n".join(f" - {n} : {d}" for n, d in slots[(page - 1) * max_length : page * max_length])
366+
else "\n".join(f" - {n} : {d}" for n, d in slots[(page - 1) * max_length: page * max_length])
358367
)
359368
help_names = set()
360369
for i in cmds:

tests/core_test.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -429,20 +429,20 @@ def test_shortcut():
429429
# 原始命令
430430
alc16 = Alconna("core16", Args["foo", int], Option("bar", Args["baz", str]))
431431
assert alc16.parse("core16 123 bar abcd").matched is True
432-
# 指令缩写传入, TEST1 -> core16 321
433-
alc16.parse("core16 --shortcut TEST1 'core16 321'")
434-
res1 = alc16.parse("TEST1")
435-
assert res1.foo == 321
436-
# 指令缩写传入的不允许后随参数
437-
alc16.parse("core16 --shortcut TEST2 core16")
438-
res2 = alc16.parse("TEST2 442")
439-
assert not res2.matched
440432
# 构造体缩写传入;{i} 将被可能的正则匹配替换
441433
alc16.shortcut(r"TEST(\d+)(.+)", {"args": ["{0}", "bar {1}"]})
442434
res = alc16.parse("TEST123aa")
443435
assert res.matched is True
444436
assert res.foo == 123
445437
assert res.baz == "aa"
438+
# 指令缩写传入, TEST1 -> core16 321
439+
alc16.parse("core16 --shortcut TEST1 'core16 321'")
440+
res1 = alc16.parse("TEST1")
441+
assert res1.foo == 321
442+
# 指令缩写传入的允许后随参数
443+
alc16.parse("core16 --shortcut TEST2 core16")
444+
res2 = alc16.parse("TEST2 442")
445+
assert res2.foo == 442
446446
# 指令缩写也支持正则
447447
alc16.parse(r"core16 --shortcut TESTa4(\d+) 'core16 {0}'")
448448
res3 = alc16.parse("TESTa4257")
@@ -462,6 +462,7 @@ def test_shortcut():
462462
assert not res7.matched
463463
res8 = alc16_1.parse("echo \\\\'123\\\\'")
464464
assert res8.content == "print('123')"
465+
assert not alc16_1.parse("echo").matched
465466

466467
alc16_2 = Alconna([1, 2, "3"], "core16_2", Args["foo", bool])
467468
alc16_2.shortcut("test", {"command": [1, "core16_2 True"]}) # type: ignore
@@ -507,6 +508,9 @@ def wrapper(slot, content):
507508
alc16_6.parse("testhelp")
508509
assert cap["output"] == "core16_6 <bar: str> \nUnknown"
509510

511+
alc16_7 = Alconna("core16_7", Args["bar", str])
512+
alc16_7.shortcut("test 123", {"args": ["abc"]})
513+
assert alc16_7.parse("test 123").bar == "abc"
510514

511515
def test_help():
512516
from arclet.alconna import output_manager

0 commit comments

Comments
 (0)