Make multi command parser work with string literals

Presently the multi command parser doesn't work correctly if one of the
commands includes a string literal. For example, suppose that there is
an input task defined name "bash" that shells out and runs the input.
Then the following does not work with the current multi command parser:
; bash "rm target/classes/Foo.class; touch src/main/scala/Foo.scala"; comple
Note that this is a real use case that has caused me issues in the past.

The problem is that the semicolon inside of the quote gets interpreted
as a command separator token. To fix this, I rework the parser so that
it consumes string literals and doesn't modify them. By using
StringEscapable, I allow the string to contain quotation marks itself.

I couldn't write a scripted test for this because in a command like
`; foo "bar"; baz`, the quotes around bar seem to get stripped. This
could be fixed by adding an alternative to StringEscapable that matches
an escaped string, but that is more work than I'm willing to do right
now.
This commit is contained in:
Ethan Atkins 2018-11-19 08:36:48 -08:00
parent 05e3a8609b
commit 51d986d751
2 changed files with 20 additions and 5 deletions

View File

@ -155,13 +155,16 @@ object BasicCommands {
}
private[sbt] def multiParserImpl(state: Option[State]): Parser[List[String]] = {
val nonSemi = token(charClass(_ != ';', "not ';'").+, hide = const(true))
val nonSemi = charClass(_ != ';', "not ';'")
val semi = token(';' ~> OptSpace)
def commandParser = state.map(s => (s.combinedParser & nonSemi) | nonSemi).getOrElse(nonSemi)
val part = semi flatMap (
_ => matched(commandParser) <~ token(OptSpace)
val nonQuote = charClass(_ != '"', label = "not '\"'")
val cmdPart = token(
((nonSemi & nonQuote).map(_.toString) | StringEscapable.map(c => s""""$c"""")).+,
hide = const(true)
)
(part map (_.trim)).+ map (_.toList)
def commandParser = state.map(s => (s.combinedParser & cmdPart) | cmdPart).getOrElse(cmdPart)
val part = semi.flatMap(_ => matched(commandParser) <~ token(OptSpace)).map(_.trim)
part.+ map (_.toList)
}
def multiParser(s: State): Parser[List[String]] = multiParserImpl(Some(s))

View File

@ -33,4 +33,16 @@ class MultiParserSpec extends FlatSpec with Matchers {
"; foo; bar".parse shouldBe Seq("foo", "bar")
";foo; bar".parse shouldBe Seq("foo", "bar")
}
it should "parse command with string literal" in {
"; foo \"barbaz\"".parse shouldBe Seq("foo \"barbaz\"")
"; foo \"bar;baz\"".parse shouldBe Seq("foo \"bar;baz\"")
"; foo \"barbaz\"; bar".parse shouldBe Seq("foo \"barbaz\"", "bar")
"; foo \"barbaz\"; bar \"blah\"".parse shouldBe Seq("foo \"barbaz\"", "bar \"blah\"")
"; foo \"bar;baz\"; bar".parse shouldBe Seq("foo \"bar;baz\"", "bar")
"; foo \"bar;baz\"; bar \"buzz\"".parse shouldBe Seq("foo \"bar;baz\"", "bar \"buzz\"")
"; foo \"bar;baz\"; bar \"buzz;two\"".parse shouldBe Seq("foo \"bar;baz\"", "bar \"buzz;two\"")
"""; foo "bar;\"baz\""; bar""".parse shouldBe Seq("""foo "bar;\"baz\""""", "bar")
"""; setStringValue "foo;bar"; checkStringValue "foo;bar"""".parse shouldBe
Seq("""setStringValue "foo;bar"""", """checkStringValue "foo;bar"""")
}
}