mirror of
https://github.com/nonebot/nonebot2.git
synced 2024-11-28 05:16:48 +08:00
✨ add rich text support for shell command (#1171)
This commit is contained in:
parent
00af815b8a
commit
3f8af04803
@ -5,16 +5,16 @@ FrontMatter:
|
|||||||
description: nonebot.params 模块
|
description: nonebot.params 模块
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, List, Tuple, Optional
|
from typing import Any, Dict, List, Tuple, Union, Optional
|
||||||
|
|
||||||
from nonebot.typing import T_State
|
from nonebot.typing import T_State
|
||||||
from nonebot.matcher import Matcher
|
from nonebot.matcher import Matcher
|
||||||
from nonebot.adapters import Event, Message
|
|
||||||
from nonebot.internal.params import Arg as Arg
|
from nonebot.internal.params import Arg as Arg
|
||||||
from nonebot.internal.params import ArgStr as ArgStr
|
from nonebot.internal.params import ArgStr as ArgStr
|
||||||
from nonebot.internal.params import Depends as Depends
|
from nonebot.internal.params import Depends as Depends
|
||||||
from nonebot.internal.params import ArgParam as ArgParam
|
from nonebot.internal.params import ArgParam as ArgParam
|
||||||
from nonebot.internal.params import BotParam as BotParam
|
from nonebot.internal.params import BotParam as BotParam
|
||||||
|
from nonebot.adapters import Event, Message, MessageSegment
|
||||||
from nonebot.internal.params import EventParam as EventParam
|
from nonebot.internal.params import EventParam as EventParam
|
||||||
from nonebot.internal.params import StateParam as StateParam
|
from nonebot.internal.params import StateParam as StateParam
|
||||||
from nonebot.internal.params import DependParam as DependParam
|
from nonebot.internal.params import DependParam as DependParam
|
||||||
@ -109,15 +109,15 @@ def CommandStart() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _shell_command_args(state: T_State) -> Any:
|
def _shell_command_args(state: T_State) -> Any:
|
||||||
return state[SHELL_ARGS]
|
return state[SHELL_ARGS] # Namespace or ParserExit
|
||||||
|
|
||||||
|
|
||||||
def ShellCommandArgs():
|
def ShellCommandArgs() -> Any:
|
||||||
"""shell 命令解析后的参数字典"""
|
"""shell 命令解析后的参数字典"""
|
||||||
return Depends(_shell_command_args, use_cache=False)
|
return Depends(_shell_command_args, use_cache=False)
|
||||||
|
|
||||||
|
|
||||||
def _shell_command_argv(state: T_State) -> List[str]:
|
def _shell_command_argv(state: T_State) -> List[Union[str, MessageSegment]]:
|
||||||
return state[SHELL_ARGV]
|
return state[SHELL_ARGV]
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,10 +10,26 @@ FrontMatter:
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
from itertools import product
|
from argparse import Action
|
||||||
from argparse import Namespace
|
from argparse import ArgumentError
|
||||||
|
from itertools import chain, product
|
||||||
|
from argparse import Namespace as Namespace
|
||||||
from argparse import ArgumentParser as ArgParser
|
from argparse import ArgumentParser as ArgParser
|
||||||
from typing import Any, List, Tuple, Union, Optional, Sequence, TypedDict, NamedTuple
|
from typing import (
|
||||||
|
IO,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
List,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
TypeVar,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
TypedDict,
|
||||||
|
NamedTuple,
|
||||||
|
cast,
|
||||||
|
overload,
|
||||||
|
)
|
||||||
|
|
||||||
from pygtrie import CharTrie
|
from pygtrie import CharTrie
|
||||||
|
|
||||||
@ -44,6 +60,8 @@ from nonebot.consts import (
|
|||||||
REGEX_MATCHED,
|
REGEX_MATCHED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
CMD_RESULT = TypedDict(
|
CMD_RESULT = TypedDict(
|
||||||
"CMD_RESULT",
|
"CMD_RESULT",
|
||||||
{
|
{
|
||||||
@ -318,25 +336,48 @@ class ArgumentParser(ArgParser):
|
|||||||
参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
|
参考文档: [argparse](https://docs.python.org/3/library/argparse.html)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _print_message(self, message, file=None):
|
if TYPE_CHECKING:
|
||||||
old_message: str = getattr(self, "message", "")
|
|
||||||
if old_message:
|
|
||||||
old_message += "\n"
|
|
||||||
old_message += message
|
|
||||||
setattr(self, "message", old_message)
|
|
||||||
|
|
||||||
def exit(self, status: int = 0, message: Optional[str] = None):
|
@overload
|
||||||
raise ParserExit(
|
def parse_args(
|
||||||
status=status, message=message or getattr(self, "message", None)
|
self, args: Optional[Sequence[Union[str, MessageSegment]]] = ...
|
||||||
)
|
) -> Namespace:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parse_args(
|
||||||
|
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: None
|
||||||
|
) -> Namespace:
|
||||||
|
... # type: ignore[misc]
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def parse_args(
|
||||||
|
self, args: Optional[Sequence[Union[str, MessageSegment]]], namespace: T
|
||||||
|
) -> T:
|
||||||
|
...
|
||||||
|
|
||||||
def parse_args(
|
def parse_args(
|
||||||
self,
|
self,
|
||||||
args: Optional[Sequence[str]] = None,
|
args: Optional[Sequence[Union[str, MessageSegment]]] = None,
|
||||||
namespace: Optional[Namespace] = None,
|
namespace: Optional[T] = None,
|
||||||
) -> Namespace:
|
) -> Union[Namespace, T]:
|
||||||
setattr(self, "message", "")
|
...
|
||||||
return super().parse_args(args=args, namespace=namespace) # type: ignore
|
|
||||||
|
def _parse_optional(
|
||||||
|
self, arg_string: Union[str, MessageSegment]
|
||||||
|
) -> Optional[Tuple[Optional[Action], str, Optional[str]]]:
|
||||||
|
return (
|
||||||
|
super()._parse_optional(arg_string) if isinstance(arg_string, str) else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def _print_message(self, message: str, file: Optional[IO[str]] = None):
|
||||||
|
if message:
|
||||||
|
setattr(self, "_message", getattr(self, "_message", "") + message)
|
||||||
|
|
||||||
|
def exit(self, status: int = 0, message: Optional[str] = None):
|
||||||
|
if message:
|
||||||
|
self._print_message(message)
|
||||||
|
raise ParserExit(status=status, message=getattr(self, "_message", None))
|
||||||
|
|
||||||
|
|
||||||
class ShellCommandRule:
|
class ShellCommandRule:
|
||||||
@ -359,18 +400,25 @@ class ShellCommandRule:
|
|||||||
cmd: Optional[Tuple[str, ...]] = Command(),
|
cmd: Optional[Tuple[str, ...]] = Command(),
|
||||||
msg: Optional[Message] = CommandArg(),
|
msg: Optional[Message] = CommandArg(),
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if cmd in self.cmds and msg is not None:
|
if cmd not in self.cmds or msg is None:
|
||||||
message = str(msg)
|
return False
|
||||||
state[SHELL_ARGV] = shlex.split(message)
|
|
||||||
|
state[SHELL_ARGV] = list(
|
||||||
|
chain.from_iterable(
|
||||||
|
shlex.split(str(seg)) if cast(MessageSegment, seg).is_text() else (seg,)
|
||||||
|
for seg in msg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if self.parser:
|
if self.parser:
|
||||||
try:
|
try:
|
||||||
args = self.parser.parse_args(state[SHELL_ARGV])
|
args = self.parser.parse_args(state[SHELL_ARGV])
|
||||||
state[SHELL_ARGS] = args
|
state[SHELL_ARGS] = args
|
||||||
|
except ArgumentError as e:
|
||||||
|
state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
|
||||||
except ParserExit as e:
|
except ParserExit as e:
|
||||||
state[SHELL_ARGS] = e
|
state[SHELL_ARGS] = e
|
||||||
return True
|
return True
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def shell_command(
|
def shell_command(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import sys
|
||||||
from typing import Tuple, Union
|
from typing import Tuple, Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -202,7 +203,104 @@ async def test_command(app: App, cmds: Tuple[Tuple[str, ...]]):
|
|||||||
assert await dependent(state=state)
|
assert await dependent(state=state)
|
||||||
|
|
||||||
|
|
||||||
# TODO: shell command
|
@pytest.mark.asyncio
|
||||||
|
async def test_shell_command(app: App):
|
||||||
|
from nonebot.typing import T_State
|
||||||
|
from nonebot.exception import ParserExit
|
||||||
|
from nonebot.consts import CMD_KEY, PREFIX_KEY, SHELL_ARGS, SHELL_ARGV, CMD_ARG_KEY
|
||||||
|
from nonebot.rule import Namespace, ArgumentParser, ShellCommandRule, shell_command
|
||||||
|
|
||||||
|
state: T_State
|
||||||
|
CMD = ("test",)
|
||||||
|
Message = make_fake_message()
|
||||||
|
MessageSegment = Message.get_segment_class()
|
||||||
|
|
||||||
|
test_not_cmd = shell_command(CMD)
|
||||||
|
dependent = list(test_not_cmd.checkers)[0]
|
||||||
|
checker = dependent.call
|
||||||
|
assert isinstance(checker, ShellCommandRule)
|
||||||
|
message = Message()
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {PREFIX_KEY: {CMD_KEY: ("not",), CMD_ARG_KEY: message}}
|
||||||
|
assert not await dependent(event=event, state=state)
|
||||||
|
|
||||||
|
test_no_parser = shell_command(CMD)
|
||||||
|
dependent = list(test_no_parser.checkers)[0]
|
||||||
|
checker = dependent.call
|
||||||
|
assert isinstance(checker, ShellCommandRule)
|
||||||
|
message = Message()
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||||
|
assert await dependent(event=event, state=state)
|
||||||
|
assert state[SHELL_ARGV] == []
|
||||||
|
assert SHELL_ARGS not in state
|
||||||
|
|
||||||
|
parser = ArgumentParser("test")
|
||||||
|
parser.add_argument("-a", required=True)
|
||||||
|
|
||||||
|
test_simple_parser = shell_command(CMD, parser=parser)
|
||||||
|
dependent = list(test_simple_parser.checkers)[0]
|
||||||
|
checker = dependent.call
|
||||||
|
assert isinstance(checker, ShellCommandRule)
|
||||||
|
message = Message("-a 1")
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||||
|
assert await dependent(event=event, state=state)
|
||||||
|
assert state[SHELL_ARGV] == ["-a", "1"]
|
||||||
|
assert state[SHELL_ARGS] == Namespace(a="1")
|
||||||
|
|
||||||
|
test_parser_help = shell_command(CMD, parser=parser)
|
||||||
|
dependent = list(test_parser_help.checkers)[0]
|
||||||
|
checker = dependent.call
|
||||||
|
assert isinstance(checker, ShellCommandRule)
|
||||||
|
message = Message("-h")
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||||
|
assert await dependent(event=event, state=state)
|
||||||
|
assert state[SHELL_ARGV] == ["-h"]
|
||||||
|
assert isinstance(state[SHELL_ARGS], ParserExit)
|
||||||
|
assert state[SHELL_ARGS].status == 0
|
||||||
|
assert state[SHELL_ARGS].message == parser.format_help()
|
||||||
|
|
||||||
|
test_parser_error = shell_command(CMD, parser=parser)
|
||||||
|
dependent = list(test_parser_error.checkers)[0]
|
||||||
|
checker = dependent.call
|
||||||
|
assert isinstance(checker, ShellCommandRule)
|
||||||
|
message = Message()
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||||
|
assert await dependent(event=event, state=state)
|
||||||
|
assert state[SHELL_ARGV] == []
|
||||||
|
assert isinstance(state[SHELL_ARGS], ParserExit)
|
||||||
|
assert state[SHELL_ARGS].status != 0
|
||||||
|
|
||||||
|
test_message_parser = shell_command(CMD, parser=parser)
|
||||||
|
dependent = list(test_message_parser.checkers)[0]
|
||||||
|
checker = dependent.call
|
||||||
|
assert isinstance(checker, ShellCommandRule)
|
||||||
|
message = MessageSegment.text("-a") + MessageSegment.image("test")
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||||
|
assert await dependent(event=event, state=state)
|
||||||
|
assert state[SHELL_ARGV] == ["-a", MessageSegment.image("test")]
|
||||||
|
assert state[SHELL_ARGS] == Namespace(a=MessageSegment.image("test"))
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 9):
|
||||||
|
parser = ArgumentParser("test", exit_on_error=False)
|
||||||
|
parser.add_argument("-a", required=True)
|
||||||
|
|
||||||
|
test_not_exit = shell_command(CMD, parser=parser)
|
||||||
|
dependent = list(test_not_exit.checkers)[0]
|
||||||
|
checker = dependent.call
|
||||||
|
assert isinstance(checker, ShellCommandRule)
|
||||||
|
message = Message()
|
||||||
|
event = make_fake_event(_message=message)()
|
||||||
|
state = {PREFIX_KEY: {CMD_KEY: CMD, CMD_ARG_KEY: message}}
|
||||||
|
assert await dependent(event=event, state=state)
|
||||||
|
assert state[SHELL_ARGV] == []
|
||||||
|
assert isinstance(state[SHELL_ARGS], ParserExit)
|
||||||
|
assert state[SHELL_ARGS].status != 0
|
||||||
|
|
||||||
|
|
||||||
# TODO: regex
|
# TODO: regex
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ async def _(foo: str = CommandStart()): ...
|
|||||||
|
|
||||||
### ShellCommandArgs
|
### ShellCommandArgs
|
||||||
|
|
||||||
获取 shell 命令解析后的参数。
|
获取 shell 命令解析后的参数,支持 MessageSegment 富文本(如:图片)。
|
||||||
|
|
||||||
:::tip 提示
|
:::tip 提示
|
||||||
如果参数解析失败,则为 [`ParserExit`](../../api/exception.md#ParserExit) 异常,并携带错误码与错误信息。
|
如果参数解析失败,则为 [`ParserExit`](../../api/exception.md#ParserExit) 异常,并携带错误码与错误信息。
|
||||||
@ -288,21 +288,28 @@ async def _(foo: str = CommandStart()): ...
|
|||||||
```python {8,12}
|
```python {8,12}
|
||||||
from nonebot import on_shell_command
|
from nonebot import on_shell_command
|
||||||
from nonebot.params import ShellCommandArgs
|
from nonebot.params import ShellCommandArgs
|
||||||
|
from nonebot.rule import Namespace, ArgumentParser
|
||||||
|
|
||||||
|
parser = ArgumentParser("demo")
|
||||||
|
# parser.add_argument ...
|
||||||
matcher = on_shell_command("cmd", parser)
|
matcher = on_shell_command("cmd", parser)
|
||||||
|
|
||||||
# 解析失败
|
# 解析失败
|
||||||
@matcher.handle()
|
@matcher.handle()
|
||||||
async def _(foo: ParserExit = ShellCommandArgs()): ...
|
async def _(foo: ParserExit = ShellCommandArgs()):
|
||||||
|
if foo.status == 0:
|
||||||
|
foo.message # help message
|
||||||
|
else:
|
||||||
|
foo.message # error message
|
||||||
|
|
||||||
# 解析成功
|
# 解析成功
|
||||||
@matcher.handle()
|
@matcher.handle()
|
||||||
async def _(foo: Dict[str, Any] = ShellCommandArgs()): ...
|
async def _(foo: Namespace = ShellCommandArgs()): ...
|
||||||
```
|
```
|
||||||
|
|
||||||
### ShellCommandArgv
|
### ShellCommandArgv
|
||||||
|
|
||||||
获取 shell 命令解析前的参数列表。
|
获取 shell 命令解析前的参数列表,支持 MessageSegment 富文本(如:图片)。
|
||||||
|
|
||||||
```python {7}
|
```python {7}
|
||||||
from nonebot import on_shell_command
|
from nonebot import on_shell_command
|
||||||
@ -311,7 +318,7 @@ from nonebot.params import ShellCommandArgs
|
|||||||
matcher = on_shell_command("cmd")
|
matcher = on_shell_command("cmd")
|
||||||
|
|
||||||
@matcher.handle()
|
@matcher.handle()
|
||||||
async def _(foo: List[str] = ShellCommandArgv()): ...
|
async def _(foo: List[Union[str, MessageSegment]] = ShellCommandArgv()): ...
|
||||||
```
|
```
|
||||||
|
|
||||||
### RegexMatched
|
### RegexMatched
|
||||||
|
Loading…
Reference in New Issue
Block a user