Merge pull request #4456 from eatkins/multi-command

Multi command
This commit is contained in:
eugene yokota 2018-11-20 10:35:46 -05:00 committed by GitHub
commit 4f27319beb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 187 additions and 10 deletions

View File

@ -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)

View File

@ -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]]]) {

View File

@ -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 ^")
}
}

View File

@ -241,6 +241,7 @@ object BuiltinCommands {
export,
boot,
initialize,
BasicCommands.multi,
act,
continuous,
flushFileTreeRepository

View File

@ -0,0 +1,14 @@
import Build._
organization := "sbt"
name := "scripted-multi-command-parser"
setStringValue := setStringValueImpl.evaluated
checkStringValue := checkStringValueImpl.evaluated
taskThatFails := {
throw new IllegalArgumentException("")
()
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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 }

View File

@ -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)
}
}

View File

@ -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;