Created
November 25, 2019 17:19
-
-
Save afollestad/bb527e90f9efcd126da598c9b495d639 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
test { | |
useJUnitPlatform() | |
} | |
dependencies { | |
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' | |
testImplementation 'io.kotlintest:kotlintest-runner-junit5:3.3.2' | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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