diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 0e8c51a41..f5432333e 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -39,7 +39,6 @@ object BasicCommands { ignore, help, completionsCommand, - multi, ifLast, append, setOnFailure, @@ -154,15 +153,24 @@ object BasicCommands { state } - def multiParser(s: State): Parser[List[String]] = { - val nonSemi = token(charClass(_ != ';', "not ';'").+, hide = const(true)) + private[sbt] def multiParserImpl(state: Option[State]): Parser[List[String]] = { + val nonSemi = charClass(_ != ';', "not ';'") val semi = token(';' ~> OptSpace) - val part = semi flatMap ( - _ => matched((s.combinedParser & nonSemi) | nonSemi) <~ 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) + (cmdPart.? ~ part.+).map { + case (Some(h), t) => h.mkString.trim +: t.toList + case (_, t) => t.toList + } } + def multiParser(s: State): Parser[List[String]] = multiParserImpl(Some(s)) + def multiApplied(s: State): Parser[() => State] = Command.applyEffect(multiParser(s))(_ ::: s) diff --git a/main-command/src/main/scala/sbt/Watched.scala b/main-command/src/main/scala/sbt/Watched.scala index e6b851d14..082f4374d 100644 --- a/main-command/src/main/scala/sbt/Watched.scala +++ b/main-command/src/main/scala/sbt/Watched.scala @@ -21,7 +21,7 @@ import sbt.internal.LegacyWatched import sbt.internal.inc.Stamper import sbt.internal.io.{ EventMonitor, Source, WatchState } import sbt.internal.util.Types.const -import sbt.internal.util.complete.DefaultParsers +import sbt.internal.util.complete.{ DefaultParsers, Parser } import sbt.internal.util.{ AttributeKey, JLine } import sbt.io.FileEventMonitor.{ Creation, Deletion, Event, Update } import sbt.io._ @@ -279,9 +279,9 @@ object Watched { onFailure = Some(Exec(failureCommandName, None)), definedCommands = s0.definedCommands :+ onFail ) - val commands = command.split(";") match { - case Array("", rest @ _*) => rest - case Array(cmd) => Seq(cmd) + val commands = Parser.parse(command, BasicCommands.multiParserImpl(Some(s))) match { + case Left(_) => command :: Nil + case Right(c) => c } val parser = Command.combine(s.definedCommands)(s) val tasks = commands.foldLeft(Nil: Seq[Either[String, () => Either[Exception, Boolean]]]) { diff --git a/main-command/src/test/scala/sbt/MultiParserSpec.scala b/main-command/src/test/scala/sbt/MultiParserSpec.scala new file mode 100644 index 000000000..ec18b1c58 --- /dev/null +++ b/main-command/src/test/scala/sbt/MultiParserSpec.scala @@ -0,0 +1,65 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt + +import org.scalatest.{ FlatSpec, Matchers } +import sbt.internal.util.complete.Parser + +object MultiParserSpec { + val parser: Parser[Seq[String]] = BasicCommands.multiParserImpl(None) + implicit class StringOps(val s: String) { + def parse: Seq[String] = Parser.parse(s, parser).right.get + def parseEither: Either[String, Seq[String]] = Parser.parse(s, parser) + } +} +import MultiParserSpec._ +class MultiParserSpec extends FlatSpec with Matchers { + "parsing" should "parse single commands" in { + ";foo".parse shouldBe Seq("foo") + "; foo".parse shouldBe Seq("foo") + } + it should "parse multiple commands" in { + ";foo;bar".parse shouldBe Seq("foo", "bar") + } + it should "parse single command with leading spaces" in { + "; foo".parse shouldBe Seq("foo") + } + it should "parse multiple commands with leading spaces" in { + "; foo;bar".parse shouldBe Seq("foo", "bar") + "; 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"""") + } + it should "parse commands without leading ';'" in { + "setStringValue foo; setStringValue bar".parse shouldBe Seq( + "setStringValue foo", + "setStringValue bar" + ) + "foo; bar".parse shouldBe Seq("foo", "bar") + "foo bar ;bar".parse shouldBe Seq("foo bar", "bar") + "foo \"a;b\"; bar".parse shouldBe Seq("foo \"a;b\"", "bar") + " foo ; bar \"b;c\"".parse shouldBe Seq("foo", "bar \"b;c\"") + } + it should "not parse single commands without leading ';'" in { + "foo".parseEither shouldBe Left("Expected ';'\nfoo\n ^") + "foo bar baz".parseEither shouldBe Left("Expected ';'\nfoo bar baz\n ^") + "foo bar baz;".parseEither shouldBe + Left("Expected not ';'\nExpected '\"'\nfoo bar baz;\n ^") + } +} diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 03dd44844..23c4b232e 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -241,6 +241,7 @@ object BuiltinCommands { export, boot, initialize, + BasicCommands.multi, act, continuous, flushFileTreeRepository diff --git a/sbt/src/sbt-test/actions/multi-command/build.sbt b/sbt/src/sbt-test/actions/multi-command/build.sbt new file mode 100644 index 000000000..0aff2d364 --- /dev/null +++ b/sbt/src/sbt-test/actions/multi-command/build.sbt @@ -0,0 +1,14 @@ +import Build._ + +organization := "sbt" + +name := "scripted-multi-command-parser" + +setStringValue := setStringValueImpl.evaluated + +checkStringValue := checkStringValueImpl.evaluated + +taskThatFails := { + throw new IllegalArgumentException("") + () +} diff --git a/sbt/src/sbt-test/actions/multi-command/project/Build.scala b/sbt/src/sbt-test/actions/multi-command/project/Build.scala new file mode 100644 index 000000000..5190dc191 --- /dev/null +++ b/sbt/src/sbt-test/actions/multi-command/project/Build.scala @@ -0,0 +1,18 @@ +import sbt._ + +object Build { + private[this] var string: String = "" + private[this] val stringFile = file("string.txt") + val setStringValue = inputKey[Unit]("set a global string to a value") + val checkStringValue = inputKey[Unit]("check the value of a global") + val taskThatFails = taskKey[Unit]("this should fail") + def setStringValueImpl: Def.Initialize[InputTask[Unit]] = Def.inputTask { + string = Def.spaceDelimited().parsed.mkString(" ").trim + IO.write(stringFile, string) + } + def checkStringValueImpl: Def.Initialize[InputTask[Unit]] = Def.inputTask { + val actual = Def.spaceDelimited().parsed.mkString(" ").trim + assert(string == actual) + assert(IO.read(stringFile) == string) + } +} diff --git a/sbt/src/sbt-test/actions/multi-command/test b/sbt/src/sbt-test/actions/multi-command/test new file mode 100644 index 000000000..8bc828c03 --- /dev/null +++ b/sbt/src/sbt-test/actions/multi-command/test @@ -0,0 +1,21 @@ +> ; setStringValue baz + +> ; checkStringValue baz + +> ; setStringValue foo; setStringValue bar + +> checkStringValue bar + +> ; setStringValue foo; setStringValue bar; setStringValue baz; checkStringValue baz + +> setStringValue foo; setStringValue bar + +> checkStringValue bar + +> setStringValue foo; setStringValue bar; setStringValue baz + +> checkStringValue baz + +-> setStringValue foo; taskThatFails; setStringValue bar + +> checkStringValue foo diff --git a/sbt/src/sbt-test/watch/watch-parser/build.sbt b/sbt/src/sbt-test/watch/watch-parser/build.sbt new file mode 100644 index 000000000..c29f61af0 --- /dev/null +++ b/sbt/src/sbt-test/watch/watch-parser/build.sbt @@ -0,0 +1,13 @@ +import Build._ + +organization := "sbt" + +name := "scripted-watch-parser" + +setStringValue := setStringValueImpl.evaluated + +checkStringValue := checkStringValueImpl.evaluated + +watchSources += file("string.txt") + +watchOnEvent := { _ => Watched.CancelWatch } diff --git a/sbt/src/sbt-test/watch/watch-parser/project/Build.scala b/sbt/src/sbt-test/watch/watch-parser/project/Build.scala new file mode 100644 index 000000000..19380e885 --- /dev/null +++ b/sbt/src/sbt-test/watch/watch-parser/project/Build.scala @@ -0,0 +1,16 @@ +import sbt._ + +object Build { + private[this] var string: String = "" + private[this] val stringFile = file("string.txt") + val setStringValue = inputKey[Unit]("set a global string to a value") + val checkStringValue = inputKey[Unit]("check the value of a global") + def setStringValueImpl: Def.Initialize[InputTask[Unit]] = Def.inputTask { + string = Def.spaceDelimited().parsed.mkString(" ").trim + IO.write(stringFile, string) + } + def checkStringValueImpl: Def.Initialize[InputTask[Unit]] = Def.inputTask { + assert(string == Def.spaceDelimited().parsed.mkString(" ").trim) + assert(IO.read(stringFile) == string) + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/watch/watch-parser/test b/sbt/src/sbt-test/watch/watch-parser/test new file mode 100644 index 000000000..63a58eb8a --- /dev/null +++ b/sbt/src/sbt-test/watch/watch-parser/test @@ -0,0 +1,21 @@ +> ~; setStringValue foo; setStringValue bar + +> checkStringValue bar + +> ~;setStringValue foo;setStringValue bar; checkStringValue bar + +> ~; setStringValue foo;setStringValue bar; checkStringValue bar + +> ~; setStringValue foo; setStringValue bar; checkStringValue bar + +# no leading semicolon +> ~ setStringValue foo; setStringValue bar; checkStringValue bar + +> ~ setStringValue foo + +> checkStringValue foo + +# All of the other tests have involved input tasks, so include commands with regular tasks as well. +> ~; compile; setStringValue baz; checkStringValue baz +# No trailing semicolons are allowed +-> ~; compile; setStringValue baz; checkStringValue baz;