输入任务
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:
Parser[I]:不使用任何设置的基本解析器Initialize[Parser[I]]:定义依赖一个或多个设置的解析器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]]。
分解如下:
- 您可使用其他设置(通过 Initialize)构建输入任务。
- 您可使用当前 State 构建解析器。
- 解析器接受用户输入并提供 Tab 补全。
- 解析器产生要运行的任务。
因此,您可使用设置或 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
预应用输入
由于 InputTasks 由 Parsers 构建,可通过编程方式应用输入来生成新的 InputTask。(也可生成 Task,下一节将介绍。)InputTask[T] 和 Initialize[InputTask[T]] 上提供了两个接受要应用的 String 的便捷方法。
partialInput应用输入并允许进一步输入,如来自命令行fullInput应用输入并终止解析,不再接受进一步输入
每种情况下,输入都会应用于输入任务的解析器。由于输入任务处理任务名后的所有输入,通常需要在输入中提供起始空白。
考虑上一节的示例。我们可以修改为:
- 显式指定第一个
run的全部参数。我们使用name和version展示设置可用于定义和修改解析器。 - 定义传给第二个
run的初始参数,但允许命令行上的进一步输入。
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。本节中,应用输入会产生 Task。Initialize[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!