diff --git a/website/docs/best-practice/alconna/command.md b/website/docs/best-practice/alconna/command.md index e23a7bc1..0b2c7c10 100644 --- a/website/docs/best-practice/alconna/command.md +++ b/website/docs/best-practice/alconna/command.md @@ -3,114 +3,42 @@ sidebar_position: 2 description: Alconna 基本介绍 --- -# Alconna 命令解析 +# Alconna 本体 -[Alconna](https://github.com/ArcletProject/Alconna) 作为命令解析器, -是一个简单、灵活、高效的命令参数解析器,并且不局限于解析命令式字符串。 +[`Alconna`](https://github.com/ArcletProject/Alconna) 隶属于 `ArcletProject`,是一个简单、灵活、高效的命令参数解析器, 并且不局限于解析命令式字符串。 -特点包括: - -- 高效 -- 直观的命令组件创建方式 -- 强大的类型解析与类型转换功能 -- 自定义的帮助信息格式 -- 多语言支持 -- 易用的快捷命令创建与使用 -- 可创建命令补全会话,以实现多轮连续的补全提示 -- 可嵌套的多级子命令 -- 正则匹配支持 - -## 命令示范 +我们通过一个例子来讲解 **Alconna** 的核心 —— `Args`, `Subcommand`, `Option`: ```python -import sys -from io import StringIO +from arclet.alconna import Alconna, Args, Subcommand, Option -from arclet.alconna import Alconna, Args, Field, Option, CommandMeta, MultiVar, Arparma -from nepattern import AnyString alc = Alconna( - "exec", - Args["code", MultiVar(AnyString), Field(completion=lambda: "print(1+1)")] / "\n", - Option("纯文本"), - Option("无输出"), - Option("目标", Args["name", str, "res"]), - meta=CommandMeta("exec python code", example="exec\\nprint(1+1)"), -) - -alc.shortcut( - "echo", - {"command": "exec 纯文本\nprint(\\'{*}\\')"}, -) - -alc.shortcut( - "sin(\d+)", - {"command": "exec 纯文本\nimport math\nprint(math.sin({0}*math.pi/180))"}, -) - - -def exec_code(result: Arparma): - if result.find("纯文本"): - codes = list(result.code) - else: - codes = str(result.origin).split("\n")[1:] - output = result.query[str]("目标.name", "res") - if not codes: - return "" - lcs = {} - _stdout = StringIO() - _to = sys.stdout - sys.stdout = _stdout - try: - exec( - "def rc(__out: str):\n " - + " ".join(_code + "\n" for _code in codes) - + " return locals().get(__out)", - {**globals(), **locals()}, - lcs, - ) - code_res = lcs["rc"](output) - sys.stdout = _to - if result.find("无输出"): - return "" - if code_res is not None: - return f"{output}: {code_res}" - _out = _stdout.getvalue() - return f"输出: {_out}" - except Exception as e: - sys.stdout = _to - return str(e) - finally: - sys.stdout = _to - -print(exec_code(alc.parse("echo 1234"))) -print(exec_code(alc.parse("sin30"))) -print( - exec_code( - alc.parse( -"""\ -exec -print( - exec_code( - alc.parse( - "exec\\n" - "import sys;print(sys.version)" - ) - ) -) -""" - ) + "pip", + Subcommand( + "install", + Args["package", str], + Option("-r|--requirement", Args["file", str]), + Option("-i|--index-url", Args["url", str]), ) ) + +res = alc.parse("pip install nonebot2 -i URL") + +print(res) +# matched=True, header_match=(origin='pip' result='pip' matched=True groups={}), subcommands={'install': (value=Ellipsis args={'package': 'nonebot2'} options={'index-url': (value=None args={'url': 'URL'})} subcommands={})}, other_args={'package': 'nonebot2', 'url': 'URL'} + +print(res.all_matched_args) +# {'package': 'nonebot2', 'url': 'URL'} ``` -## 命令编写 +这段代码通过`Alconna`创捷了一个接受主命令名为`pip`, 子命令为`install`且子命令接受一个 **Args** 参数`package`和二个 **Option** 参数`-r`和`-i`的命令参数解析器, 通过`parse`方法返回解析结果 **Arparma** 的实例。 + +## 组成 ### 命令头 -命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 `!help` 中的 `!` 与 `help`。 - -在 Alconna 中,你可以传入多种类型的命令头,例如: +命令头是指命令的前缀 (Prefix) 与命令名 (Command) 的组合,例如 !help 中的 ! 与 help。 | 前缀 | 命令名 | 匹配内容 | 说明 | | :--------------------------: | :--------: | :---------------------------------------------------------: | :--------------: | @@ -127,23 +55,20 @@ print( | [123, "foo"] | "bar" | `[123, "bar"]` 或 `"foobar"` 或 `["foo", "bar"]` | 混合头 | | [(int, "foo"), (456, "bar")] | "baz" | `[123, "foobaz"]` 或 `[456, "foobaz"]` 或 `[456, "barbaz"]` | 对头 | -其中 +无前缀的类型头:此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型, 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。同时,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。 -- 元素头:只会匹配对应的值,例如 `[123, 456]` 只会匹配 `123` 或 `456`,不会匹配 `789`。 -- 纯文字头:只会匹配对应的字符串,例如 `["foo", "bar"]` 只会匹配 `"foo"` 或 `"bar"`,不会匹配 `"baz"`。 -- 正则头:`re:xxx` 会将 `xxx` 转为正则表达式,然后匹配对应的字符串,例如 `re:\d{2}` 只会匹配 `"12"` 或 `"34"`,不会匹配 `"foo"`。 - **正则只在命令名上生效,命令前缀中的正则会被转义**。 -- 类型头:只会匹配对应的类型,例如 `[int, bool]` 只会匹配 `123` 或 `True`,不会匹配 `"foo"`。 - - 无前缀的类型头:此时会将传入的值尝试转为 BasePattern,例如 `int` 会转为 `nepattern.INTEGER`。此时命令头会匹配对应的类型, - 例如 `int` 会匹配 `123` 或 `"456"`,但不会匹配 `"foo"`。同时,Alconna 会将命令头匹配到的值转为对应的类型,例如 `int` 会将 `"123"` 转为 `123`。 -- 表达式头:只会匹配对应的表达式,例如 `[nepattern.NUMBER]` 只会匹配 `123` 或 `123.456`,不会匹配 `"foo"`。 -- 混合头: +:::tip -除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,那就是 Bracket Header。 +**正则只在命令名上生效,命令前缀中的正则会被转义** + +::: + +除了通过传入 `re:xxx` 来使用正则表达式外,Alconna 还提供了一种更加简洁的方式来使用正则表达式,那就是 Bracket Header: ```python from alconna import Alconna + alc = Alconna(".rd{roll:int}") assert alc.parse(".rd123").header["roll"] == 123 ``` @@ -152,362 +77,185 @@ Bracket Header 类似 python 里的 f-string 写法,通过 "{}" 声明匹配 "{}" 中的内容为 "name:type or pat": -- "{}", "{:}": 占位符,等价于 "(.+)" -- "{foo}": 等价于 "(?P<foo>.+)" -- "{:\d+}": 等价于 "(\d+)" -- "{foo:int}": 等价于 "(?P<foo>\d+)",其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式 +- "{}", "{:}" ⇔ "(.+)", 占位符 +- "{foo}" ⇔ "(?P<foo>.+)" +- "{:\d+}" ⇔ "(\d+)" +- "{foo:int}" ⇔ "(?P<foo>\d+)",其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式 -### 组件 +### 参数声明(Args) -我们可以看到主要的两大组件:`Option` 与 `Subcommand`。 +`Args` 是用于声明命令参数的组件, 可以通过以下几种方式构造 **Args** : -`Option` 可以传入一组 `alias`,如 `Option("--foo|-F|FOO|f")` 或 `Option("--foo", alias=["-F"])` +- `Args[key, var, default][key1, var1, default1][...]` +- `Args[(key, var, default)]` +- `Args.key[var, default]` -传入别名后,Option 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f",则命令名称为 "--foo"。 +其中,key **一定**是字符串,而 var 一般为参数的类型,default 为具体的值或者 **arclet.alconna.args.Field** -:::tip 特别提醒!!! +其与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。 -在 Alconna 中 Option 的名字或别名**没有要求**必须在前面写上 `-` +#### key -::: +`key` 的作用是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 -`Subcommand` 则可以传入自己的 **Option** 与 **Subcommand**。 +其有三种为 Args 注解的标识符:  `?`、`/`、 `!`, 标识符与 key 之间建议以 `;` 分隔: -```python -from arclet.alconna import Alconna, Option, Subcommand +- `!` 标识符表示该处传入的参数应**不是**规定的类型,或**不在**指定的值中。 +- `?` 标识符表示该参数为**可选**参数,会在无参数匹配时跳过。 +- `/` 标识符表示该参数的类型注解需要隐藏。 -alc = Alconna( - "command_name", - Option("opt1"), - Option("--opt2"), - Subcommand( - "sub1", - Option("sub1_opt1"), - Option("SO2"), - Subcommand( - "sub1_sub1" - ) - ), - Subcommand( - "sub2" - ) -) -``` - -他们拥有如下共同参数: - -- `help_text`: 传入该组件的帮助信息 -- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name) -- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换 - 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等,所以可以这么编写: - - ```python - Alconna("test", Option("qux", Args["a", int], requires=["foo", "bar", "baz"])) - ``` - -- `default`: 默认值,在该组件未被解析时使用使用该值替换。 - - 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: - - ```python - from arclet.alconna import Option, OptionResult - - opt1 = Option("--foo", default=False) - opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) - ``` - -### 选项操作 - -`Option` 可以特别设置传入一类 `Action`,作为解析操作 - -`Action` 分为三类: - -- `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值 -- `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中 - - 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性 - -- `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同 - - 当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。 - -`Alconna` 提供了预制的几类 `action`: - -- `store`,`store_value`,`store_true`,`store_false` -- `append`,`append_value` -- `count` - -### 参数声明 - -`Args` 是用于声明命令参数的组件。 - -`Args` 是参数解析的基础组件,构造方法形如 `Args["foo", str]["bar", int]["baz", bool, False]`, -与函数签名类似,但是允许含有默认值的参数在前;同时支持 keyword-only 参数不依照构造顺序传入 (但是仍需要在非 keyword-only 参数之后)。 - -`Args` 中的 `name` 是用以标记解析出来的参数并存放于 **Arparma** 中,以方便用户调用。 - -其有三种为 Args 注解的标识符: `?`、`/` 与 `!`。标识符与 key 之间建议以 `;` 分隔: - -- `!` 标识符表示该处传入的参数应不是规定的类型,或不在指定的值中。 -- `?` 标识符表示该参数为可选参数,会在无参数匹配时跳过。 -- `/` 标识符表示该参数的类型注解需要隐藏。 - -另外,对于参数的注释也可以标记在 `name` 中,其与 name 或者标识符 以 `#` 分割: - -`foo#这是注释;?` 或 `foo?#这是注释` +另外,对于参数的注释也可以标记在 `key` 中,其与 key 或者标识符 以 `#` 分割: +`foo#这是注释;?` 或 `foo?#这是注释` :::tip -`Args` 中的 `name` 在实际命令中并不需要传入(keyword 参数除外): +`Args` 中的 `key` 在实际命令中并不需要传入(keyword 参数除外): ```python from arclet.alconna import Alconna, Args + alc = Alconna("test", Args["foo", str]) -alc.parse("test --foo abc") # 错误 -alc.parse("test abc") # 正确 +alc.parse("test --foo abc") # 错误 +alc.parse("test abc") # 正确 ``` -若需要 `test --foo abc`,你应该使用 `Option`: +若需要 `test --foo abc`,你应该使用 `Option`: ```python from arclet.alconna import Alconna, Args, Option + alc = Alconna("test", Option("--foo", Args["foo", str])) ``` ::: -`Args` 的参数类型表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例。 +#### var + +var 负责命令参数的**类型检查**与**类型转化** + +`Args` 的`var`表面上看需要传入一个 `type`,但实际上它需要的是一个 `nepattern.BasePattern` 的实例: ```python from arclet.alconna import Args from nepattern import BasePattern + # 表示 foo 参数需要匹配一个 @number 样式的字符串 args = Args["foo", BasePattern("@\d+")] ``` -示例中传入的 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]`。 +示例中可以传入 `str` 是因为 `str` 已经注册在了 `nepattern.global_patterns` 中,因此会替换为 `nepattern.global_patterns[str]` -默认支持的类型有: +`nepattern.global_patterns`默认支持的类型有: - `str`: 匹配任意字符串 - `int`: 匹配整数 - `float`: 匹配浮点数 -- `bool`: 匹配 `True` 与 `False` 以及他们小写形式 -- `hex`: 匹配 `0x` 开头的十六进制字符串 +- `bool`: 匹配 `True` 与 `False` 以及他们小写形式 +- `hex`: 匹配 `0x` 开头的十六进制字符串 - `url`: 匹配网址 -- `email`: 匹配 `xxxx@xxx` 的字符串 -- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串 -- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串 -- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串 -- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳 +- `email`: 匹配 `xxxx@xxx` 的字符串 +- `ipv4`: 匹配 `xxx.xxx.xxx.xxx` 的字符串 +- `list`: 匹配类似 `["foo","bar","baz"]` 的字符串 +- `dict`: 匹配类似 `{"foo":"bar","baz":"qux"}` 的字符串 +- `datetime`: 传入一个 `datetime` 支持的格式字符串,或时间戳 - `Any`: 匹配任意类型 -- `AnyString`: 匹配任意类型,转为 `str` -- `Number`: 匹配 `int` 与 `float`,转为 `int` +- `AnyString`: 匹配任意类型,转为 `str` +- `Number`: 匹配 `int` 与 `float`,转为 `int` 同时可以使用 typing 中的类型: - `Literal[X]`: 匹配其中的任意一个值 - `Union[X, Y]`: 匹配其中的任意一个类型 -- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值 -- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型 -- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型 +- `Optional[xxx]`: 会自动将默认值设为 `None`,并在解析失败时使用默认值 +- `List[X]`: 匹配一个列表,其中的元素为 `X` 类型 +- `Dict[X, Y]`: 匹配一个字典,其中的 key 为 `X` 类型,value 为 `Y` 类型 - ... :::tip 几类特殊的传入标记: -- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) -- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换) +- `"foo"`: 匹配字符串 "foo" (若没有某个 `BasePattern` 与之关联) +- `RawStr("foo")`: 匹配字符串 "foo" (不会被 `BasePattern` 替换) - `"foo|bar|baz"`: 匹配 "foo" 或 "bar" 或 "baz" - `[foo, bar, Baz, ...]`: 匹配其中的任意一个值或类型 -- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值 -- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0] -- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象 +- `Callable[[X], Y]`: 匹配一个参数为 `X` 类型的值,并返回通过该函数调用得到的 `Y` 类型的值 +- `"re:xxx"`: 匹配一个正则表达式 `xxx`,会返回 Match[0] +- `"rep:xxx"`: 匹配一个正则表达式 `xxx`,会返回 `re.Match` 对象 - `{foo: bar, baz: qux}`: 匹配字典中的任意一个键, 并返回对应的值 (特殊的键 ... 会匹配任意的值) - ... ::: -`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。 -同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 +`MultiVar` 则是一个特殊的标注,用于告知解析器该参数可以接受多个值,其构造方法形如 `MultiVar(str)`。 同样的还有 `KeyWordVar`,其构造方法形如 `KeyWordVar(str)`,用于告知解析器该参数为一个 keyword-only 参数。 :::tip -`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))` +`MultiVar` 与 `KeyWordVar` 组合时,代表该参数为一个可接受多个 key-value 的参数,其构造方法形如 `MultiVar(KeyWordVar(str))` -`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值。 +`MultiVar` 与 `KeyWordVar` 也可以传入 `default` 参数,用于指定默认值 -`MultiVar` 不能在 `KeyWordVar` 之后传入。 +`MultiVar` 不能在 `KeyWordVar` 之后传入 ::: -### 紧凑命令 +### Option 和 Subcommand -`Alconna`,`Option` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: +`Option` 可以传入一组 `alias`,如 `Option("--foo|-F|--FOO|-f")` 或 `Option("--foo", alias=["-F"]` + +传入别名后,`option` 会选择其中长度最长的作为选项名称。若传入为 "--foo|-f",则命令名称为 "--foo" + +:::tip 特别提醒!!! + +在 Alconna 中 Option 的名字或别名**没有要求**必须在前面写上 `-` + +::: + +`Subcommand` 可以传入自己的 **Option** 与 **Subcommand** + +他们拥有如下共同参数: + +- `help_text`: 传入该组件的帮助信息 +- `dest`: 被指定为解析完成时标注匹配结果的标识符,不传入时默认为选项或子命令的名称 (name) +- `requires`: 一段指定顺序的字符串列表,作为唯一的前置序列与命令嵌套替换 + 对于命令 `test foo bar baz qux ` 来讲,因为`foo bar baz` 仅需要判断是否相等, 所以可以这么编写: ```python -from arclet.alconna import Alconna, Option, CommandMeta, Args - -alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) - -assert alc.parse("test123 BARabc").matched +Alconna("test", Option("qux", Args.a[int], requires=["foo", "bar", "baz"])) ``` -这使得我们可以实现如下命令: +- `default`: 默认值,在该组件未被解析时使用使用该值替换。 + 特别的,使用 `OptionResult` 或 `SubcomanndResult` 可以设置包括参数字典在内的默认值: ```python ->>> from arclet.alconna import Alconna, Option, Args, append ->>> alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) ->>> alc.parse("gcc -Fabc -Fdef -Fxyz").query[list[str]]("flag.content") -['abc', 'def', 'xyz'] +from arclet.alconna import Option, OptionResult + +opt1 = Option("--foo", default=False) +opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1})) ``` -当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: +`Option` 可以特别设置传入一类 `Action`,作为解析操作 -```python ->>> from arclet.alconna import Alconna, Option, Args, count ->>> alc = Alconna("pp", Option("--verbose|-v", action=count, default=0)) ->>> alc.parse("pp -vvv").query[int]("verbose.value") -3 -``` +`Action` 分为三类: -## 命令特性 +- `store`: 无 Args 时, 仅存储一个值, 默认为 Ellipsis; 有 Args 时, 后续的解析结果会覆盖之前的值 +- `append`: 无 Args 时, 将多个值存为列表, 默认为 Ellipsis; 有 Args 时, 每个解析结果会追加到列表中, 当存在默认值并且不为列表时, 会自动将默认值变成列表, 以保证追加的正确性 +- `count`: 无 Args 时, 计数器加一; 有 Args 时, 表现与 STORE 相同, 当存在默认值并且不为数字时, 会自动将默认值变成 1, 以保证计数器的正确性。 -### 配置 +`Alconna` 提供了预制的几类 `Action`: -`arclet.alconna.Namespace` 表示某一命名空间下的默认配置: +- `store`(默认),`store_value`,`store_true`,`store_false` +- `append`,`append_value` +- `count` -```python -from arclet.alconna import config, namespace, Namespace -from arclet.alconna.tools import ShellTextFormatter +### Arparma +`Alconna.parse` 会返回由 **Arparma** 承载的解析结果 -np = Namespace("foo", prefixes=["/"]) # 创建 Namespace 对象,并进行初始配置 - -with namespace("bar") as np1: - np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 - np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter - np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 - -config.namespaces["foo"] = np # 将命名空间挂载到 config 上 -``` - -同时也提供了默认命名空间配置与修改方法: - -```python -from arclet.alconna import config, namespace, Namespace - - -config.default_namespace.prefixes = [...] # 直接修改默认配置 - -np = Namespace("xxx", prefixes=[...]) -config.default_namespace = np # 更换默认的命名空间 - -with namespace(config.default_namespace.name) as np: - np.prefixes = [...] -``` - -### 半自动补全 - -半自动补全为用户提供了推荐后续输入的功能。 - -补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) - -```python -from arclet.alconna import Alconna, Args, Option - -alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") -alc.parse("test ?") - -''' -output - -以下是建议的输入: -* -* --help -* -h -* -sct -* --shortcut -* foo -* bar -''' -``` - -### 快捷指令 - -快捷指令顾名思义,可以为基础指令创建便捷的触发方式 - -一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除); - -```python ->>> from arclet.alconna import Alconna, Args ->>> alc = Alconna("setu", Args["count", int]) ->>> alc.shortcut("涩图(\d+)张", {"args": ["{0}"]}) -'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功' ->>> alc.parse("涩图3张").query("count") -3 -``` - -`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置 - -```python -class ShortcutArgs(TypedDict): - """快捷指令参数""" - - command: NotRequired[DataCollection[Any]] - """快捷指令的命令""" - args: NotRequired[list[Any]] - """快捷指令的附带参数""" - fuzzy: NotRequired[bool] - """是否允许命令后随参数""" - prefix: NotRequired[bool] - """是否调用时保留指令前缀""" -``` - -当 `fuzzy` 为 False 时,传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 - -快捷指令允许三类特殊的 placeholder: - -- `{%X}`: 如 `setu {%0}`,表示此处必须填入快捷指令后随的第 X 个参数。 - - 例如,若快捷指令为 `涩图`,配置为 `{"command": "setu {%0}"}`,则指令 `涩图 1` 相当于 `setu 1` - -- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 -- `{X}`: 表示此处填入可能的正则匹配的组: - - 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 - - 若 `command` 中存储匹配组 `(?P...)`,则 `{X}` 表示名字为 X 的匹配结果 - -除此之外,通过内置选项 `--shortcut` 可以动态操作快捷指令。 - -例如: - -- `cmd --shortcut ` 来增加一个快捷指令 -- `cmd --shortcut list` 来列出当前指令的所有快捷指令 -- `cmd --shortcut delete key` 来删除一个快捷指令 - -### 使用模糊匹配 - -模糊匹配通过在 Alconna 中设置其 CommandMeta 开启。 - -模糊匹配会应用在任意需要进行名称判断的地方,如**命令名称**,**选项名称**和**参数名称**(如指定需要传入参数名称)。 - -```python -from arclet.alconna import Alconna, CommandMeta - -alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True)) -alc.parse("test_fuzy") -# output: test_fuzy is not matched. Do you mean "test_fuzzy"? -``` - -## 解析结果 - -`Alconna.parse` 会返回由 **Arparma** 承载的解析结果。 - -`Arpamar` 会有如下参数: +`Arparma` 会有如下参数: - 调试类 @@ -524,47 +272,286 @@ alc.parse("test_fuzy") - other_args: 除主参数外的其他解析结果 - all_matched_args: 所有 Args 的解析结果 -`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回 +`Arparma` 同时提供了便捷的查询方法 `query[type]()`,会根据传入的 `path` 查找参数并返回 -`path` 支持如下: +`path` 支持如下: -- `main_args`,`options`,...: 返回对应的属性 +- `main_args`, `options`, ...: 返回对应的属性 - `args`: 返回 all_matched_args -- `main_args.xxx`,`options.xxx`,...: 返回字典中 `xxx`键对应的值 -- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值 -- `options.foo`,`foo`: 返回选项 `foo` 的解析结果 (OptionResult) -- `options.foo.value`,`foo.value`: 返回选项 `foo` 的解析值 -- `options.foo.args`,`foo.args`: 返回选项 `foo` 的解析参数字典 -- `options.foo.args.bar`,`foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值 - ... +- `main_args.xxx`, `options.xxx`, ...: 返回字典中 `xxx`键对应的值 +- `args.xxx`: 返回 all_matched_args 中 `xxx`键对应的值 +- `options.foo`, `foo`: 返回选项 `foo` 的解析结果 (OptionResult) +- `options.foo.value`, `foo.value`: 返回选项 `foo` 的解析值 +- `options.foo.args`, `foo.args`: 返回选项 `foo` 的解析参数字典 +- `options.foo.args.bar`, `foo.bar`: 返回选项 `foo` 的参数字典中 `bar` 键对应的值 ... -同样,`Arparma["foo.bar"]` 的表现与 `query()` 一致 +## 命名空间配置 + +命名空间配置 (以下简称命名空间) 相当于`Alconna`的设置,`Alconna`默认使用“Alconna”命名空间,命名空间有以下几个属性: + +- name: 命名空间名称 +- prefixes: 默认前缀配置 +- separators: 默认分隔符配置 +- formatter_type: 默认格式化器类型 +- fuzzy_match: 默认是否开启模糊匹配 +- raise_exception: 默认是否抛出异常 +- builtin_option_name: 默认的内置选项名称(--help, --shortcut, --comp) +- enable_message_cache: 默认是否启用消息缓存 +- compact: 默认是否开启紧凑模式 +- strict: 命令是否严格匹配 +- ... + +### 新建命名空间并替换 + +```python +from arclet.alconna import Alconna, namespace, Namespace, Subcommand, Args, config + + +ns = Namespace("foo", prefixes=["/"])  # 创建 "foo"命名空间配置, 它要求创建的Alconna的主命令前缀必须是/ + +alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=ns) # 在创建Alconna时候传入命名空间以替换默认命名空间 + +# 可以通过with方式创建命名空间 +with namespace("bar") as np1: + np1.prefixes = ["!"] # 以上下文管理器方式配置命名空间,此时配置会自动注入上下文内创建的命令 + np1.formatter_type = ShellTextFormatter # 设置此命名空间下的命令的 formatter 默认为 ShellTextFormatter + np1.builtin_option_name["help"] = {"帮助", "-h"} # 设置此命名空间下的命令的帮助选项名称 + +# 你还可以使用config来管理所有命名空间并切换至任意命名空间 +config.namespaces["foo"] = ns # 将命名空间挂载到 config 上 + +alc = Alconna("pip", Subcommand("install", Args["package", str]), namespace=config.namespaces["foo"]) # 也是同样可以切换到"foo"命名空间 +``` + +### 修改默认的命名空间 + +```python +from arclet.alconna import config, namespace, Namespace + + +config.default_namespace.prefixes = [...] # 直接修改默认配置 + +np = Namespace("xxx", prefixes=[...]) +config.default_namespace = np # 更换默认的命名空间 + +with namespace(config.default_namespace.name) as np: + np.prefixes = [...] +``` + +## 快捷指令 + +快捷命令可以做到标识一段命令, 并且传递参数给原命令 + +一般情况下你可以通过 `Alconna.shortcut` 进行快捷指令操作 (创建,删除) + +`shortcut` 的第一个参数为快捷指令名称,第二个参数为 `ShortcutArgs`,作为快捷指令的配置: + +```python +class ShortcutArgs(TypedDict): + """快捷指令参数""" + + command: NotRequired[DataCollection[Any]] + """快捷指令的命令""" + args: NotRequired[list[Any]] + """快捷指令的附带参数""" + fuzzy: NotRequired[bool] + """是否允许命令后随参数""" + prefix: NotRequired[bool] + """是否调用时保留指令前缀""" +``` + +### args的使用 + +```python +from arclet.alconna import Alconna, Args + + +alc = Alconna("setu", Args["count", int]) + +alc.shortcut("涩图(\d+)张", {"args": ["{0}"]}) +# 'Alconna::setu 的快捷指令: "涩图(\\d+)张" 添加成功' + +alc.parse("涩图3张").query("count") +# 3 +``` + +### command的使用 + +```python +from arclet.alconna import Alconna, Args + + +alc = Alconna("eval", Args["content", str]) + +alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) +# 'Alconna::eval 的快捷指令: "echo" 添加成功' + +alc.shortcut("echo", delete=True) # 删除快捷指令 +# 'Alconna::eval 的快捷指令: "echo" 删除成功' + +@alc.bind() # 绑定一个命令执行器, 若匹配成功则会传入参数, 自动执行命令执行器 +def cb(content: str): + eval(content, {}, {}) + +alc.parse('eval print(\\"hello world\\")') +# hello world + +alc.parse("echo hello world!") +# hello world! +``` + +当 `fuzzy` 为 False 时,第一个例子中传入 `"涩图1张 abc"` 之类的快捷指令将视为解析失败 + +快捷指令允许三类特殊的 placeholder: + +- `{%X}`: 如 `setu {%0}`,表示此处填入快捷指令后随的第 X 个参数。 + +例如,若快捷指令为 `涩图`, 配置为 `{"command": "setu {%0}"}`, 则指令 `涩图 1` 相当于 `setu 1` + +- `{*}`: 表示此处填入所有后随参数,并且可以通过 `{*X}` 的方式指定组合参数之间的分隔符。 + +- `{X}`: 表示此处填入可能的正则匹配的组: + +- 若 `command` 中存在匹配组 `(xxx)`,则 `{X}` 表示第 X 个匹配组的内容 +- 若 `command` 中存储匹配组 `(?P...)`, 则 `{X}` 表示 **名字** 为 X 的匹配结果 + +除此之外, 通过 **Alconna** 内置选项 `--shortcut` 可以动态操作快捷指令 + +例如: + +- `cmd --shortcut ` 来增加一个快捷指令 +- `cmd --shortcut list` 来列出当前指令的所有快捷指令 +- `cmd --shortcut delete key` 来删除一个快捷指令 + +```python +from arclet.alconna import Alconna, Args + + +alc = Alconna("eval", Args["content", str]) + +alc.shortcut("echo", {"command": "eval print(\\'{*}\\')"}) + +alc.parse("eval --shortcut list") +# 'echo' +``` + +## 紧凑命令 + +`Alconna`,  `Option` 与 `Subcommand` 可以设置 `compact=True` 使得解析命令时允许名称与后随参数之间没有分隔: + +```python +from arclet.alconna import Alconna, Option, CommandMeta, Args + + +alc = Alconna("test", Args["foo", int], Option("BAR", Args["baz", str], compact=True), meta=CommandMeta(compact=True)) + +assert alc.parse("test123 BARabc").matched +``` + +这使得我们可以实现如下命令: + +```python +from arclet.alconna import Alconna, Option, Args, append + + +alc = Alconna("gcc", Option("--flag|-F", Args["content", str], action=append, compact=True)) +print(alc.parse("gcc -Fabc -Fdef -Fxyz").query[list]("flag.content")) +# ['abc', 'def', 'xyz'] +``` + +当 `Option` 的 `action` 为 `count` 时,其自动支持 `compact` 特性: + +```python +from arclet.alconna import Alconna, Option, count + + +alc = Alconna("pp", Option("--verbose|-v", action=count, default=0)) +print(alc.parse("pp -vvv").query[int]("verbose.value")) +# 3 +``` + +## 模糊匹配 + +模糊匹配通过在 Alconna 中设置其 CommandMeta 开启 + +模糊匹配会应用在任意需要进行名称判断的地方,如 **命令名称**,**选项名称** 和 **参数名称** (如指定需要传入参数名称)。 + +```python +from arclet.alconna import Alconna, CommandMeta + + +alc = Alconna("test_fuzzy", meta=CommandMeta(fuzzy_match=True)) + +alc.parse("test_fuzy") +# test_fuzy is not matched. Do you mean "test_fuzzy"? +``` + +## 半自动补全 + +半自动补全为用户提供了推荐后续输入的功能 + +补全默认通过 `--comp` 或 `-cp` 或 `?` 触发:(命名空间配置可修改名称) + +```python +from arclet.alconna import Alconna, Args, Option + + +alc = Alconna("test", Args["abc", int]) + Option("foo") + Option("bar") +alc.parse("test --comp") + +''' +output + +以下是建议的输入: +* +* --help +* -h +* -sct +* --shortcut +* foo +* bar +''' +``` ## Duplication -**Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace**,经测试表现良好(好耶)。 +**Duplication** 用来提供更好的自动补全,类似于 **ArgParse** 的 **Namespace** -普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分, +普通情况下使用,需要利用到 **ArgsStub**、**OptionStub** 和 **SubcommandStub** 三个部分 -以 pip 为例,其对应的 Duplication 应如下构造: +以pip为例,其对应的 Duplication 应如下构造: ```python -from arclet.alconna import OptionResult, Duplication, SubcommandStub +from arclet.alconna import Alconna, Args, Option, OptionResult, Duplication, SubcommandStub, Subcommand, count + class MyDup(Duplication): - verbose: OptionResult - install: SubcommandStub # 选项与子命令对应的stub的变量名必须与其名字相同 -``` +    verbose: OptionResult +    install: SubcommandStub -并在解析时传入 Duplication: -```python +alc = Alconna( +    "pip", +    Subcommand( +        "install", +        Args["package", str], +        Option("-r|--requirement", Args["file", str]), +        Option("-i|--index-url", Args["url", str]), +    ), +    Option("-v|--version"), +    Option("-v|--verbose", action=count), +) + +res = alc.parse("pip -v install ...") # 不使用duplication获得的提示较少 +print(res.query("install")) +# (value=Ellipsis args={'package': '...'} options={} subcommands={}) + result = alc.parse("pip -v install ...", duplication=MyDup) ->>> type(result) - +print(result.install) +# SubcommandStub(_origin=Subcommand('install', args=Args('package': str)), _value=Ellipsis, available=True, args=ArgsStub(_origin=Args('package': str), _value={'package': '...'}, available=True), dest='install', options=[OptionStub(_origin=Option('requirement', args=Args('file': str)), _value=None, available=False, args=ArgsStub(_origin=Args('file': str), _value={}, available=False), dest='requirement', aliases=['r', 'requirement'], name='requirement'), OptionStub(_origin=Option('index-url', args=Args('url': str)), _value=None, available=False, args=ArgsStub(_origin=Args('url': str), _value={}, available=False), dest='index-url', aliases=['index-url', 'i'], name='index-url')], subcommands=[], name='install') ``` -**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: +**Duplication** 也可以如 **Namespace** 一样直接标明参数名称和类型: ```python from typing import Optional diff --git a/website/docs/best-practice/alconna/config.md b/website/docs/best-practice/alconna/config.md index 75135eb1..eee4a57a 100644 --- a/website/docs/best-practice/alconna/config.md +++ b/website/docs/best-practice/alconna/config.md @@ -10,7 +10,7 @@ description: 配置项 - **类型**: `bool` - **默认值**: `False` -是否全局启用输出信息自动发送,不启用则会在触特殊内置选项后仍然将解析结果传递至响应器。 +是否全局启用输出信息自动发送,不启用则会在触发特殊内置选项后仍然将解析结果传递至响应器。 ## alconna_use_command_start @@ -38,11 +38,11 @@ description: 配置项 - **类型**: `bool` - **默认值**: `False` -是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符 +是否读取 Nonebot 的配置项 `COMMAND_SEP` 来作为全局的 Alconna 命令分隔符。 ## alconna_global_extensions - **类型**: `List[str]` - **默认值**: `[]` -全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension` +全局加载的扩展,路径以 . 分隔,如 `foo.bar.baz:DemoExtension`。 diff --git a/website/docs/best-practice/alconna/matcher.md b/website/docs/best-practice/alconna/matcher.md index c05c7a6c..8ed13ac9 100644 --- a/website/docs/best-practice/alconna/matcher.md +++ b/website/docs/best-practice/alconna/matcher.md @@ -3,15 +3,16 @@ sidebar_position: 3 description: 响应规则的使用 --- -# Alconna 响应规则 +# Alconna 插件 -以下为一个使用示例: +展示: ```python from nonebot_plugin_alconna.adapters.onebot12 import Image from nonebot_plugin_alconna import At, on_alconna from arclet.alconna import Args, Option, Alconna, Arparma, MultiVar, Subcommand + alc = Alconna( ["/", "!"], "role-group", @@ -41,64 +42,103 @@ async def _(result: Arparma): ## 响应器使用 -`on_alconna` 的所有参数如下: - -- `command: Alconna | str`: Alconna 命令 -- `skip_for_unmatch: bool = True`: 是否在命令不匹配时跳过该响应 -- `auto_send_output: bool = False`: 是否自动发送输出信息并跳过响应 -- `aliases: set[str | tuple[str, ...]] | None = None`: 命令别名,作用类似于 `on_command` 中的 aliases -- `comp_config: CompConfig | None = None`: 补全会话配置,不传入则不启用补全会话 -- `extensions: list[type[Extension] | Extension] | None = None`: 需要加载的匹配扩展,可以是扩展类或扩展实例 -- `exclude_ext: list[type[Extension] | str] | None = None`: 需要排除的匹配扩展,可以是扩展类或扩展的 id -- `use_origin: bool = False`: 是否使用未经 to_me 等处理过的消息 -- `use_cmd_start: bool = False`: 是否使用 COMMAND_START 作为命令前缀 -- `use_cmd_sep: bool = False`: 是否使用 COMMAND_SEP 作为命令分隔符 - -`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher`,其拓展了如下方法: - -- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理 -- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换 -- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`,为 `got_path` 的特化版本 -- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path` -- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher` -- `.got`, `send`, `reject`, ...: 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt - -用例: +本插件基于 **Alconna**,为 **Nonebot** 提供了一类新的事件响应器辅助函数 `on_alconna`: ```python +def on_alconna( + command: Alconna | str, + skip_for_unmatch: bool = True, + auto_send_output: bool = False, + aliases: set[str | tuple[str, ...]] | None = None, + comp_config: CompConfig | None = None, + extensions: list[type[Extension] | Extension] | None = None, + exclude_ext: list[type[Extension] | str] | None = None, + use_origin: bool = False, + use_cmd_start: bool = False, + use_cmd_sep: bool = False, + **kwargs, + ..., +): +``` + +- `command`: Alconna 命令或字符串,字符串将通过 `AlconnaFormat` 转换为 Alconna 命令 +- `skip_for_unmatch`: 是否在命令不匹配时跳过该响应 +- `auto_send_output`: 是否自动发送输出信息并跳过响应 +- `aliases`: 命令别名, 作用类似于 `on_command` 中的 aliases +- `comp_config`: 补全会话配置, 不传入则不启用补全会话 +- `extensions`: 需要加载的匹配扩展, 可以是扩展类或扩展实例 +- `exclude_ext`: 需要排除的匹配扩展, 可以是扩展类或扩展的id +- `use_origin`: 是否使用未经 to_me 等处理过的消息 +- `use_cmd_start`: 是否使用 COMMAND_START 作为命令前缀 +- `use_cmd_sep`: 是否使用 COMMAND_SEP 作为命令分隔符 + +`on_alconna` 返回的是 `Matcher` 的子类 `AlconnaMatcher` ,其拓展了如下方法: + +- `.assign(path, value, or_not)`: 用于对包含多个选项/子命令的命令的分派处理(具体请看[条件控制](./matcher.md#条件控制)) +- `.got_path(path, prompt, middleware)`: 在 `got` 方法的基础上,会以 path 对应的参数为准,读取传入 message 的最后一个消息段并验证转换 +- `.set_path_arg(key, value)`, `.get_path_arg(key)`: 类似 `set_arg` 和 `got_arg`,为 `got_path` 的特化版本 +- `.reject_path(path[, prompt, fallback])`: 类似于 `reject_arg`,对应 `got_path` +- `.dispatch`: 同样的分派处理,但是是类似 `CommandGroup` 一样返回新的 `AlconnaMatcher` +- `.got`, `send`, `reject`, ... : 拓展了 prompt 类型,即支持使用 `UniMessage` 作为 prompt + +实例: + +```python +from nonebot import require +require("nonebot_plugin_alconna") + from arclet.alconna import Alconna, Option, Args from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match, UniMessage -login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) +login = on_alconna(Alconna(["/"], "login", Args["password?", str], Option("-r|--recall"))) # 这里["/"]指命令前缀必须是/ + +# /login -r 触发 @login.assign("recall") async def login_exit(): - await login.finish("已退出") +    await login.finish("已退出") +# /login xxx 触发 @login.assign("password") async def login_handle(pw: Match[str] = AlconnaMatch("password")): - if pw.available: - login.set_path_arg("password", pw.result) +    if pw.available: +        login.set_path_arg("password", pw.result) +# /login 触发 @login.got_path("password", prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请输入密码")) async def login_got(password: str): - assert password - await login.send("登录成功") + assert password + await login.send("登录成功") ``` ## 依赖注入 -`Alconna` 的解析结果会放入 `Arparma` 类中,或用户指定的 `Duplication` 类。 +本插件提供了一系列依赖注入函数,便于在响应函数中获取解析结果: -而 `AlconnaMatcher` 在原有 Matcher 的基础上拓展了允许的依赖注入: +- `AlconnaResult`: `CommandResult` 类型的依赖注入函数 +- `AlconnaMatches`: `Arparma` 类型的依赖注入函数 +- `AlconnaDuplication`: `Duplication` 类型的依赖注入函数 +- `AlconnaMatch`: `Match` 类型的依赖注入函数 +- `AlconnaQuery`: `Query` 类型的依赖注入函数 + +同时,基于 [`Annotated` 支持](https://github.com/nonebot/nonebot2/pull/1832),添加了两类注解: + +- `AlcMatches`:同 `AlconnaMatches` +- `AlcResult`:同 `AlconnaResult` + +可以看到,本插件提供了几类额外的模型: + +- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段 +- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值 +- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果 + +**Alconna** 默认依赖注入的目标参数皆不需要使用依赖注入函数, 该效果对于 `AlconnaMatcher.got_path` 下的 Arg 同样有效: ```python -@cmd.handle() async def handle( result: CommandResult, arp: Arparma, - dup: Duplication, # 基类或子类都可以 - ext: Extension, + dup: Duplication, source: Alconna, abc: str, # 类似 Match, 但是若匹配结果不存在对应字段则跳过该 handler foo: Match[str], @@ -107,12 +147,6 @@ async def handle( ... ``` -可以看到,本插件提供了几类额外的模型: - -- `CommandResult`: 解析结果,包括了源命令 `source: Alconna` ,解析结果 `result: Arparma`,以及可能的输出信息 `output: str | None` 字段 -- `Match`: 匹配项,表示参数是否存在于 `all_matched_args` 内,可用 `Match.available` 判断是否匹配,`Match.result` 获取匹配的值 -- `Query`: 查询项,表示参数是否可由 `Arparma.query` 查询并获得结果,可用 `Query.available` 判断是否查询成功,`Query.result` 获取查询结果 - :::note 如果你更喜欢 Depends 式的依赖注入,`nonebot_plugin_alconna` 同时提供了一系列的依赖注入函数,他们包括: @@ -130,14 +164,19 @@ async def handle( 实例: ```python -... from nonebot import require require("nonebot_plugin_alconna") -... -from nonebot_plugin_alconna import on_alconna, Match, Query, AlconnaQuery +from nonebot_plugin_alconna import ( + on_alconna, + Match, + Query, + AlconnaMatch, + AlcResult +) from arclet.alconna import Alconna, Args, Option, Arparma + test = on_alconna( Alconna( "test", @@ -147,41 +186,34 @@ test = on_alconna( auto_send_output=True ) - @test.handle() async def handle_test1(result: AlcResult): await test.send(f"matched: {result.matched}") await test.send(f"maybe output: {result.output}") @test.handle() -async def handle_test2(bar: Match[int]): +async def handle_test2(result: Arparma): + await test.send(f"head result: {result.header_result}") + await test.send(f"args: {result.all_matched_args}") + +@test.handle() +async def handle_test3(bar: Match[int] = AlconnaMatch("bar")): if bar.available: await test.send(f"foo={bar.result}") @test.handle() -async def handle_test3(qux: Query[bool] = AlconnaQuery("baz.qux", False)): +async def handle_test4(qux: Query[bool] = Query("baz.qux", False)): if qux.available: await test.send(f"baz.qux={qux.result}") ``` -## 消息段标注 +## 多平台适配 -示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。 +本插件提供了通用消息段标注, 通用消息段序列, 使插件使用者可以忽略平台之间字段的差异 -适配器下的消息段标注会匹配特定的 `MessageSegment`: +响应器使用示例中使用了消息段标注,其中 `At` 属于通用标注,而 `Image` 属于 `onebot12` 适配器下的标注。 -而通用标注与适配器标注的区别在于,通用标注会匹配多个适配器中相似类型的消息段,并返回 -`nonebot_plugin_alconna.uniseg` 中定义的 [`Segment` 模型](./utils.md#通用消息段) - -例如: - -```python -... -ats = result.query[tuple[At, ...]]("add.member.target") -group.extend(member.target for member in ats) -``` - -这样插件使用者就不用考虑平台之间字段的差异 +具体介绍和使用请查看 [通用信息组件](./uniseg.mdx#通用消息段) 本插件为以下适配器提供了专门的适配器标注: @@ -219,6 +251,7 @@ require("nonebot_plugin_alconna") from arclet.alconna import Alconna, Subcommand, Option, Args from nonebot_plugin_alconna import on_alconna, CommandResult + pip = Alconna( "pip", Subcommand( @@ -262,6 +295,7 @@ async def update(arp: CommandResult): ```python from nonebot_plugin_alconna import At, Match, UniMessage, on_alconna + test_cmd = on_alconna(Alconna("test", Args["target?", Union[str, At]])) @test_cmd.handle() @@ -305,15 +339,101 @@ async def tt(target: Union[str, At]): ::: +## 响应器创建装饰 + +本插件提供了一个 `funcommand` 装饰器, 其用于将一个接受任意参数, 返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器: + +```python +from nonebot_plugin_alconna import funcommand + + +@funcommand() +async def echo(msg: str): + return msg +``` + +其等同于: + +```python +from arclet.alconna import Alconna, Args +from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match + + +echo = on_alconna(Alconna("echo", Args["msg", str])) + +@echo.handle() +async def echo_exit(msg: Match[str] = AlconnaMatch("msg")): + await echo.finish(msg.result) + +``` + +## 类Koishi构造器 + +本插件提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`, 以类似 `Koishi` 中注册命令的方式来构建一个 **AlconnaMatcher** : + +```python +from nonebot_plugin_alconna import Command, Arparma + + +book = ( + Command("book", "测试") + .option("writer", "-w ") + .option("writer", "--anonymous", {"id": 0}) + .usage("book [-w | --anonymous]") + .shortcut("测试", {"args": ["--anonymous"]}) + .build() +) + +@book.handle() +async def _(arp: Arparma): + await book.send(str(arp.options)) +``` + +甚至,你可以设置 `action` 来设定响应行为: + +```python +book = ( + Command("book", "测试") + .option("writer", "-w ") + .option("writer", "--anonymous", {"id": 0}) + .usage("book [-w | --anonymous]") + .shortcut("测试", {"args": ["--anonymous"]}) + .action(lambda options: str(options)) # 会自动通过 bot.send 发送 + .build() +) +``` + +## 返回值回调 + +在 `AlconnaMatch`,`AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数: + +```python +from nonebot_plugin_alconna import image_fetch + + +mask_cmd = on_alconna( + Alconna("search", Args["img?", Image]), +) + + +@mask_cmd.handle() +async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)): + result = await search_img(img.result) + await matcher.send(result.content) +``` + +其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。 + ## 匹配拓展 -本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为。 +本插件提供了一个 `Extension` 类,其用于自定义 AlconnaMatcher 的部分行为 -例如 `LLMExtension` (仅举例): +例如 `LLMExtension` (仅举例): ```python from nonebot_plugin_alconna import Extension, Alconna, on_alconna, Interface + class LLMExtension(Extension): @property def priority(self) -> int: @@ -347,9 +467,9 @@ matcher = on_alconna( ... ``` -那么使用了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息,同时可以在响应器中为所有 `llm` 参数注入模型变量 +那么添加了 `LLMExtension` 的响应器便能接受任何能通过 llm 翻译为具体命令的自然语言消息,同时可以在响应器中为所有 `llm` 参数注入模型变量。 -目前 `Extension` 的功能有: +目前 `Extension` 的功能有: - `validate`: 对于事件的来源适配器或 bot 选择是否接受响应 - `output_converter`: 输出信息的自定义转换方法 @@ -360,15 +480,15 @@ matcher = on_alconna( - `send_wrapper`: 对发送的消息 (Message 或 UniMessage) 的额外处理 - `before_catch`: 自定义依赖注入的绑定确认函数 - `catch`: 自定义依赖注入处理函数 -- `post_init`: 响应器创建后对命令对象的额外除了 +- `post_init`: 响应器创建后对命令对象的额外处理 -例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash 指令并注册,且将收到的指令交互事件转为指令供命令解析: +例如内置的 `DiscordSlashExtension`,其可自动将 Alconna 对象翻译成 slash 指令并注册,且将收到的指令交互事件转为指令供命令解析: ```python from nonebot_plugin_alconna import Match, on_alconna - from nonebot_plugin_alconna.adapters.discord import DiscordSlashExtension + alc = Alconna( ["/"], "permission", diff --git a/website/docs/best-practice/alconna/uniseg.mdx b/website/docs/best-practice/alconna/uniseg.mdx index 3a76a034..ba44af25 100644 --- a/website/docs/best-practice/alconna/uniseg.mdx +++ b/website/docs/best-practice/alconna/uniseg.mdx @@ -12,6 +12,9 @@ import TabItem from "@theme/TabItem"; ## 通用消息段 +适配器下的消息段标注会匹配适配器特定的 `MessageSegment`, 而通用消息段与适配器消息段的区别在于: +通用消息段会匹配多个适配器中相似类型的消息段,并返回 `uniseg` 模块中定义的 [`Segment` 模型](https://nonebot.dev/docs/next/best-practice/alconna/utils#%E9%80%9A%E7%94%A8%E6%B6%88%E6%81%AF%E6%AE%B5), 以达到**跨平台接收消息**的作用。 + `nonebot-plugin-alconna.uniseg` 提供了类似 `MessageSegment` 的通用消息段,并可在 `Alconna` 下直接标注使用: ```python @@ -80,13 +83,13 @@ class Other(Segment): """其他 Segment""" ``` -来自各自适配器的消息序列都会经过这些通用消息段对应的标注转换,以达到跨平台接收消息的作用 +此类消息段通过 `UniMessage.export` 可以转为特定的 `MessageSegment` ## 通用消息序列 `nonebot-plugin-alconna.uniseg` 同时提供了一个类似于 `Message` 的 `UniMessage` 类型,其元素为经过通用标注转换后的通用消息段。 -你可以用如下方式获取 `UniMessage` : +你可以用如下方式获取 `UniMessage`: @@ -96,6 +99,7 @@ class Other(Segment): ```python from nonebot_plugin_alconna.uniseg import UniMsg, At, Reply + matcher = on_xxx(...) @matcher.handle() @@ -117,6 +121,7 @@ async def _(msg: UniMsg): from nonebot import Message, EventMessage from nonebot_plugin_alconna.uniseg import UniMessage + matcher = on_xxx(...) @matcher.handle() @@ -129,12 +134,13 @@ async def _(message: Message = EventMessage()): 不仅如此,你还可以通过 `UniMessage` 的 `export` 与 `send` 方法来**跨平台发送消息**。 -`UniMessage.export` 会通过传入的 `bot: Bot` 参数,或上下文中的 `Bot` 对象读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列 +`UniMessage.export` 会通过传入的 `bot: Bot` 参数,或上下文中的 `Bot` 对象读取适配器信息,并使用对应的生成方法把通用消息转为适配器对应的消息序列: ```python from nonebot import Bot, on_command from nonebot_plugin_alconna.uniseg import Image, UniMessage + test = on_command("test") @test.handle() @@ -149,6 +155,7 @@ from arclet.alconna import Alconna, Args from nonebot_plugin_alconna import Match, AlconnaMatcher, on_alconna from nonebot_plugin_alconna.uniseg import At, UniMessage + test_cmd = on_alconna(Alconna("test", Args["target?", At])) @test_cmd.handle() @@ -167,6 +174,7 @@ async def tt(target: At): from nonebot import Bot, on_command from nonebot_plugin_alconna.uniseg import UniMessage + test = on_command("test") @test.handle() @@ -188,6 +196,7 @@ async def handle(): ```python from nonebot_plugin_alconna.uniseg import UniMessage, At + msg = UniMessage("Hello") msg1 = UniMessage(At("user", "124")) msg2 = UniMessage(["Hello", At("user", "124")]) @@ -198,33 +207,96 @@ msg2 = UniMessage(["Hello", At("user", "124")]) ```python from nonebot_plugin_alconna.uniseg import UniMessage, At, Image + msg = UniMessage.text("Hello").at("124").image(path="/path/to/img") assert msg == UniMessage( ["Hello", At("user", "124"), Image(path="/path/to/img")] ) ``` -### 获取消息纯文本 +### 拼接消息 -类似于 `Message.extract_plain_text()`,用于获取通用消息的纯文本。 +`str`、`UniMessage`、`Segment` 对象之间可以直接相加,相加均会返回一个新的 `UniMessage` 对象: ```python -from nonebot_plugin_alconna.uniseg import UniMessage, At -# 提取消息纯文本字符串 -assert UniMessage( - [At("user", "1234"), "text"] -).extract_plain_text() == "text" +# 消息序列与消息段相加 +UniMessage("text") + Text("text") +# 消息序列与字符串相加 +UniMessage([Text("text")]) + "text" +# 消息序列与消息序列相加 +UniMessage("text") + UniMessage([Text("text")]) +# 字符串与消息序列相加 +"text" + UniMessage([Text("text")]) +# 消息段与消息段相加 +Text("text") + Text("text") +# 消息段与字符串相加 +Text("text") + "text" +# 消息段与消息序列相加 +Text("text") + UniMessage([Text("text")]) +# 字符串与消息段相加 +"text" + Text("text") ``` -### 遍历 - -通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段。 +如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加: ```python -for segment in message: # type: Segment - ... +msg = UniMessage([Text("text")]) +# 自加 +msg += "text" +msg += Text("text") +msg += UniMessage([Text("text")]) +# 附加 +msg.append(Text("text")) +# 扩展 +msg.extend([Text("text")]) ``` +### 使用消息模板 + +`UniMessage.template` 同样类似于 `Message.template`,可以用于格式化消息,大体用法参考 [消息模板](../../tutorial/message#使用消息模板)。 + +这里额外说明 `UniMessage.template` 的拓展控制符 + +相比 `Message`,UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行 + +以 At(...) 为例: + +```python title=使用通用消息段的拓展控制符 +>>> from nonebot_plugin_alconna.uniseg import UniMessage +>>> UniMessage.template("{:At(user, target)}").format(target="123") +UniMessage(At("user", "123")) +>>> UniMessage.template("{:At(type=user, target=id)}").format(id="123") +UniMessage(At("user", "123")) +>>> UniMessage.template("{:At(type=user, target=123)}").format() +UniMessage(At("user", "123")) +``` + +而在 `AlconnaMatcher` 中,{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能: + +```python title=在AlconnaMatcher中使用通用消息段的拓展控制符 +from arclet.alconna import Alconna, Args +from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna + + +test_cmd = on_alconna(Alconna("test", Args["target?", At])) + +@test_cmd.handle() +async def tt_h(matcher: AlconnaMatcher, target: Match[At]): + if target.available: + matcher.set_path_arg("target", target.result) + +@test_cmd.got_path( + "target", + prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标") +) +async def tt(): + await test_cmd.send( + UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}") + ) +``` + +另外也有 `$message_id` 与 `$target` 两个特殊值。 + ### 检查消息段 我们可以通过 `in` 运算符或消息序列的 `has` 方法来: @@ -236,7 +308,7 @@ At("user", "1234") in message At in message ``` -我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段。 +我们还可以使用 `only` 方法来检查消息中是否仅包含指定的消息段: ```python # 是否都为 "test" @@ -245,13 +317,37 @@ message.only("test") message.only(Text) ``` +### 获取消息纯文本 + +类似于 `Message.extract_plain_text()`,用于获取通用消息的纯文本: + +```python +from nonebot_plugin_alconna.uniseg import UniMessage, At + + +# 提取消息纯文本字符串 +assert UniMessage( + [At("user", "1234"), "text"] +).extract_plain_text() == "text" +``` + +### 遍历 + +通用消息序列继承自 `List[Segment]` ,因此可以使用 `for` 循环遍历消息段: + +```python +for segment in message: # type: Segment + ... +``` + ### 过滤、索引与切片 -消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片。 +消息序列对列表的索引与切片进行了增强,在原有列表 `int` 索引与 `slice` 切片的基础上,支持 `type` 过滤索引与切片: ```python from nonebot_plugin_alconna.uniseg import UniMessage, At, Text, Reply + message = UniMessage( [ Reply(...), @@ -272,14 +368,14 @@ message[At, 0] == At("user", "1234") message[Text, 0:2] == UniMessage([Text("text1"), Text("text2")]) ``` -我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤。 +我们也可以通过消息序列的 `include`、`exclude` 方法进行类型过滤: ```python message.include(Text, At) message.exclude(Reply) ``` -同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段。 +同样的,消息序列对列表的 `index`、`count` 方法也进行了增强,可以用于索引指定类型的消息段: ```python # 指定类型首个消息段索引 @@ -288,96 +384,14 @@ message.index(Text) == 1 message.count(Text) == 2 ``` -此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段。 +此外,消息序列添加了一个 `get` 方法,可以用于获取指定类型指定个数的消息段: ```python # 获取指定类型指定个数的消息段 message.get(Text, 1) == UniMessage([Text("test1")]) ``` -### 拼接消息 - -`str`、`UniMessage`、`Segment` 对象之间可以直接相加,相加均会返回一个新的 `UniMessage` 对象。 - -```python -# 消息序列与消息段相加 -UniMessage("text") + Text("text") -# 消息序列与字符串相加 -UniMessage([Text("text")]) + "text" -# 消息序列与消息序列相加 -UniMessage("text") + UniMessage([Text("text")]) -# 字符串与消息序列相加 -"text" + UniMessage([Text("text")]) -# 消息段与消息段相加 -Text("text") + Text("text") -# 消息段与字符串相加 -Text("text") + "text" -# 消息段与消息序列相加 -Text("text") + UniMessage([Text("text")]) -# 字符串与消息段相加 -"text" + Text("text") -``` - -如果需要在当前消息序列后直接拼接新的消息段,可以使用 `Message.append`、`Message.extend` 方法,或者使用自加。 - -```python -msg = UniMessage([Text("text")]) -# 自加 -msg += "text" -msg += Text("text") -msg += UniMessage([Text("text")]) -# 附加 -msg.append(Text("text")) -# 扩展 -msg.extend([Text("text")]) -``` - -### 使用消息模板 - -`UniMessage.template` 同样类似于 `Message.template`,可以用于格式化消息。大体用法参考 [消息模板](../../tutorial/message#使用消息模板)。 - -这里额外说明 `UniMessage.template` 的拓展控制符 - -相比 `Message`,UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行 - -以 At(...) 为例: - -```python title=使用通用消息段的拓展控制符 ->>> from nonebot_plugin_alconna.uniseg import UniMessage ->>> UniMessage.template("{:At(user, target)}").format(target="123") -UniMessage(At("user", "123")) ->>> UniMessage.template("{:At(type=user, target=id)}").format(id="123") -UniMessage(At("user", "123")) ->>> UniMessage.template("{:At(type=user, target=123)}").format() -UniMessage(At("user", "123")) -``` - -而在 `AlconnaMatcher` 中,{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能 - -```python title=在AlconnaMatcher中使用通用消息段的拓展控制符 -from arclet.alconna import Alconna, Args -from nonebot_plugin_alconna import At, Match, UniMessage, AlconnaMatcher, on_alconna - -test_cmd = on_alconna(Alconna("test", Args["target?", At])) - -@test_cmd.handle() -async def tt_h(matcher: AlconnaMatcher, target: Match[At]): - if target.available: - matcher.set_path_arg("target", target.result) - -@test_cmd.got_path( - "target", - prompt=UniMessage.template("{:At(user, $event.get_user_id())} 请确认目标") -) -async def tt(): - await test_cmd.send( - UniMessage.template("{:At(user, $event.get_user_id())} 已确认目标为 {target}") - ) -``` - -另外也有 `$message_id` 与 `$target` 两个特殊值。 - -## 消息发送 +### 消息发送 前面提到,通用消息可用 `UniMessage.send` 发送自身: @@ -398,6 +412,7 @@ async def send( from nonebot import Event, Bot from nonebot_plugin_alconna.uniseg import UniMessage, Target + matcher = on_xxx(...) @matcher.handle() diff --git a/website/docs/best-practice/alconna/utils.md b/website/docs/best-practice/alconna/utils.md deleted file mode 100644 index 9cf5b190..00000000 --- a/website/docs/best-practice/alconna/utils.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -sidebar_position: 6 -description: 杂项 ---- - -# 杂项 - -## 特殊装饰器 - -`nonebot_plugin_alconna` 提供 了一个 `funcommand` 装饰器,其用于将一个接受任意参数, -返回 `str` 或 `Message` 或 `MessageSegment` 的函数转换为命令响应器。 - -```python -from nonebot_plugin_alconna import funcommand - -@funcommand() -async def echo(msg: str): - return msg -``` - -其等同于 - -```python -from arclet.alconna import Alconna, Args -from nonebot_plugin_alconna import on_alconna, AlconnaMatch, Match - -echo = on_alconna(Alconna("echo", Args["msg", str])) - -@echo.handle() -async def echo_exit(msg: Match[str] = AlconnaMatch("msg")): - await echo.finish(msg.result) -``` - -## 特殊构造器 - -`nonebot_plugin_alconna` 提供了一个 `Command` 构造器,其基于 `arclet.alconna.tools` 中的 `AlconnaString`, -以类似 `Koishi` 中注册命令的方式来构建一个 AlconnaMatcher: - -```python -from nonebot_plugin_alconna import Command, Arparma - -book = ( - Command("book", "测试") - .option("writer", "-w ") - .option("writer", "--anonymous", {"id": 0}) - .usage("book [-w | --anonymous]") - .shortcut("测试", {"args": ["--anonymous"]}) - .build() -) - -@book.handle() -async def _(arp: Arparma): - await book.send(str(arp.options)) -``` - -甚至,你可以设置 `action` 来设定响应行为: - -```python -book = ( - Command("book", "测试") - .option("writer", "-w ") - .option("writer", "--anonymous", {"id": 0}) - .usage("book [-w | --anonymous]") - .shortcut("测试", {"args": ["--anonymous"]}) - .action(lambda options: str(options)) # 会自动通过 bot.send 发送 - .build() -) -``` - -## 中间件 - -在 `AlconnaMatch`, `AlconnaQuery` 或 `got_path` 中,你可以使用 `middleware` 参数来传入一个对返回值进行处理的函数, - -```python {1, 9} -from nonebot_plugin_alconna import image_fetch - -mask_cmd = on_alconna( - Alconna("search", Args["img?", Image]), -) - - -@mask_cmd.handle() -async def mask_h(matcher: AlconnaMatcher, img: Match[bytes] = AlconnaMatch("img", image_fetch)): - result = await search_img(img.result) - await matcher.send(result.content) -``` - -其中,`image_fetch` 是一个中间件,其接受一个 `Image` 对象,并提取图片的二进制数据返回。