1. 輸入任務

輸入任務 

輸入任務會剖析使用者輸入,並產生要執行的任務。剖析輸入說明如何使用剖析器組合子來定義輸入語法和 Tab 完成。此頁面說明如何將這些剖析器組合子掛接到輸入任務系統中。

輸入鍵 

輸入任務的鍵類型為 InputKey,表示輸入任務,就像 SettingKey 表示設定或 TaskKey 表示任務一樣。使用 inputKey.apply 工廠方法定義新的輸入任務鍵

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

輸入任務的定義類似於一般任務,但也可以使用

剖析器套用至使用者輸入的結果。正如特殊的 value 方法會取得設定或任務的值,特殊的 parsed 方法會取得 Parser 的結果。

基本輸入任務定義 

最簡單的輸入任務會接受以空格分隔的引數序列。它不提供有用的 Tab 完成,而且剖析很基本。以空格分隔引數的內建剖析器是透過 spaceDelimited 方法建構的,該方法只接受一個引數,也就是在 Tab 完成期間向使用者呈現的標籤。

例如,下列任務會印出目前的 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. 剖析器會接受使用者輸入,並提供 Tab 完成。
  4. 剖析器會產生要執行的任務。

因此,您可以使用設定或 State 來建構定義輸入任務命令列語法的剖析器。這已在上一節中說明。然後,您可以使用設定、State 或使用者輸入來建構要執行的任務。這在輸入任務語法中是隱含的。

使用其他輸入任務 

輸入任務中涉及的類型是可組合的,因此可以重複使用輸入任務。.parsed.evaluated 方法定義於 InputTasks 上,以便在常見情況下更方便地使用

  • InputTask[T]Initialize[InputTask[T]] 上呼叫 .parsed,以取得在剖析命令列之後建立的 Task[T]
  • InputTask[T]Initialize[InputTask[T]] 上呼叫 .evaluated,以從評估該任務取得 T 類型的值

在這兩種情況下,底層的 Parser 會與輸入任務定義中的其他剖析器排序。在 .evaluated 的情況下,會評估產生的任務。

下列範例會套用 run 輸入任務、常值分隔符號剖析器 -- 和再次 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 輸入,並產生一個可以正常使用的 task。例如,以下定義了一個普通的 task runFixed,可以被其他 task 使用或直接執行,而無需提供任何輸入。

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 中執行專案的主類別。也就是說,fork 設定會同時配置兩者,每個都有相同的類別路徑,並且每個都執行相同的主類別。然而,每個 task 都會將不同的參數傳遞給主類別。對於一個會回顯其參數的主類別 Demo,執行 runFixed2 的輸出可能看起來像這樣

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