mirror of
https://github.com/mamoe/mirai.git
synced 2025-01-01 10:12:51 +08:00
support escape for text parameter parsing (#1897)
This commit is contained in:
parent
5a1059b0b3
commit
96e943c33f
@ -28,12 +28,36 @@ import kotlin.reflect.full.*
|
||||
|
||||
internal val ILLEGAL_SUB_NAME_CHARS = "\\/!@#$%^&*()_+-={}[];':\",.<>?`~".toCharArray()
|
||||
|
||||
internal val WORD_DIVIDER = Regex("""(?<!\\)\s+""") // 分词的空格
|
||||
internal val ESCAPE_PATTERN = Regex("""\\(.)""") // 转义符和被转义的符号
|
||||
internal val STOP_PARSE_INDICATOR = Regex("""(?<!\\)\s+--\s+|^--\s+""") // 暂停解析符号,前后都有空格,或前为字符串开始
|
||||
internal val QUOTE_BEGIN = Regex("""(?<!\\)\s"|^\s"|^"""") // 左双引号,前有一个未转义的空格,或前为字符串开始
|
||||
internal val QUOTE_END = Regex("""(?<!\\)"\s|(?<!\\)"$""") // 右双引号,前无转义符,后为空格或字符串结束
|
||||
|
||||
// 提取引号包围的原始文本,用空格分割剩余部分,并还原被转义的符号
|
||||
internal fun String.parseParameterTextToList(): List<CharSequence> =
|
||||
if (this.isBlank()) emptyList()
|
||||
else this.split(QUOTE_BEGIN, 2)
|
||||
.flatMapIndexed { index, part -> if (index > 0) part.split(QUOTE_END, 2) else listOf(part) }
|
||||
.let { unquotedTexts ->
|
||||
return if (unquotedTexts.size == 3)
|
||||
unquotedTexts[0].parseParameterTextToList() + listOf(unquotedTexts[1]) +
|
||||
unquotedTexts[2].parseParameterTextToList()
|
||||
else
|
||||
this@parseParameterTextToList.split(WORD_DIVIDER).filterNot { it.isBlank() }
|
||||
.map { it.replace(ESCAPE_PATTERN, "$1") }.toList()
|
||||
}
|
||||
|
||||
internal fun CharSequence.flattenCommandTextParts(addCallback: (CharSequence) -> Unit) =
|
||||
this.split(STOP_PARSE_INDICATOR, 2).filterNot { it.isBlank() }.let { stopParseParts ->
|
||||
stopParseParts.getOrNull(0)?.parseParameterTextToList()?.forEach(addCallback)
|
||||
stopParseParts.getOrNull(1)?.let(addCallback)
|
||||
}
|
||||
|
||||
internal fun Any.flattenCommandComponents(): MessageChain = buildMessageChain {
|
||||
when (this@flattenCommandComponents) {
|
||||
is PlainText -> this@flattenCommandComponents.content.splitToSequence(' ').filterNot { it.isBlank() }
|
||||
.forEach { +PlainText(it) }
|
||||
is CharSequence -> this@flattenCommandComponents.splitToSequence(' ').filterNot { it.isBlank() }
|
||||
.forEach { +PlainText(it) }
|
||||
is PlainText -> this@flattenCommandComponents.content.flattenCommandTextParts { +PlainText(it) }
|
||||
is CharSequence -> this@flattenCommandComponents.flattenCommandTextParts { +PlainText(it) }
|
||||
is SingleMessage -> add(this@flattenCommandComponents)
|
||||
is Array<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) }
|
||||
is Iterable<*> -> this@flattenCommandComponents.forEach { if (it != null) addAll(it.flattenCommandComponents()) }
|
||||
|
@ -322,6 +322,123 @@ internal class InstanceTestCommand : AbstractConsoleInstanceTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsEscape() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "esc ape").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test esc\\ ape")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsQuote() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "esc ape").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test \"esc ape\"")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsQuoteReject() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "es\"c", "ape\"").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test es\"c ape\"")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsQuoteEscape() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "\"esc", "ape\"").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test \\\"esc ape\"")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsMultipleQuotes() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "esc ape", "1 2").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test \"esc ape\" \"1 2\"")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsMisplacedQuote() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "esc ape", "1\"", "\"2").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test \"esc ape\" 1\" \"2 ")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsQuoteSpaceEscape() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test \"esc", "ape\"").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test\\ \"esc ape\"")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsStopParse() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "esc ape ").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test -- esc ape ")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsStopParse2() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "esc ape test\\12\"\"3").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test -- esc ape test\\12\"\"3")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsStopParseReject() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test--", "esc", "ape").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test-- esc ape ")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsStopParseEscape() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "--", "esc", "ape").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test \\-- esc ape")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsStopParseEscape2() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", " --", "esc", "ape").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test \\ -- esc ape")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleArgsStopParseQuote() = runBlocking {
|
||||
TestSimpleCommand.withRegistration {
|
||||
assertEquals(arrayOf("test", "--", "esc", "ape").joinToString(), withTesting<MessageChain> {
|
||||
assertSuccess(TestSimpleCommand.execute(sender, PlainText("test \"--\" esc ape")))
|
||||
}.joinToString())
|
||||
}
|
||||
}
|
||||
|
||||
val image = Image("/f8f1ab55-bf8e-4236-b55e-955848d7069f")
|
||||
|
||||
@Test
|
||||
|
@ -151,8 +151,7 @@ object MySimpleCommand : SimpleCommand(
|
||||
6. `"Hello"` 也会按照 4~5 的步骤转换为 `String` 类型的参数
|
||||
7. 解析完成的参数被传入 `handle`
|
||||
|
||||
|
||||
## [`CompositeCommand`]
|
||||
### [`CompositeCommand`]
|
||||
|
||||
[`CompositeCommand`] 的参数解析与 [`SimpleCommand`] 一样,只是多了「子指令」概念。
|
||||
|
||||
@ -207,6 +206,51 @@ object MyCompositeCommand : CompositeCommand(
|
||||
}
|
||||
```
|
||||
|
||||
### 文本参数的转义
|
||||
|
||||
不同的参数默认用空格分隔。有时用户希望在文字参数中包含空格本身,参数解析器可以接受三种表示方法。
|
||||
|
||||
以上文中定义的 `MySimpleCommand` 为例:
|
||||
|
||||
#### 英文双引号
|
||||
|
||||
表示将其中内容作为一个参数,可以包括空格。
|
||||
|
||||
例如:用户输入 `/tell 123456 "Hello world!"` ,`message` 会收到 `Hello world!`。
|
||||
|
||||
注意:双引号仅在参数的首尾部生效。例如,用户输入 `/tell 123456 He"llo world!"`,`message` 只会得到 `He"llo`。
|
||||
|
||||
#### 转义符
|
||||
|
||||
即英文反斜杠 `\`。表示忽略之后一个字符的特殊含义,仅看作字符本身。
|
||||
|
||||
例如:
|
||||
|
||||
- 用户输入 `/tell 123456 Hello\ world!`,`message` 得到 `Hello world!`;
|
||||
- 用户输入 `/tell 123456 \"Hello world!\"`,`message` 得到 `"Hello`。
|
||||
|
||||
#### 暂停解析标志
|
||||
|
||||
即连续两个英文短横线 `--`。表示从此处开始,到**这段文字内容**结束为止,都作为一个完整参数。
|
||||
|
||||
例如:
|
||||
|
||||
- 用户输入 `/tell 123456 -- Hello:::test\12""3`,`message` 得到 `Hello:::test\12""3`(`:` 表示空格);
|
||||
- 用户输入 `/tell 123456 -- Hello @全体成员 test1 test2`,那么暂停解析的作用范围到 `@` 为止,之后的 `test1` 和 `test2` 是不同的参数。
|
||||
- 用户输入 `/tell 123456 \-- Hello` 或 `/tell 123456 "--" Hello`,这不是暂停解析标志,`message` 得到 `--` 本身。
|
||||
|
||||
注意:
|
||||
|
||||
`--` 的前后都应与其他参数有间隔,否则不认为这是暂停解析标志。
|
||||
|
||||
例如,用户输入 `/tell 123456--Hello world!`,`123456--Hello` 会被试图转换为 `User` 并出错。即使转换成功,`message` 也只会得到 `world!`。
|
||||
|
||||
### 非文本参数的转义
|
||||
|
||||
有时可能需要只用一个参数来接受各种消息内容,例如用户可以在 `/tell 123456` 后接图片、表情等,它们都是 `message` 的一部分。
|
||||
|
||||
对于这种定义方式,Mirai Console 的支持尚待实现,目前可以使用 [`RawCommand`] 替代。
|
||||
|
||||
### 选择 [`RawCommand`], [`SimpleCommand`] 或 [`CompositeCommand`]
|
||||
|
||||
若需要不限长度的,自由的参数列表,使用 [`RawCommand`]。
|
||||
|
Loading…
Reference in New Issue
Block a user