support escape for text parameter parsing (#1897)

This commit is contained in:
Marcia Sun 2022-02-22 18:37:24 +08:00 committed by GitHub
parent 5a1059b0b3
commit 96e943c33f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 191 additions and 6 deletions

View File

@ -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()) }

View File

@ -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

View File

@ -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`]。