タブ補完パーサー
このページでは 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 が生成される。その他の基本パーサー構築子は charClass、success、failure メソッドである:
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 で定義された複数の組み込みパーサーが含まれる。
よく使われる組み込みパーサーは以下の通りである:
Space、NotSpace、OptSpace、OptNotSpaceはスペースまたは非スペースをパースし、必須か任意かを指定する。StringBasicは引用符付きのテキストをパースする。IntBasicは符号付き Int 値をパースする。DigitとHexDigitは 1 桁の 10 進または 16 進数字をパースする。BoolはBoolean値をパースする
詳細は 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").?
結果の変換
パーサーコンビネータの重要な側面は、結果をより有用なデータ構造へと変換することである。そのための基本メソッドは map と flatMap である。以下は 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 コンビネータも使われる。
パーサーは、選択肢がなくなる自明なケースに達するまで再帰的に呼ばれる。