Skip to content

Instantly share code, notes, and snippets.

@afollestad
Created November 25, 2019 17:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save afollestad/bb527e90f9efcd126da598c9b495d639 to your computer and use it in GitHub Desktop.
Save afollestad/bb527e90f9efcd126da598c9b495d639 to your computer and use it in GitHub Desktop.
test {
useJUnitPlatform()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
testImplementation 'io.kotlintest:kotlintest-runner-junit5:3.3.2'
}
class CommandLineParser(
private val input: String
) : Iterator<String> {
private var lastIndex: Int = 0
override fun hasNext(): Boolean = lastIndex in 0..input.length - 2
override fun next(): String {
val value = StringBuilder()
var quoteStarter: Char? = null
var lastQuoteIndex: Int = -1
var inQuote = false
for (currentIndex in lastIndex until input.length) {
lastIndex = currentIndex
val currentChar: Char = input[currentIndex]
val lastChar: Char? = if (currentIndex > 0) input[currentIndex - 1] else null
if (currentChar == SPACE && !inQuote) {
lastIndex = nextNonSpace(currentIndex + 1)
break
}
if (currentChar.isQuote() && lastChar != QUOTE_ESCAPE) {
lastQuoteIndex = currentIndex
if (inQuote) {
if (currentChar != quoteStarter!!) {
error("Unescaped quote at index $currentIndex!")
} else if (currentChar == quoteStarter) {
lastIndex = nextNonSpace(currentIndex + 1)
inQuote = false
break
}
}
quoteStarter = currentChar
inQuote = true
continue
}
value += currentChar
}
if (inQuote) {
error(
"""
Unescaped quote at index $lastQuoteIndex!
$input
${" ".repeat(lastQuoteIndex)}^
""".trimIndent()
)
}
return value.unescapeQuotes()
}
private fun nextNonSpace(startIndex: Int): Int {
for (currentIndex in startIndex until input.length) {
if (input[currentIndex] != SPACE) {
return currentIndex
}
}
return -1
}
}
private const val QUOTE_ESCAPE: Char = '\\'
private const val SPACE: Char = ' '
private const val SINGLE_QUOTE: Char = '\''
private const val DOUBLE_QUOTE: Char = '"'
private operator fun StringBuilder.plusAssign(c: Char) {
append(c)
}
private fun Char?.isQuote(): Boolean = this == SINGLE_QUOTE || this == DOUBLE_QUOTE
private fun StringBuilder.unescapeQuotes(): String {
return toString()
.replace("$QUOTE_ESCAPE$SINGLE_QUOTE", "$SINGLE_QUOTE")
.replace("$QUOTE_ESCAPE$DOUBLE_QUOTE", "$DOUBLE_QUOTE")
}
import com.squareup.scripting.kcli.internal.CommandLineParser
import io.kotlintest.shouldBe
import io.kotlintest.shouldThrow
import io.kotlintest.specs.StringSpec
class CommandLineParserTest : StringSpec({
"simple" {
val parser = CommandLineParser("one two three four")
parser.hasNext() shouldBe true
parser.next() shouldBe "one"
parser.hasNext() shouldBe true
parser.next() shouldBe "two"
parser.hasNext() shouldBe true
parser.next() shouldBe "three"
parser.hasNext() shouldBe true
parser.next() shouldBe "four"
parser.hasNext() shouldBe false
}
"with excess spaces" {
val parser = CommandLineParser("one two three four")
parser.hasNext() shouldBe true
parser.next() shouldBe "one"
parser.hasNext() shouldBe true
parser.next() shouldBe "two"
parser.hasNext() shouldBe true
parser.next() shouldBe "three"
parser.hasNext() shouldBe true
parser.next() shouldBe "four"
parser.hasNext() shouldBe false
}
"with quotes" {
val parser = CommandLineParser("one \"two three\" four")
parser.hasNext() shouldBe true
parser.next() shouldBe "one"
parser.hasNext() shouldBe true
parser.next() shouldBe "two three"
parser.hasNext() shouldBe true
parser.next() shouldBe "four"
parser.hasNext() shouldBe false
}
"with escaped quotes" {
val parser = CommandLineParser("one \\\"two three\\\" four")
parser.hasNext() shouldBe true
parser.next() shouldBe "one"
parser.hasNext() shouldBe true
parser.next() shouldBe "\"two"
parser.hasNext() shouldBe true
parser.next() shouldBe "three\""
parser.hasNext() shouldBe true
parser.next() shouldBe "four"
parser.hasNext() shouldBe false
}
"with escaped quotes inside of quotes" {
val parser = CommandLineParser("one \"\\\"two three\\\"\" \"four\\\"\"")
parser.hasNext() shouldBe true
parser.next() shouldBe "one"
parser.hasNext() shouldBe true
parser.next() shouldBe "\"two three\""
parser.hasNext() shouldBe true
parser.next() shouldBe "four\""
parser.hasNext() shouldBe false
}
"with empty quotes" {
val parser = CommandLineParser("one two three four \"\" ")
parser.hasNext() shouldBe true
parser.next() shouldBe "one"
parser.hasNext() shouldBe true
parser.next() shouldBe "two"
parser.hasNext() shouldBe true
parser.next() shouldBe "three"
parser.hasNext() shouldBe true
parser.next() shouldBe "four"
parser.hasNext() shouldBe true
parser.next() shouldBe ""
parser.hasNext() shouldBe false
}
"starts with quote" {
val parser = CommandLineParser("\"one two\" three four")
parser.hasNext() shouldBe true
parser.next() shouldBe "one two"
parser.hasNext() shouldBe true
parser.next() shouldBe "three"
parser.hasNext() shouldBe true
parser.next() shouldBe "four"
parser.hasNext() shouldBe false
}
"unescaped quote" {
val parser = CommandLineParser("\"hello \"world\"")
parser.hasNext() shouldBe true
parser.next() shouldBe "hello "
parser.hasNext() shouldBe true
val exception = shouldThrow<IllegalStateException> {
parser.next()
}
exception.message shouldBe """
Unescaped quote at index 13!
"hello "world"
^
""".trimIndent()
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment