输入任务

sbt 支持定义可解析用户输入并提供 Tab 补全的自定义任务。解析器详情将在后续的 Tab 补全解析器中介绍。

本页介绍如何将这些解析器组合子接入输入任务系统。

输入键

输入任务的键类型为 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 的结果。

基本输入任务定义

最简单的输入任务接受空格分隔的参数序列。它不提供有用的 Tab 补全,解析也较基础。空格分隔参数的内置解析器通过 spaceDelimited 方法构建,其唯一参数为 Tab 补全时向用户展示的标签。

例如,以下任务打印当前 Scala 版本,然后将传入的参数逐行回显。

import complete.DefaultParsers.{ *, given }

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 在定义输入语法时缺乏灵活性。使用自定义解析器只需按 Parsing Input 页所述定义您自己的 Parser

构建解析器

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

  1. Parser[I]:不使用任何设置的基本解析器
  2. Initialize[Parser[I]]:定义依赖一个或多个设置的解析器
  3. Initialize[State => Parser[I]]:使用设置和当前 state 定义的解析器

我们已在 spaceDelimited 中见过第一种情况的示例,其定义不使用任何设置。作为第三种情况的示例,以下定义了一个使用项目 Scala 和 sbt 版本设置以及 state 的示例 Parser。要使用这些设置,需将 Parser 构建包装在 Def.setting 中,并用特殊的 value 方法获取设置值:

import sbt.complete.DefaultParsers.{ *, given }
import sbt.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) )
}

This Parser definition will produce a value of type (String,String). The input syntax defined isn't very flexible; it is just a demonstration. It will produce one of the following values for a successful parse (assuming the current Scala version is 3.8.2, the current sbt version is 2.0.0-RC10, and there are 3 commands left to run):

  • (scala,3.8.2)
  • (sbt,2.0.0-RC10)
  • (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[A1](val parser: State => Parser[Task[A1]])

通常,输入任务会赋给设置,您会使用 Initialize[InputTask[A1]]

分解如下:

  1. 您可使用其他设置(通过 Initialize)构建输入任务。
  2. 您可使用当前 State 构建解析器。
  3. 解析器接受用户输入并提供 Tab 补全。
  4. 解析器产生要运行的任务。

因此,您可使用设置或 State 构建定义输入任务命令行语法的解析器。上一节已说明。然后可使用设置、State 或用户输入构建要运行的任务。这在输入任务语法中是隐式的。

使用其他输入任务

输入任务涉及的类型可组合,因此可复用输入任务。InputTasks 上定义了 .parsed.evaluated 方法,便于常见场景:

  • InputTask[A1]Initialize[InputTask[A1]] 上调用 .parsed,获取解析命令行后创建的 Task[A1]
  • InputTask[A1]Initialize[InputTask[A1]] 上调用 .evaluated,从执行该任务获取类型为 A1 的值

两种情况下,底层 Parser 都会与输入任务定义中的其他解析器按序组合。对于 .evaluated,会执行生成的任务。

以下示例依次应用 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

预应用输入

由于 InputTasksParsers 构建,可通过编程方式应用输入来生成新的 InputTask。(也可生成 Task,下一节将介绍。)InputTask[T]Initialize[InputTask[T]] 上提供了两个接受要应用的 String 的便捷方法。

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

每种情况下,输入都会应用于输入任务的解析器。由于输入任务处理任务名后的所有输入,通常需要在输入中提供起始空白。

考虑上一节的示例。我们可以修改为:

  • 显式指定第一个 run 的全部参数。我们使用 nameversion 展示设置可用于定义和修改解析器。
  • 定义传给第二个 run 的初始参数,但允许命令行上的进一步输入。

Note

If the input derives from settings you need to use, for example, 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[A1]] 上的 toTask 方法接受要应用的 String 输入,并产生可正常使用的任务。例如,以下定义了一个普通任务 runFixed,可被其他任务使用或直接运行而无需提供任何输入:

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

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

For a main class Demo that echoes its arguments, running runFixed looks like:

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

每次调用 toTask 都会生成新任务,但每个任务的配置与原始 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 调用定义不同的任务,每个任务都在新的 jvm 中运行项目主类。即,fork 设置同时配置两者,每个都有相同的 classpath,都运行相同的主类。但每个任务向主类传递不同的参数。对于回显其参数的主类 Demo,运行 runFixed2 的输出可能如下:

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