この記事を読むと分かること
- 複数個の入力を受け付ける sbt タスクの作り方
- 選択済みの候補が補完から除かれていく sbt パーサの作り方
- 補完の一覧から正規表現で候補を選択できる sbt パーサの作り方
導入
まずは最小構成のプロジェクトを用意
build.sbt
lazy val root = project.settings(HelloSettings.task)
タブ補完を試すためのモジュールも定義
project/HelloSettings.scala
import sbt.Def
import sbt.Def.inputKey
import sbt.Keys.streams
object HelloSettings {
lazy val hello = inputKey[Unit]("hello, parser")
def task = hello := {
println("selected items..")
val items = Def.setting(sampleParser).parsed
items foreach println
}
private def sampleParser: Parser[Seq[String]] = ???
}
この Parser[Seq[String]] = ???
をいい感じに作り上げるのが今回の主旨です
公式ドキュメントの読解
若干不親切ながらも、目的のパーサに近いコード (Dependent parsers) が申しわけ程度に記載されています
ほぼそのまま抜粋 :
project/SampleParser.scala
import sbt.complete.DefaultParsers._
import sbt.complete.{FixedSetExamples, Parser}
object SampleParser {
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)
success(v :: Nil)
else
selectSome(remaining).?.map(v +: _.getOrElse(Seq()))
}
}
}
先の実装を埋めるとこんな感じです
project/HelloSettings.scala
private def sampleParser =
SampleParser selectSome Seq(
"foo1.xml",
"foo2.xml",
"foo3.txt",
"bar1.yml"
)
試してみましょう
> hello
bar1.yml foo1.xml foo2.xml foo3.txt
> hello foo
foo1.xml foo2.xml foo3.txt
> hello foo1.xml foo
foo2.xml foo3.txt
> hello foo1.xml foo2.xml
selected items..
foo1.xml
foo2.xml
このままでも一応
- 指定した文字列のみが候補に表示されて
- 最初に foo を入力すると foo で始まる候補に絞られて
- foo1.xml を選択すると次からは foo1.xml が候補から除かれる
ので、動作は概ね問題ないようにも見えますが…
> hello bazbazbaz
selected items..
bazbazbaz
> hello .*xml
selected items..
.*xml
- 一覧にない不正な文字列も許可してしまう
- 複数個の .xml を一度に選択するようなことはできない
という点で残念ながら使い勝手はイマイチです
指定した候補のみを許可したい
ここまでで判明していること :
-
flatMap
によって「選択した文字列に応じて次にマッチする文字列が変わる」パーサを実現できる -
token
を|
でつなげれば複数の候補にマッチするParser[String]
を作れる
を押さえてあればあとはシンプルです
StrictParser.scala
import sbt.complete.DefaultParsers.{Space, failure, token}
import sbt.complete.Parser
object StrictParser {
def from(items: Seq[String]): Parser[Seq[String]] = {
new StrictParser(items).parser
}
}
class StrictParser private (items: Seq[String]) {
private type Filter = String => Boolean
def parser: Parser[Seq[String]] = {
val fixed: Parser[Filter] = {
val base = items map (token(_)) reduceOption (_ | _)
base getOrElse failure("none") map (item => _ == item)
}
(Space ~> fixed flatMap next) ?? Nil
}
private def next(filter: Filter): Parser[Seq[String]] = {
val (consumed, remains) = items partition filter
if (consumed.nonEmpty){
StrictParser from remains map (consumed ++ _)
} else {
failure("input not matched")
}
}
}
使い方はさっきとほとんど同じです
project/HelloSettings.scala
private def strictParser: Parser[Seq[String]] =
StrictParser from Seq(
"foo1.xml",
"foo2.xml",
"foo3.txt",
"bar1.yml"
)
試してみましょう
> hello
bar1.yml foo1.xml foo2.xml foo3.txt
> hello foo
foo1.xml foo2.xml foo3.txt
> hello foo1.xml
bar1.yml foo2.xml foo3.txt
> hello foo1.xml foo
foo2.xml foo3.txt
> hello foo1.xml foo2.xml
selected items..
foo1.xml
foo2.xml
> hello foo1.xml foo2.xml bazbazbaz
[error] Expected 'bar1.yml'
[error] hello foo1.xml foo2.xml bazbazbaz
[error] ^
不正な文字列を無事に弾くことができるようになりました
正規表現で候補を選択したい
上記の StrictParser
に
- 正規表現を受け付けるために
String#matches
による判定を追加 - ただし不正な入力に対しては例外が吐かれるので
Try
で捕捉
という変更を加えるだけで済みます
ReductiveParser.scala
import sbt.complete.DefaultParsers.{NotSpace, Space, failure, token}
import sbt.complete.Parser
import scala.util.{Failure, Success, Try}
object ReductiveParser {
def from(items: Seq[String]): Parser[Seq[String]] = {
new ReductiveParser(items).parser
}
}
class ReductiveParser private (items: Seq[String]) {
private type Filter = String => Boolean
def parser: Parser[Seq[String]] = {
val fixed: Parser[Filter] = {
val base = items map (token(_)) reduceOption (_ | _)
base getOrElse failure("none") map (item => _ == item)
}
val manually: Parser[Filter] = {
val base = NotSpace
base map (input => _ matches input)
}
(Space ~> (fixed | manually) flatMap next) ?? Nil
}
private def next(filter: Filter): Parser[Seq[String]] =
Try(items partition filter) match {
case Success((consumed, remains)) if consumed.nonEmpty =>
ReductiveParser from remains map (consumed ++ _)
case Success(_) =>
failure("input not matched")
case Failure(e) =>
failure(s"invalid input: ${e.getMessage}")
}
}
試してみましょう
> hello .*xml
bar1.yml foo3.txt
> hello .*xml bar1.yml
selected items..
foo1.xml
foo2.xml
bar1.yml
> hello .*1.* foo
foo2.xml foo3.txt
> hello .*1.* foo2.xml
selected items..
foo1.xml
bar1.yml
foo2.xml
正規表現によって選択できるようになりました
> hello .*conf
[error] input not matched
[error] hello .*conf
[error] ^
> hello (*)
[error] invalid input: Dangling meta character '*' near index 1
[error] (*)
[error] ^
不正な入力も問題なく検出できています
めでたしめでたし
終わり
備考
- 動作確認コードの置き場所 : github.com - Cliche/scala/sbt-teal