インプットタスク

sbt はユーザー入力をパースし、タブ補完を提供するカスタムタスクを定義する機能を提供する。パーサーの詳細は後でタブ補完パーサー で説明する。

このページでは、パーサーコンビネータをインプットタスク・システムに組み込む方法を説明する。

Input キー

インプットタスクのキーは InputKey 型であり、SettingKey がセッティングを、TaskKey がタスクを表すのと同様にインプットタスクを表す。inputKey.apply ファクトリメソッドで新しいインプットタスクのキーを定義する:

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

インプットタスクの定義は通常のタスクと同様だが、ユーザー入力を捕獲した Parser の結果も使用できる。セッティングやタスクの値を取得する特殊な value メソッドと同様に、Parser の結果は特殊な parsed メソッドで取得する。

基本的なインプットタスクの定義

最もシンプルなインプットタスクはスペース区切りの引数シーケンスを受け付ける。パースは簡素で、有用なタブ補完は提供されない。スペース区切り引数用の組み込みパーサーは spaceDelimited メソッドで構築され、唯一の引数としてタブ補完時にユーザーに表示するラベルを受け取る。

例えば、以下のタスクは現在の 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(_))
}

Parser を使ったインプットタスク

spaceDelimited メソッドが提供する Parser は入力構文の定義に柔軟性がない。カスタムパーサーを使うには、タブ補完パーサーで説明する通り独自の Parser を定義すればよい。

Parser の構築

最初のステップは、以下のいずれかの型の値を定義して実際の Parser を構築することである:

  1. Parser[I]: セッティングを使用しない基本パーサー
  2. Initialize[Parser[I]]: 定義が 1 つ以上のセッティングに依存するパーサー
  3. Initialize[State => Parser[I]]: セッティングと現在の state の両方を使って定義されるパーサー

spaceDelimited による最初のケースの例は既に見た。定義でセッティングを使用していない。3 つ目のケースの例として、以下はプロジェクトの 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) )
}

この Parser 定義は (String,String) 型の値を生成する。定義した入力構文は柔軟ではない。デモンストレーションである。パースが成功すると以下のいずれかの値を生成する(現在の Scala バージョンが 3.8.1、sbt バージョンが 2.0.0-RC9、実行待ちコマンドが 3 つの場合):

  • (scala,3.8.1)
  • (sbt,2.0.0-RC9)
  • (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. パーサーはユーザー入力を受け付け、タブ補完を提供する。
  4. パーサーは実行するタスクを生成する。

つまり、セッティングまたは State を使って インプットタスク のコマンドライン構文を定義するパーサーを構築できる。これは前セクションで説明した。次に、セッティング、State、またはユーザー入力を使って実行するタスクを構築できる。これは インプットタスク 構文に暗黙的に含まれる。

他のインプットタスクの再利用

インプットタスクに関わる型は合成可能であるため、インプットタスクを再利用できる。InputTask には .parsed.evaluated メソッドが定義され、一般的な状況で便利になる:

  • InputTask[A1] または Initialize[InputTask[A1]].parsed を呼ぶと、コマンドラインをパースした後に作成された Task[A1] を取得する
  • InputTask[A1] または Initialize[InputTask[A1]].evaluated を呼ぶと、そのタスクを評価した型 A1 の値を取得する

いずれの場合も、基となる Parser はインプットタスク定義内の他のパーサーと順序付けられる。.evaluated の場合は、生成されたタスクが評価される。

以下の例は run インプットタスク、リテラル区切りパーサー --、そして再度 run を適用する。パーサーは構文的な出現順に並べられるため、-- の前の引数は最初の run に、後の引数は 2 番目の 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

入力の事前適用

InputTaskParser から構築されるため、プログラムで入力を適用して新しい InputTask を生成できる。(Task を生成することも可能で、次セクションで説明する。)適用する String を受け取る 2 つの便利メソッドが InputTask[T]Initialize[InputTask[T]] に用意されている。

  • partialInput は入力を適用し、コマンドラインなどからの追加入力を許可する
  • fullInput は入力を適用してパースを終了し、追加入力を受け付けない

いずれの場合も、入力は インプットタスク のパーサーに適用される。インプットタスク はタスク名以降のすべての入力を扱うため、入力に先頭の空白を含める必要があることが多い。

前セクションの例を考える。以下のように変更できる:

  • 最初の run への引数をすべて明示的に指定する。nameversion を使って、セッティングでパーサーを定義・変更できることを示す。
  • 2 番目の run に渡す初期引数を定義するが、コマンドラインからの追加入力を許可する。

Note

入力がセッティングに由来する場合は、例えば 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 を導出する方法を示した。このセクションでは、入力を適用すると 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!")
}

引数をエコーするメインクラス Demo の場合、runFixed の実行は以下のようになる:

$ 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 セッティングが両方に適用され、同じクラスパスを持ち、同じメインクラスを実行する。ただし、各タスクはメインクラスに異なる引数を渡す。引数をエコーするメインクラス Demo の場合、runFixed2 の実行出力は以下のようになる:

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