タブ補完パーサー

このページでは sbt のパーサーコンビネータを解説する。これらのパーサーはユーザー入力をパースし、インプットタスクコマンドのタブ補完を提供するために使われる。

パーサーコンビネータは、小さなパーサー組み合わせてパーサーを組み立てる。最も基本的な用法では、Parser[A]String => Option[A] という関数だと考えることができる。パースする String を受け取り、パースが成功すれば Some に包んだ値を、失敗すれば None を返す。エラー処理とタブ補完により話は複雑になるが、本稿では Option に留める。

基本パーサー

最もシンプルなパーサーコンビネータは入力と完全に一致する:

import sbt.{ *, given }
import sbt.complete.DefaultParsers.{ *, given }

// 入力が 'x' なら成功して Char 'x' を返し、
//  それ以外は失敗するパーサー
val singleChar: Parser[Char] = 'x'

// 入力が "blue" なら成功して String "blue" を返し、
//  それ以外は失敗するパーサー
val litString: Parser[String] = "blue"

これらの例では、暗黙変換により Char または String からリテラル Parser が生成される。その他の基本パーサー構築子は charClasssuccessfailure メソッドである:

import sbt.{ *, given }
import sbt.complete.DefaultParsers.{ *, given }

// 文字が数字なら成功し、マッチした Char を返すパーサー。
//   第 2 引数 "digit" はパーサーを説明し、エラーメッセージで使われる
val digit: Parser[Char] = charClass((c: Char) => c.isDigit, "digit")

// 空の入力文字列なら値 3 を返し、それ以外は失敗するパーサー
val alwaysSucceed: Parser[Int] = success(3)

// 失敗を表す(入力 String に対して常に None を返す)。
//  引数はエラーメッセージである。
val alwaysFail: Parser[Nothing] = failure("Invalid input.")

組み込みパーサー

sbt には sbt.complete.DefaultParsers で定義された複数の組み込みパーサーが含まれる。

よく使われる組み込みパーサーは以下の通りである:

  • SpaceNotSpaceOptSpaceOptNotSpace はスペースまたは非スペースをパースし、必須か任意かを指定する。
  • StringBasic は引用符付きのテキストをパースする。
  • IntBasic は符号付き Int 値をパースする。
  • DigitHexDigit は 1 桁の 10 進または 16 進数字をパースする。
  • BoolBoolean 値をパースする

詳細は DefaultParsers API を参照。

パーサーの組み合わせ

これらの基本パーサーを土台に、より興味深いパーサーを構築する。パーサーを順序で組み合わせたり、パーサー間で選択したり、パーサーを繰り返したりできる。

// 入力が "blue" または "green" なら成功し、
//  マッチした入力を返すパーサー
val color: Parser[String] = "blue" | "green"

// "fg" または "bg" にマッチするパーサー
val select: Parser[String] = "fg" | "bg"

// "fg" または "bg"、スペース、そして色にマッチし、マッチした値を返すパーサー
val setColor: Parser[(String, Char, String)] =
  select ~ ' ' ~ color

// 多くの場合、上記のスペースのようにパーサーがマッチした値は不要である
//  そのため ~> または <~ を使い、
//  右または左のパーサーの結果をそれぞれ保持する
val setColor2: Parser[(String, String)]  =  select ~ (' ' ~> color)

// 1 桁以上の数字にマッチし、マッチした文字のリストを返す
val digits: Parser[Seq[Char]] = charClass(_.isDigit, "digit").+

// 0 桁以上の数字にマッチし、マッチした文字のリストを返す
val digits0: Parser[Seq[Char]] = charClass(_.isDigit, "digit").*

// オプションで 1 桁の数字にマッチする
val optDigit: Parser[Option[Char]] = charClass(_.isDigit, "digit").?

結果の変換

パーサーコンビネータの重要な側面は、結果をより有用なデータ構造へと変換することである。そのための基本メソッドは mapflatMap である。以下は map の例と、map の上に実装された便利メソッドである。

// `digits` パーサーを適用し、マッチした
//   文字シーケンスに提供された関数を適用する
val num: Parser[Int] = digits.map: (chars: Seq[Char]) =>
  chars.mkString.toInt }

// 数字文字にマッチし、マッチした文字を返す。入力が数字でなければ '0' を返す
val digitWithDefault: Parser[Char] = charClass(_.isDigit, "digit") ?? '0'

// 上記の例は以下と等価である:
val digitDefault: Parser[Char] =
  charClass(_.isDigit, "digit").?.map: (d: Option[Char]) =>
    d.getOrElse('0')

// 入力が "blue" なら成功し、値 4 を返す
val blue = "blue" ^^^ 4

// 上記は以下と等価である:
val blueM = "blue".map((s: String) => 4)

タブ補完の制御

ほとんどのパーサーは妥当なデフォルトのタブ補完動作を持つ。例えば、文字列および文字リテラルパーサーは空の入力文字列に対して基となるリテラルを提案する。しかしcharClass は任意の条件関数を受け付けるため、有効な補完を決定するのは現実的ではない。examples(...) はそのようなパーサーの明示的な補完を定義する:

val digit = charClass(_.isDigit, "digit").examples("0", "1", "2")

タブ補完は examples を提案として使う。タブ補完を制御するもう一つのメソッドは token である。token の主な目的は提案の境界を決めることである。例えば、パーサーが以下の場合:

("fg" | "bg") ~ ' ' ~ ("green" | "blue")

空入力時の候補補完は以下の通りである: console fg green fg blue bg green bg blue

通常、より小さなセグメントを提案したい。そうしないと提案数が扱いにくくなる。より良いパーサーは以下の通りである:

token( ("fg" | "bg") ~ ' ') ~ token("green" | "blue")

この場合、初期提案は(_ がスペースを表す): console fg_ bg_ となる。

token("green" ~ token("blue")) のようにトークンを重ねたりネストしたりしないこと。動作は未定義であり(将来的にはエラーを生成する予定)、通常は最も外側のトークン定義が使われる。

依存パーサー

パーサーがデータを解析した後、その結果に依存してさらにデータをパースする必要がある場合がある。この動作を得るには flatMap 関数を使う。

例として、有効なリストから複数項目を補完付きで選択する方法を示す。重複は不可能である。項目の区切りにはスペースを使う。

def select1(items: Iterable[String]) =
  token(Space ~> StringBasic.examples(FixedSetExamples(items)))

def selectSome(items: Seq[String]): Parser[Seq[String]] = {
   select1(items).flatMap: v =>
     val remaining = items.filter(_ != v)
     if remaining.size == 0 then success(v :: Nil)
     else selectSome(remaining).?.map(v +: _.getOrElse(Seq()))
 }

この例から分かる通り、flatMap 関数は前の値を提供する。この情報で、残りの項目用に新しいパーサーが構築される。パーサーの出力を変換するために map コンビネータも使われる。

パーサーは、選択肢がなくなる自明なケースに達するまで再帰的に呼ばれる。