1. 输入任务

输入任务 

输入任务解析用户输入并生成要运行的任务。 解析输入 描述了如何使用定义输入语法和制表符完成的解析器组合器。此页面描述了如何将这些解析器组合器挂钩到输入任务系统。

输入键 

输入任务的键类型为 InputKey,它表示输入任务,就像 SettingKey 表示设置或 TaskKey 表示任务一样。使用 inputKey.apply 工厂方法定义新的输入任务键

// goes in project/Build.scala or in build.sbt
val demo = inputKey[Unit]("A demo input task.")

输入任务的定义类似于普通任务,但它也可以使用 Parser 应用于用户输入的结果。

就像特殊的 value 方法获取设置或任务的值一样,特殊的 parsed 方法获取 Parser 的结果。

基本输入任务定义 

最简单的输入任务接受空格分隔的 аргументы 序列。它不提供有用的制表符完成,解析是基本的。内置的空格分隔 аргументы 解析器是通过 spaceDelimited 方法构建的,该方法接受唯一的 аргументы 作为其唯一的 аргументы,即在制表符完成期间向用户显示的标签。

例如,以下任务打印当前 Scala 版本,然后在单独的行上回显传递给它的 аргументы。

import complete.DefaultParsers._

demo := {
  // get the result of parsing
  val args: Seq[String] = spaceDelimited("<arg>").parsed
  // Here, we also use the value of the `scalaVersion` setting
  println("The current Scala version is " + scalaVersion.value)
  println("The arguments to demo were:")
  args foreach println
}

使用解析器的输入任务 

spaceDelimited 方法提供的解析器在定义输入语法方面没有提供任何灵活性。使用自定义解析器只是定义您自己的 Parser 的问题,如 解析输入 页面所述。

构造解析器 

第一步是通过定义以下类型之一的值来构造实际的 Parser

  • Parser[I]:一个不使用任何设置的基本解析器
  • Initialize[Parser[I]]:一个定义依赖于一个或多个设置的解析器
  • Initialize[State => Parser[I]]:一个使用设置和当前 状态 定义的解析器

我们已经看到了第一个案例的示例,spaceDelimited,它在定义中没有使用任何设置。作为第三个案例的示例,以下定义了一个虚构的 Parser,它使用项目的 Scala 和 sbt 版本设置以及状态。要使用这些设置,我们需要将解析器构造包装在 Def.setting 中,并使用特殊的 value 方法获取设置值

import complete.DefaultParsers._
import complete.Parser

val parser: Def.Initialize[State => Parser[(String,String)]] =
Def.setting {
  (state: State) =>
    ( token("scala" <~ Space) ~ token(scalaVersion.value) ) |
    ( token("sbt" <~ Space) ~ token(sbtVersion.value) ) |
    ( token("commands" <~ Space) ~
        token(state.remainingCommands.size.toString) )
}

此解析器定义将产生类型为 (String,String) 的值。定义的输入语法不是非常灵活;它只是一个演示。对于成功的解析,它将产生以下值之一(假设当前 Scala 版本为 2.12.18,当前 sbt 版本为 1.9.8,并且还有 3 个命令要运行)

  • (scala,2.12.18)
  • (sbt,1.9.8)
  • (commands,3)

同样,我们能够访问项目当前的 Scala 和 sbt 版本,因为它们是设置。任务不能用于定义解析器。

构造任务 

接下来,我们从 Parser 的结果中构建实际要执行的任务。为此,我们像往常一样定义一个任务,但我们可以通过 Parser 上的特殊 parsed 方法访问解析的结果。

以下虚构示例使用上一个示例的输出(类型为 (String,String))以及 package 任务的结果,将一些信息打印到屏幕上。

demo := {
    val (tpe, value) = parser.parsed
    println("Type: " + tpe)
    println("Value: " + value)
    println("Packaged: " + packageBin.value.getAbsolutePath)
}

InputTask 类型 

查看 InputTask 类型有助于理解输入任务的更高级用法。核心输入任务类型是

class InputTask[T](val parser: State => Parser[Task[T]])

通常,输入任务被分配给一个设置,并且您使用 Initialize[InputTask[T]]

分解它,

  1. 您可以使用其他设置(通过 Initialize)来构造输入任务。
  2. 您可以使用当前状态来构造解析器。
  3. 解析器接受用户输入并提供制表符完成。
  4. 解析器生成要运行的任务。

因此,您可以使用设置或 State 来构造定义输入任务命令行语法的解析器。这在上一节中进行了描述。然后,您可以使用设置、State 或用户输入来构造要运行的任务。这是输入任务语法中的隐式内容。

使用其他输入任务 

参与输入任务的类型是可组合的,因此可以重复使用输入任务。.parsed.evaluated 方法是在 InputTasks 上定义的,以使在常见情况下更方便

  • InputTask[T]Initialize[InputTask[T]] 上调用 .parsed 以获取在解析命令行后创建的 Task[T]
  • InputTask[T]Initialize[InputTask[T]] 上调用 .evaluated 以获取从评估该任务获得的类型为 T 的值

在这两种情况下,底层的 Parser 都按输入任务定义中的语法顺序与其他解析器进行排序。在 .evaluated 的情况下,将评估生成的 task。

以下示例应用 run 输入任务、一个文字分隔符解析器 --,以及 run 再次。解析器按语法出现顺序排序,因此 -- 之前的 аргументы 传递给第一个 run,之后的 аргументы 传递给第二个。

val run2 = inputKey[Unit](
    "Runs the main class twice with different argument lists separated by --")

val separator: Parser[String] = "--"

run2 := {
   val one = (Compile / run).evaluated
   val sep = separator.parsed
   val two = (Compile / run).evaluated
}

对于回显其 аргументы 的主类 Demo,这看起来像

$ sbt
> run2 a b -- c d
[info] Running Demo c d
[info] Running Demo a b
c
d
a
b

预应用输入 

因为 InputTasks 是从 Parsers 构建的,所以可以通过以编程方式应用一些输入来生成新的 InputTask。(也可以生成 Task,这将在下一节中介绍。)InputTask[T]Initialize[InputTask[T]] 上提供了两个方便方法,它们接受要应用的字符串。

  • partialInput 应用输入并允许进一步输入,例如来自命令行
  • fullInput 应用输入并终止解析,因此不接受进一步输入

在每种情况下,输入都应用于输入任务的解析器。因为输入任务处理任务名称后的所有输入,所以它们通常需要在输入中提供初始空格。

考虑上一节中的示例。我们可以修改它,这样我们就可以

  • 明确指定第一个 run 的所有 аргументы。我们使用 nameversion 来表明设置可以用于定义和修改解析器。
  • 定义传递给第二个 run 的初始 аргументы,但允许在命令行上进行进一步输入。

注意:如果输入来自您需要使用的设置,您需要使用,例如,Def.taskDyn { ... }.value

lazy val run2 = inputKey[Unit]("Runs the main class twice: " +
   "once with the project name and version as arguments"
   "and once with command line arguments preceded by hard coded values.")

// The argument string for the first run task is ' <name> <version>'
lazy val firstInput: Initialize[String] =
   Def.setting(s" ${name.value} ${version.value}")

// Make the first arguments to the second run task ' red blue'
lazy val secondInput: String = " red blue"

run2 := {
   val one = (Compile / run).fullInput(firstInput.value).evaluated
   val two = (Compile / run).partialInput(secondInput).evaluated
}

对于回显其 аргументы 的主类 Demo,这看起来像

$ sbt
> run2 green
[info] Running Demo demo 1.0
[info] Running Demo red blue green
demo
1.0
red
blue
green

从 InputTask 获取 Task 

上一节展示了如何通过应用输入来推导出新的 InputTask。在本节中,应用输入会产生一个 TaskInitialize[InputTask[T]] 上的 toTask 方法接受要应用的 String 输入,并生成一个可以正常使用或直接运行的任务,而无需提供任何输入

lazy val runFixed = taskKey[Unit]("A task that hard codes the values to `run`")

runFixed := {
   val _ = (Compile / run).toTask(" blue green").value
   println("Done!")
}

对于回显其 аргументы 的主类 Demo,运行 runFixed 看起来像

$ sbt
> runFixed
[info] Running Demo blue green
blue
green
Done!

每次调用 toTask 都会生成一个新的 task,但每个 task 的配置都与原始 InputTask(在本例中为 run)相同,但应用了不同的输入。例如

lazy val runFixed2 = taskKey[Unit]("A task that hard codes the values to `run`")

run / fork := true

runFixed2 := {
   val x = (Compile / run).toTask(" blue green").value
   val y = (Compile / run).toTask(" red orange").value
   println("Done!")
}

不同的 toTask 调用定义了不同的 task,每个 task 都在新的 jvm 中运行项目的 main 类。也就是说,fork 设置配置了这两个,每个都具有相同的类路径,并且每个都运行相同的 main 类。但是,每个 task 将不同的 аргументы 传递给 main 类。对于回显其 аргументы 的主类 Demo,运行 runFixed2 的输出可能如下所示

$ sbt
> runFixed2
[info] Running Demo blue green
[info] Running Demo red orange
blue
green
red
orange
Done!