diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 73c0184e5..819a71b2e 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -155,15 +155,14 @@ object BasicCommands { private[sbt] def multiParserImpl(state: Option[State]): Parser[List[String]] = { val nonSemi = charClass(_ != ';', "not ';'") - val semi = token(OptSpace ~> ';' ~> OptSpace) + val semi = token(';') val nonDelim = charClass(c => c != '"' && c != '{' && c != '}', label = "not '\"', '{', '}'") val components = ((nonSemi & nonDelim) | StringEscapable | braces('{', '}')).+ val cmdPart = matched(components).examples() val completionParser: Option[Parser[String]] = - state.map(s => OptSpace ~> matched(s.nonMultiParser) <~ OptSpace) - val cmdParser = - completionParser.map(sp => (sp & cmdPart) | cmdPart).getOrElse(cmdPart).map(_.trim) + state.map(s => OptSpace ~> matched((s.nonMultiParser & cmdPart) | cmdPart) <~ OptSpace) + val cmdParser = completionParser.getOrElse(cmdPart).map(_.trim) val multiCmdParser: Parser[String] = semi ~> cmdParser /* * There are two cases that need to be handled separately: diff --git a/main-command/src/test/scala/sbt/MultiParserSpec.scala b/main-command/src/test/scala/sbt/MultiParserSpec.scala index 1beb4059d..1ea3c8556 100644 --- a/main-command/src/test/scala/sbt/MultiParserSpec.scala +++ b/main-command/src/test/scala/sbt/MultiParserSpec.scala @@ -7,6 +7,7 @@ package sbt +import scala.concurrent.duration._ import org.scalatest.FlatSpec import sbt.internal.util.complete.Parser @@ -28,6 +29,15 @@ class MultiParserSpec extends FlatSpec { } it should "parse single command with leading spaces" in { assert("; foo".parse == Seq("foo")) + assert(" ; foo".parse == Seq("foo")) + assert(" foo;".parse == Seq("foo")) + } + it should "parse single command with trailing spaces" in { + assert("; foo ".parse == Seq("foo")) + assert(";foo ".parse == Seq("foo")) + assert("foo; ".parse == Seq("foo")) + assert(" foo; ".parse == Seq("foo")) + assert(" foo ; ".parse == Seq("foo")) } it should "parse multiple commands with leading spaces" in { assert("; foo;bar".parse == Seq("foo", "bar")) @@ -103,4 +113,14 @@ class MultiParserSpec extends FlatSpec { val unmatchedEmptyBraces = "{{{{}}}" assert(s"compile; $unmatchedEmptyBraces".parseEither.isLeft) } + it should "handle cosmetic whitespace" in { + val commands = (1 to 100).map(_ => "compile") + val multiLine = commands.mkString(" \n ;", " \n ;", " \n ") + val start = System.nanoTime + assert(multiLine.parse == commands) + val elapsed = System.nanoTime - start + // Make sure this took less than 10 seconds. It takes about 30 milliseconds to run with + // 100 commands and 3 milliseconds with 3 commands. With a bad parser, it will run indefinitely. + assert(elapsed.nanoseconds < 10.seconds) + } }