Tab 补全解析器

本页介绍 sbt 中的解析器组合子。这些解析器用于解析用户输入,并为输入任务命令提供 Tab 补全。

解析器组合子由较小的解析器构建解析器。Parser[A] 最基本用法是函数 String => Option[A]。它接受要解析的 String,解析成功时产生包装在 Some 中的值,失败时产生 None。错误处理和 Tab 补全使情况更复杂,但本文仅讨论 Option

基本解析器

最简单的解析器组合子匹配精确输入:

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

// A parser that succeeds if the input is 'x', returning the Char 'x'
//  and failing otherwise
val singleChar: Parser[Char] = 'x'

// A parser that succeeds if the input is "blue", returning the String "blue"
//   and failing otherwise
val litString: Parser[String] = "blue"

在这些示例中,隐式转换从 CharString 产生字面 Parser。其他基本解析器构造器为 charClasssuccessfailure 方法:

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

// A parser that succeeds if the character is a digit, returning the matched Char
//   The second argument, "digit", describes the parser and is used in error messages
val digit: Parser[Char] = charClass((c: Char) => c.isDigit, "digit")

// A parser that produces the value 3 for an empty input string, fails otherwise
val alwaysSucceed: Parser[Int] = success(3)

// Represents failure (always returns None for an input String).
//  The argument is the error message.
val alwaysFail: Parser[Nothing] = failure("Invalid input.")

内置解析器

sbt 在 sbt.complete.DefaultParsers 中定义了若干内置解析器。

常用内置解析器包括:

  • SpaceNotSpaceOptSpaceOptNotSpace 用于解析空格或非空格,必需或可选。
  • StringBasic 用于解析可能带引号的文本。
  • IntBasic 用于解析有符号 Int 值。
  • DigitHexDigit 用于解析单个十进制或十六进制数字。
  • Bool 用于解析 Boolean

详情请参阅 DefaultParsers API

组合解析器

我们在这些基本解析器基础上构建更有用的解析器。可将解析器按序组合、在解析器间选择或重复解析器。

// A parser that succeeds if the input is "blue" or "green",
//  returning the matched input
val color: Parser[String] = "blue" | "green"

// A parser that matches either "fg" or "bg"
val select: Parser[String] = "fg" | "bg"

// A parser that matches "fg" or "bg", a space, and then the color, returning the matched values.
val setColor: Parser[(String, Char, String)] =
  select ~ ' ' ~ color

// Often, we don't care about the value matched by a parser, such as the space above
//  For this, we can use ~> or <~, which keep the result of
//  the parser on the right or left, respectively
val setColor2: Parser[(String, String)]  =  select ~ (' ' ~> color)

// Match one or more digits, returning a list of the matched characters
val digits: Parser[Seq[Char]] = charClass(_.isDigit, "digit").+

// Match zero or more digits, returning a list of the matched characters
val digits0: Parser[Seq[Char]] = charClass(_.isDigit, "digit").*

// Optionally match a digit
val optDigit: Parser[Option[Char]] = charClass(_.isDigit, "digit").?

转换结果

解析器组合子的关键是在解析过程中将结果转换为更有用的数据结构。基本方法为 mapflatMap。以下是 map 及基于 map 实现的便捷方法示例。

// Apply the `digits` parser and apply the provided function to the matched
//   character sequence
val num: Parser[Int] = digits.map: (chars: Seq[Char]) =>
  chars.mkString.toInt }

// Match a digit character, returning the matched character or return '0' if the input is not a digit
val digitWithDefault: Parser[Char] = charClass(_.isDigit, "digit") ?? '0'

// The previous example is equivalent to:
val digitDefault: Parser[Char] =
  charClass(_.isDigit, "digit").?.map: (d: Option[Char]) =>
    d.getOrElse('0')

// Succeed if the input is "blue" and return the value 4
val blue = "blue" ^^^ 4

// The above is equivalent to:
val blueM = "blue".map((s: String) => 4)

控制 Tab 补全

大多数解析器有合理的默认 Tab 补全行为。例如,字符串和字符字面解析器会在空输入时建议底层字面。但 charClass 接受任意谓词,难以确定有效补全。examples 方法为此类解析器定义显式补全:

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

Tab 补全将使用 examples 作为建议。另一种控制 Tab 补全的方法是 tokentoken 的主要用途是确定建议边界。例如,若您的解析器为:

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

则空输入时的潜在补全为:console fg green fg blue bg green bg blue

通常您希望建议更小的片段,否则建议数量会难以管理。更好的解析器为:

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

此时,初始建议为(_ 表示空格):console fg_ bg_

注意不要重叠或嵌套 token,如 token("green" ~ token("blue"))。行为未指定(未来应会报错),但通常使用最外层的 token 定义。

依赖解析器

有时解析器需先分析部分数据,再解析更多依赖前者的数据。实现此行为的关键是使用 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 组合子也用于转换解析器输出。

解析器递归调用,直至找到无可选选择的平凡情况。