Repository: nonebot/nonebot2

Issue:
Bug: ParserExit.message 重复叠加问题
**描述问题：**

使用 ShellCommand 时，若 ArgumentParser 参数解析错误，通过 handler 接收到的 ParserExit 所带 message 会重复叠加上次的错误输出。

**如何复现？**

```py
# 来自文档的 demo，轻微改动
# https://v2.nonebot.dev/docs/tutorial/plugin/create-handler#shellcommandargs
from nonebot import on_shell_command
from nonebot.params import ShellCommandArgs
from nonebot.rule import Namespace, ArgumentParser, Rule
from nonebot.exception import ParserExit

parser = ArgumentParser("demo")
parser.add_argument("pos_arg")
matcher = on_shell_command("cmd", parser=parser)

@matcher.handle()
async def _(foo: ParserExit = ShellCommandArgs()):
    await matcher.finish(foo.message)

@matcher.handle()
async def _(foo: Namespace = ShellCommandArgs()):
    ...
```

向 bot 发送 3 次 `/cmd` 引发报错，回复分别为

```
usage: demo [-h] pos_arg
demo: error: the following arguments are required: pos_arg
```
```
usage: demo [-h] pos_arg
demo: error: the following arguments are required: pos_arg
usage: demo [-h] pos_arg
demo: error: the following arguments are required: pos_arg
```
```
usage: demo [-h] pos_arg
demo: error: the following arguments are required: pos_arg
usage: demo [-h] pos_arg
demo: error: the following arguments are required: pos_arg
usage: demo [-h] pos_arg
demo: error: the following arguments are required: pos_arg
```

**期望的结果**

每次 ParserExit 所带 message 只包含当次解析的错误信息。

猜测问题发生在

https://github.com/nonebot/nonebot2/blob/a758e6f06ec69dd64dabf10c7376b4a01914667d/nonebot/rule.py#L448-L455

在本地手动改为

```py
    def exit(self, status: int = 0, message: Optional[str] = None):
        if message:
            self._print_message(message)
        temp = getattr(self, "_message", None)
        setattr(self, "_message", "")
        raise ParserExit(status=status, message=temp)
```

后，bot 回复与预期一致。


**环境信息：**

 - OS: Windows / Linux
 - Python Version: 3.10
 - Nonebot Version: 2.0.0.rc-1 & 2.0.0.rc-2 均有

**协议端信息：**

go-cqhttp 1.0.0.rc3

**截图：**

![screenshot](https://user-images.githubusercontent.com/28810592/204105228-6984199d-3fc9-45e2-a71e-f4c1be9f2d13.png)



Discussion:

Resolving patch:
diff --git a/nonebot/rule.py b/nonebot/rule.py
--- a/nonebot/rule.py
+++ b/nonebot/rule.py
@@ -12,6 +12,7 @@
 import shlex
 from argparse import Action
 from argparse import ArgumentError
+from contextvars import ContextVar
 from itertools import chain, product
 from argparse import Namespace as Namespace
 from argparse import ArgumentParser as ArgParser
@@ -73,6 +74,8 @@
     "TRIE_VALUE", [("command_start", str), ("command", Tuple[str, ...])]
 )
 
+parser_message: ContextVar[str] = ContextVar("parser_message")
+
 
 class TrieRule:
     prefix: CharTrie = CharTrie()
@@ -446,13 +449,15 @@ def _parse_optional(
         )
 
     def _print_message(self, message: str, file: Optional[IO[str]] = None):
-        if message:
-            setattr(self, "_message", getattr(self, "_message", "") + message)
+        if (msg := parser_message.get(None)) is not None:
+            parser_message.set(msg + message)
+        else:
+            super()._print_message(message, file)
 
     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))
+        raise ParserExit(status=status, message=parser_message.get(None))
 
 
 class ShellCommandRule:
@@ -499,6 +504,7 @@ async def __call__(
         )
 
         if self.parser:
+            t = parser_message.set("")
             try:
                 args = self.parser.parse_args(state[SHELL_ARGV])
                 state[SHELL_ARGS] = args
@@ -506,6 +512,8 @@ async def __call__(
                 state[SHELL_ARGS] = ParserExit(status=2, message=str(e))
             except ParserExit as e:
                 state[SHELL_ARGS] = e
+            finally:
+                parser_message.reset(t)
         return True
 
 


Test patch:
diff --git a/tests/test_rule.py b/tests/test_rule.py
--- a/tests/test_rule.py
+++ b/tests/test_rule.py
@@ -296,6 +296,7 @@ async def test_shell_command(app: App):
     assert state[SHELL_ARGV] == []
     assert isinstance(state[SHELL_ARGS], ParserExit)
     assert state[SHELL_ARGS].status != 0
+    assert state[SHELL_ARGS].message.startswith(parser.format_usage() + "test: error:")
 
     test_message_parser = shell_command(CMD, parser=parser)
     dependent = list(test_message_parser.checkers)[0]