10

In SBT: I would like to define an inputKey that reads in command line arguments, changes them slightly and uses the result as input to other inputKeys.

I tried:

lazy val demo = inputKey[Unit]("A demo input task.")
lazy val root = (project in file(".")).settings(
  libraryDependencies += jUnitInterface
).settings(
  demo := {
    val args: Seq[String] = spaceDelimited("<arg>").parsed
    val one = (run in Compile).fullInput(args(0) + "foo").evaluated
  }
)

But I'm getting error: Illegal dynamic reference: args.

I also tried:

demo := {
  val args: Seq[String] = spaceDelimited("<arg>").parsed
  System.setProperty("args0", args(0))
  val one = (run in Compile).fullInput(System.getProperty(args0) + "foo").evaluated
}

This simply does not provide the input. I suspect it is a matter of execution order (property does not get set when I want it, since JVM is free to move lines around).

So, in my desperation, I even tried the atrocious:

demo := {
  val args: Seq[String] = spaceDelimited("<arg>").parsed
  try {
    System.setProperty("args0", args(0))
  } finally {
    val one = (run in Compile).fullInput(System.getProperty(args0) + "foo").evaluated
  }
}

to force the order. This just throws a NullPointerException.

1
  • 1
    All these parsed, evaluated, and the like are macros, I think. Possibly, the way to get around the problem is to use the non-macro expansion of this stuff. And, no, I don't know what that might be. Commented Jan 29, 2015 at 17:13

1 Answer 1

8
+300

As Daniel C. Sobral mentioned, parsed and evaluated are macros, defined in InputWrapper.

Since they are executed at compile time, and the arguments are retrieved at runtime, they do not mix well. In particular, the value of args is only really defined at runtime and cannot be retrieved by the evaluated macro.

EDIT: After a chat session with the OP, I've determined that the aim he had was a shortcut for writing myTask Foo bar instead of testOnly *Foo* -- --tests=*bar*, I've updated my answer accordingly.

Updated answer

As discussed, since you basically want a "macro" for writing myTask Foo bar instead of testOnly *Foo* -- --tests=*bar*, here's my solution:

val filtersParser = {
    import complete.DefaultParsers._
    (token(Space) ~> token(StringBasic, "<classFilter>")) ~
        (token(Space) ~> token(StringBasic, "<methodFilter>"))
}

lazy val testFiltered = inputKey[Unit]("runs test methods matching *<methodFilter>* within classes matching *<classFilter>*")

testFiltered.in(Test) := Def.inputTaskDyn {
    val (classFilter, methodFilter) = filtersParser.parsed
    runTestsFiltered(classFilter, methodFilter)
}.evaluated

def runTestsFiltered(classFilter: String, methodFilter: String) = Def.taskDyn {
    (testOnly in Test).toTask(s" *$classFilter* -- --tests *$methodFilter*")
}

In more detail

You need a custom parser to retrieve the two arguments you're expecting. This is achieved with the following code, which basically defines two groups, "chomping" both spaces without remembering them, and two StringBasic arguments, which are the result of the parser (filtersParser is of type Parser[(String, String)])

val filtersParser = {
    import complete.DefaultParsers._
    (token(Space) ~> token(StringBasic, "<classFilter>")) ~
        (token(Space) ~> token(StringBasic, "<methodFilter>"))
}

Then you need an input task to use the results of the parser and forward them to the test framework. This is done in the next snippet (if someone more knowledgeable than me wishes to chime in on the subtleties of using an inputTaskDyn, I'll gladly be enlightened :) ). Do note the definition of the scope of the task .in(Test) which grants access to the test dependencies.

lazy val testFiltered = inputKey[Unit]("runs test methods matching *<methodFilter>* within classes matching *<classFilter>*")

testFiltered.in(Test) := Def.inputTaskDyn {
    val (classFilter, methodFilter) = filtersParser.parsed
    runTestsFiltered(classFilter, methodFilter)
}.evaluated

And the last bit of code simply forwards the arguments to the pre-existing testOnly task:

def runTestsFiltered(classFilter: String, methodFilter: String) = Def.taskDyn {
    (testOnly in Test).toTask(s" *$classFilter* -- --tests *$methodFilter*")
}

Previous answer

However, you should be able to go around it by splitting definition and usage in two tasks:

import sbt._
import complete.DefaultParsers._

lazy val loadArgTask = inputKey[Unit]("loads and transforms argument")

lazy val runStuff = taskKey[Unit]("Runs some stuff")

lazy val loadArgIntoPropertyTask: Def.Initialize[InputTask[Unit]] = Def.inputTask {
    val myArg = (token(Space) ~> token(StringBasic, "<myArg>")).parsed
    System.setProperty("myArg", myArg + "foo")
}

loadArgTask <<= loadArgIntoPropertyTask

runStuff := {
    println(System.getProperty("myArg"))
}

Which can be used as follows

> loadArgTask orange
[success] Total time: 0 s, completed [...]
> runStuff
orangefoo
[success] Total time: 0 s, completed [...]
8
  • 1. If I add testOnly in Test).fullInput(System.getProperty("myArg")).evaluated to runStuff I'm getting value can only be called on an input task within a task definition macro, such as := or Def.inputTask. (testOnly in Test).fullInput(System.getProperty("myArg")).evaluated
    – hyperio
    Commented Feb 12, 2015 at 11:18
  • 2. If I make runStuff an inputKey, I'm getting [error] Not a valid command: runStuff [error] Invalid programmatic input: [error] Expected whitespace character
    – hyperio
    Commented Feb 12, 2015 at 11:18
  • An inputKey expects parameters, you can't just run it without any parameter, as opposed to a taskKey...
    – Thomas
    Commented Feb 12, 2015 at 11:52
  • Oh, a bit pushy, I know, but if you wouldn't terribly mind accepting my answer before the bounty runs out, I'd appreciate it ;-)
    – Thomas
    Commented Feb 12, 2015 at 11:53
  • Either way, I'll try and help you get to your desired result, but I'm currently heading for a meeting, might find some time later on today.
    – Thomas
    Commented Feb 12, 2015 at 11:54

Not the answer you're looking for? Browse other questions tagged or ask your own question.