From 51d986d751866491acf408ca7882b48abeecd06d Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Mon, 19 Nov 2018 08:36:48 -0800 Subject: [PATCH] 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. --- main-command/src/main/scala/sbt/BasicCommands.scala | 13 ++++++++----- .../src/test/scala/sbt/MultiParserSpec.scala | 12 ++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 8d4b9c34d..f647c9478 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -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)) diff --git a/main-command/src/test/scala/sbt/MultiParserSpec.scala b/main-command/src/test/scala/sbt/MultiParserSpec.scala index e29eca571..06a30a179 100644 --- a/main-command/src/test/scala/sbt/MultiParserSpec.scala +++ b/main-command/src/test/scala/sbt/MultiParserSpec.scala @@ -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"""") + } }