Add support for escaped whitespace to arg parser

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/109/head
trivernis 4 years ago
parent aad4143e8a
commit ec7af49267
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -9,7 +9,6 @@ import org.bstats.bukkit.Metrics
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
import org.bukkit.scheduler.BukkitTask import org.bukkit.scheduler.BukkitTask
import org.dynmap.DynmapAPI import org.dynmap.DynmapAPI
import java.lang.IllegalStateException
open class Chunkmaster : JavaPlugin() { open class Chunkmaster : JavaPlugin() {
lateinit var sqliteManager: SqliteManager lateinit var sqliteManager: SqliteManager

@ -44,7 +44,7 @@ class CmdCancel(private val chunkmaster: Chunkmaster) : Subcommand {
} }
} else { } else {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ID_REQUIRED")); sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ID_REQUIRED"))
false false
} }
} }

@ -113,7 +113,7 @@ class CmdGenerate(private val chunkmaster: Chunkmaster) : Subcommand {
) )
true true
} else if (world == null) { } else if (world == null) {
sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName)); sender.sendMessage(chunkmaster.langManager.getLocalized("WORLD_NOT_FOUND", worldName))
false false
} else { } else {
sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ALREADY_EXISTS", worldName)) sender.sendMessage(chunkmaster.langManager.getLocalized("TASK_ALREADY_EXISTS", worldName))

@ -7,7 +7,7 @@ import org.bukkit.command.CommandSender
import org.bukkit.entity.Player import org.bukkit.entity.Player
class CmdGetCenter(private val chunkmaster: Chunkmaster) : Subcommand { class CmdGetCenter(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "getCenter"; override val name = "getCenter"
override fun onTabComplete( override fun onTabComplete(
sender: CommandSender, sender: CommandSender,
@ -27,7 +27,7 @@ class CmdGetCenter(private val chunkmaster: Chunkmaster) : Subcommand {
if (args.isNotEmpty()) { if (args.isNotEmpty()) {
args[0] args[0]
} else { } else {
sender.world.name; sender.world.name
} }
} else { } else {
if (args.isEmpty()) { if (args.isEmpty()) {

@ -6,7 +6,6 @@ import net.trivernis.chunkmaster.lib.generation.taskentry.TaskEntry
import org.bukkit.command.Command import org.bukkit.command.Command
import org.bukkit.command.CommandSender import org.bukkit.command.CommandSender
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.pow
class CmdList(private val chunkmaster: Chunkmaster) : Subcommand { class CmdList(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "list" override val name = "list"
@ -50,7 +49,8 @@ class CmdList(private val chunkmaster: Chunkmaster) : Subcommand {
*/ */
private fun getGenerationEntry(task: TaskEntry): String { private fun getGenerationEntry(task: TaskEntry): String {
val genTask = task.generationTask val genTask = task.generationTask
val progress = genTask.shape.progress(if (genTask.radius < 0) (genTask.world.worldBorder.size / 32).toInt() else null) val progress =
genTask.shape.progress(if (genTask.radius < 0) (genTask.world.worldBorder.size / 32).toInt() else null)
val percentage = " (%.1f".format(progress * 100) + "%)." val percentage = " (%.1f".format(progress * 100) + "%)."
val count = if (genTask.radius > 0) { val count = if (genTask.radius > 0) {

@ -7,7 +7,7 @@ import org.bukkit.command.CommandSender
import org.bukkit.entity.Player import org.bukkit.entity.Player
class CmdSetCenter(private val chunkmaster: Chunkmaster) : Subcommand { class CmdSetCenter(private val chunkmaster: Chunkmaster) : Subcommand {
override val name = "setCenter"; override val name = "setCenter"
override fun onTabComplete( override fun onTabComplete(
sender: CommandSender, sender: CommandSender,
@ -21,7 +21,7 @@ class CmdSetCenter(private val chunkmaster: Chunkmaster) : Subcommand {
.map { it.name }.toMutableList() .map { it.name }.toMutableList()
} }
} }
return emptyList<String>().toMutableList(); return emptyList<String>().toMutableList()
} }
override fun execute(sender: CommandSender, args: List<String>): Boolean { override fun execute(sender: CommandSender, args: List<String>): Boolean {

@ -37,13 +37,16 @@ class CmdStats(private val chunkmaster: Chunkmaster) : Subcommand {
} }
private fun getWorldStatsMessage(sender: CommandSender, world: World): String { private fun getWorldStatsMessage(sender: CommandSender, world: World): String {
var message = """ return """
${chunkmaster.langManager.getLocalized("STATS_WORLD_NAME", world.name)} ${chunkmaster.langManager.getLocalized("STATS_WORLD_NAME", world.name)}
${chunkmaster.langManager.getLocalized("STATS_ENTITY_COUNT", world.entities.size)} ${chunkmaster.langManager.getLocalized("STATS_ENTITY_COUNT", world.entities.size)}
${chunkmaster.langManager.getLocalized("STATS_LOADED_CHUNKS", world.loadedChunks.size)} ${chunkmaster.langManager.getLocalized("STATS_LOADED_CHUNKS", world.loadedChunks.size)}
${chunkmaster.langManager.getLocalized("STATS_GENERATING", chunkmaster.generationManager.tasks.find { it.generationTask.world == world } != null)} ${
chunkmaster.langManager.getLocalized(
"STATS_GENERATING",
chunkmaster.generationManager.tasks.find { it.generationTask.world == world } != null)
}
""".trimIndent() """.trimIndent()
return message
} }
private fun getServerStatsMessage(sender: CommandSender): String { private fun getServerStatsMessage(sender: CommandSender): String {
@ -55,15 +58,22 @@ class CmdStats(private val chunkmaster: Chunkmaster) : Subcommand {
${chunkmaster.langManager.getLocalized("STATS_SERVER")} ${chunkmaster.langManager.getLocalized("STATS_SERVER")}
${chunkmaster.langManager.getLocalized("STATS_SERVER_VERSION", sender.server.version)} ${chunkmaster.langManager.getLocalized("STATS_SERVER_VERSION", sender.server.version)}
${chunkmaster.langManager.getLocalized("STATS_PLUGIN_VERSION", chunkmaster.description.version)} ${chunkmaster.langManager.getLocalized("STATS_PLUGIN_VERSION", chunkmaster.description.version)}
${chunkmaster.langManager.getLocalized( ${
chunkmaster.langManager.getLocalized(
"STATS_MEMORY", "STATS_MEMORY",
memUsed / 1000000, memUsed / 1000000,
runtime.maxMemory() / 1000000, runtime.maxMemory() / 1000000,
(memUsed.toFloat() / runtime.maxMemory().toFloat()) * 100 (memUsed.toFloat() / runtime.maxMemory().toFloat()) * 100
)} )
}
${chunkmaster.langManager.getLocalized("STATS_CORES", runtime.availableProcessors())} ${chunkmaster.langManager.getLocalized("STATS_CORES", runtime.availableProcessors())}
${chunkmaster.langManager.getLocalized("STATS_PLUGIN_LOADED_CHUNKS", chunkmaster.generationManager.loadedChunkCount)} ${
chunkmaster.langManager.getLocalized(
"STATS_PLUGIN_LOADED_CHUNKS",
chunkmaster.generationManager.loadedChunkCount
)
}
""".trimIndent() """.trimIndent()
for (world in sender.server.worlds) { for (world in sender.server.worlds) {
message += "\n\n" + getWorldStatsMessage(sender, world) message += "\n\n" + getWorldStatsMessage(sender, world)

@ -38,7 +38,12 @@ class CommandChunkmaster(private val chunkmaster: Chunkmaster, private val serve
/** /**
* /chunkmaster command to handle all commands * /chunkmaster command to handle all commands
*/ */
override fun onCommand(sender: CommandSender, command: Command, label: String, bukkitArgs: Array<out String>): Boolean { override fun onCommand(
sender: CommandSender,
command: Command,
label: String,
bukkitArgs: Array<out String>
): Boolean {
val args = argParser.parseArguments(bukkitArgs.joinToString(" ")) val args = argParser.parseArguments(bukkitArgs.joinToString(" "))
if (args.isNotEmpty()) { if (args.isNotEmpty()) {

@ -7,6 +7,7 @@ class ArgParser {
private var input = "" private var input = ""
private var position = 0 private var position = 0
private var currentChar = ' ' private var currentChar = ' '
private var escaped = false
/** /**
* Parses arguments from a string and respects quotes * Parses arguments from a string and respects quotes
@ -19,18 +20,24 @@ class ArgParser {
input = arguments input = arguments
position = 0 position = 0
currentChar = input[position] currentChar = input[position]
escaped = false
val args = ArrayList<String>() val args = ArrayList<String>()
var arg = "" var arg = ""
while (!endReached()) { while (!endReached()) {
nextCharacter() nextCharacter()
if (currentChar.isWhitespace()) { if (currentChar == '\\' && !escaped) {
escaped = true
continue
}
if (currentChar.isWhitespace() && !escaped) {
if (arg.isNotBlank()) { if (arg.isNotBlank()) {
args.add(arg) args.add(arg)
} }
arg = "" arg = ""
} else if (currentChar == '"') { } else if (currentChar == '"' && !escaped) {
if (arg.isNotBlank()) { if (arg.isNotBlank()) {
args.add(arg) args.add(arg)
} }
@ -42,6 +49,7 @@ class ArgParser {
} else { } else {
arg += currentChar arg += currentChar
} }
escaped = false
} }
if (arg.isNotBlank()) { if (arg.isNotBlank()) {
args.add(arg) args.add(arg)
@ -57,10 +65,17 @@ class ArgParser {
while (!endReached()) { while (!endReached()) {
nextCharacter() nextCharacter()
if (currentChar == '"') {
if (currentChar == '\\') {
escaped = !escaped
continue
}
if (currentChar == '"' && !escaped) {
break break
} }
output += currentChar output += currentChar
escaped = false
} }
return output return output
} }

@ -2,4 +2,10 @@ package net.trivernis.chunkmaster.lib.database
import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates import net.trivernis.chunkmaster.lib.generation.ChunkCoordinates
data class CompletedGenerationTask (val id: Int, val world: String, val radius: Int, val center: ChunkCoordinates, val shape: String) data class CompletedGenerationTask(
val id: Int,
val world: String,
val radius: Int,
val center: ChunkCoordinates,
val shape: String
)

@ -28,7 +28,10 @@ class CompletedGenerationTasks(private val sqliteManager: SqliteManager) {
fun getCompletedTasksForWorld(world: String): CompletableFuture<List<CompletedGenerationTask>> { fun getCompletedTasksForWorld(world: String): CompletableFuture<List<CompletedGenerationTask>> {
val completableFuture = CompletableFuture<List<CompletedGenerationTask>>() val completableFuture = CompletableFuture<List<CompletedGenerationTask>>()
sqliteManager.executeStatement("SELECT * FROM completed_generation_tasks WHERE world = ?", hashMapOf(1 to world)) { res -> sqliteManager.executeStatement(
"SELECT * FROM completed_generation_tasks WHERE world = ?",
hashMapOf(1 to world)
) { res ->
val tasks = ArrayList<CompletedGenerationTask>() val tasks = ArrayList<CompletedGenerationTask>()
while (res!!.next()) { while (res!!.next()) {
@ -51,16 +54,25 @@ class CompletedGenerationTasks(private val sqliteManager: SqliteManager) {
/** /**
* Adds a completed task * Adds a completed task
*/ */
fun addCompletedTask(id: Int, world: String, radius: Int, center: ChunkCoordinates, shape: String): CompletableFuture<Void> { fun addCompletedTask(
id: Int,
world: String,
radius: Int,
center: ChunkCoordinates,
shape: String
): CompletableFuture<Void> {
val completableFuture = CompletableFuture<Void>() val completableFuture = CompletableFuture<Void>()
sqliteManager.executeStatement("INSERT INTO completed_generation_tasks (id, world, completed_radius, center_x, center_z, shape) VALUES (?, ?, ?, ?, ?, ?)", hashMapOf( sqliteManager.executeStatement(
"INSERT INTO completed_generation_tasks (id, world, completed_radius, center_x, center_z, shape) VALUES (?, ?, ?, ?, ?, ?)",
hashMapOf(
1 to id, 1 to id,
2 to world, 2 to world,
3 to radius, 3 to radius,
4 to center.x, 4 to center.x,
5 to center.z, 5 to center.z,
6 to shape, 6 to shape,
)) { )
) {
completableFuture.complete(null) completableFuture.complete(null)
} }
return completableFuture return completableFuture

@ -7,6 +7,7 @@ import kotlin.math.ceil
class PendingChunks(private val sqliteManager: SqliteManager) { class PendingChunks(private val sqliteManager: SqliteManager) {
private val insertionCount = 300 private val insertionCount = 300
/** /**
* Returns a list of pending chunks for a taskId * Returns a list of pending chunks for a taskId
*/ */
@ -38,7 +39,15 @@ class PendingChunks(private val sqliteManager: SqliteManager) {
val statementCount = ceil(pendingChunks.size.toDouble() / insertionCount).toInt() val statementCount = ceil(pendingChunks.size.toDouble() / insertionCount).toInt()
for (i in 0 until statementCount) { for (i in 0 until statementCount) {
futures.add(insertPendingChunks(taskId, pendingChunks.subList(i * insertionCount, ((i * insertionCount) + insertionCount).coerceAtMost(pendingChunks.size)))) futures.add(
insertPendingChunks(
taskId,
pendingChunks.subList(
i * insertionCount,
((i * insertionCount) + insertionCount).coerceAtMost(pendingChunks.size)
)
)
)
} }
if (futures.size > 0) { if (futures.size > 0) {

@ -301,7 +301,8 @@ class GenerationManager(private val chunkmaster: Chunkmaster, private val server
private fun reportGenerationProgress(task: RunningTaskEntry) { private fun reportGenerationProgress(task: RunningTaskEntry) {
val genTask = task.generationTask val genTask = task.generationTask
val (speed, chunkSpeed) = task.generationSpeed val (speed, chunkSpeed) = task.generationSpeed
val progress = genTask.shape.progress(if (genTask.radius < 0) (genTask.world.worldBorder.size / 32).toInt() else null) val progress =
genTask.shape.progress(if (genTask.radius < 0) (genTask.world.worldBorder.size / 32).toInt() else null)
val percentage = val percentage =
"(${"%.2f".format(progress * 100)}%)" "(${"%.2f".format(progress * 100)}%)"

@ -18,7 +18,8 @@ class RunningTaskEntry(
get() { get() {
var generationSpeed: Double? = null var generationSpeed: Double? = null
var chunkGenerationSpeed: Double? = null var chunkGenerationSpeed: Double? = null
val progress = generationTask.shape.progress(if (generationTask.radius < 0) (generationTask.world.worldBorder.size / 32).toInt() else null) val progress =
generationTask.shape.progress(if (generationTask.radius < 0) (generationTask.world.worldBorder.size / 32).toInt() else null)
if (lastProgress != null) { if (lastProgress != null) {
val progressDiff = progress - lastProgress!!.second val progressDiff = progress - lastProgress!!.second
val timeDiff = (System.currentTimeMillis() - lastProgress!!.first).toDouble() / 1000 val timeDiff = (System.currentTimeMillis() - lastProgress!!.first).toDouble() / 1000

@ -11,9 +11,18 @@ class ArgParserTest {
argParser.parseArguments("first second third forth").shouldBe(listOf("first", "second", "third", "forth")) argParser.parseArguments("first second third forth").shouldBe(listOf("first", "second", "third", "forth"))
} }
@Test
fun `it handles escaped sequences`() {
argParser.parseArguments("first second\\ pt2 third").shouldBe(listOf("first", "second pt2", "third"))
argParser.parseArguments("first \"second\\\" part 2\" third")
.shouldBe(listOf("first", "second\" part 2", "third"))
argParser.parseArguments("first \\\\second third").shouldBe(listOf("first", "\\second", "third"))
}
@Test @Test
fun `it parses quoted arguments as one argument`() { fun `it parses quoted arguments as one argument`() {
argParser.parseArguments("first \"second with space\" third").shouldBe(listOf("first", "second with space", "third")) argParser.parseArguments("first \"second with space\" third")
.shouldBe(listOf("first", "second with space", "third"))
argParser.parseArguments("\"first\" \"second\" \"third\"").shouldBe(listOf("first", "second", "third")) argParser.parseArguments("\"first\" \"second\" \"third\"").shouldBe(listOf("first", "second", "third"))
} }
@ -36,13 +45,15 @@ class ArgParserTest {
@Test @Test
fun `it parses arguments with weird whitespace`() { fun `it parses arguments with weird whitespace`() {
argParser.parseArguments(" first second \t third \n forth ").shouldBe(listOf("first", "second", "third", "forth")) argParser.parseArguments(" first second \t third \n forth ")
.shouldBe(listOf("first", "second", "third", "forth"))
} }
@Test @Test
fun `it deals predictable with malformed input`() { fun `it deals predictable with malformed input`() {
argParser.parseArguments("first \"second third fourth").shouldBe(listOf("first", "second third fourth")) argParser.parseArguments("first \"second third fourth").shouldBe(listOf("first", "second third fourth"))
argParser.parseArguments("\"first second \"third\" fourth").shouldBe(listOf("first second ", "third", " fourth")) argParser.parseArguments("\"first second \"third\" fourth")
.shouldBe(listOf("first second ", "third", " fourth"))
argParser.parseArguments("first second third fourth\"").shouldBe(listOf("first", "second", "third", "fourth")) argParser.parseArguments("first second third fourth\"").shouldBe(listOf("first", "second", "third", "fourth"))
argParser.parseArguments("\"").shouldBe(emptyList()) argParser.parseArguments("\"").shouldBe(emptyList())
} }

@ -1,4 +1,5 @@
package net.trivernis.chunkmaster.lib package net.trivernis.chunkmaster.lib
import io.kotest.matchers.string.shouldNotBeEmpty import io.kotest.matchers.string.shouldNotBeEmpty
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -13,9 +14,9 @@ class LanguageManagerTest {
val plugin = mockk<Chunkmaster>() val plugin = mockk<Chunkmaster>()
val config = mockk<FileConfiguration>() val config = mockk<FileConfiguration>()
every {plugin.dataFolder} returns createTempDir() every { plugin.dataFolder } returns createTempDir()
every {plugin.config} returns config every { plugin.config } returns config
every {config.getString("language")} returns "en" every { config.getString("language") } returns "en"
langManager = LanguageManager(plugin) langManager = LanguageManager(plugin)
langManager.loadProperties() langManager.loadProperties()

@ -46,7 +46,8 @@ class CircleTest {
@Test @Test
fun `it returns the right edges`() { fun `it returns the right edges`() {
circle.getShapeEdgeLocations().shouldContainAll(listOf( circle.getShapeEdgeLocations().shouldContainAll(
listOf(
Pair(2, -1), Pair(2, -1),
Pair(2, 0), Pair(2, 0),
Pair(2, 1), Pair(2, 1),
@ -59,7 +60,8 @@ class CircleTest {
Pair(-1, -2), Pair(-1, -2),
Pair(0, -2), Pair(0, -2),
Pair(1, -2), Pair(1, -2),
)) )
)
} }
@Test @Test

Loading…
Cancel
Save