使用 Scala 实现 sbt.boot.properties 的函数式解析

本文介绍了如何使用Scala实现对sbt.boot.properties文件的函数式解析。首先,文章解释了文件的格式,并定义了AST(抽象语法树)的类结构。接着,详细阐述了解析过程,包括解析函数的实现,以及如何通过模式匹配进行递归解析。此外,文章还讨论了插入、删除配置的操作,并展示了搜索功能如何利用Scala内置函数。最后,作者总结了递归和不可变性在该解析器设计中的重要性,并提供了完整代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

之前在实现构建工具自动配置的小工具的时候, 需要解析 sbt.boot.properties 的内容, 看一下如何实现一个解析器

sbt.boot.properties 文件格式

官网 有个配置示例, 文件格式还是比较清晰的, 上面有个分块的标签, 然后每个分块里面每一行都是 key:value 这样的格式.
在这里插入图片描述
我们的目的是, 将 String 转换成 我们的 AST (抽象语法树), 然后使用 AST 进行查询和插入操作.

实现

AST

实现我们要用类来表达配置内容的格式:
在这里插入图片描述
我们使用Tag来表示方括号部分,

case class Tag(private val name:String)

ConfigLine 表示一行配置

case class ConfigLine(name:String,value:String)

使用 Paragraph 来表示由一个 Tag 多个 ConfigLine 组成的段落.

case class Paragraph(tag:Tag, configs: List[ConfigLine] = List.empty)

然后整个配置文件由 SBTProperties 来表示

case class SBTProperties(paragraphs: List[Paragraph])

解析

要实现一个解析器, 方法签名如下, 输入的原始内容是 List[String], 输出是上面定义的 SBTProperties

def fromLines(lines:List[String]) : SBTProperties

解析 Paragraph

函数式解析使用的是不变的数据结构, 利用模式匹配递归地解析自己那部分数据, 然后把剩余的部分数据返回.

    @tailrec
    def parseParagraph(lines:List[String],paragraph: Option[Paragraph] = None): (Option[Paragraph],List[String]) = {
      lines.headOption match {							
        case None =>									   //第一行如果为空, 直接返回		
          paragraph -> List.empty				
        case Some(headLine) =>							   //如果第一行不为空
          val trim = headLine.trim
          if(trim.startsWith("[") && trim.endsWith("]")) { //以 [ 开头, ] 结尾的是 tag
            if(paragraph.nonEmpty) {//如果当前有`paragraph`,则直接返回, 因为这里遇到的 tag 是下个 `paragraph` 了
              paragraph -> lines
            } else {//如果当前没有`paragraph`, 则新建一个 `paragraph` 并且用剩下的 lines 去递归地解析
              parseParagraph(lines.tail,Some(Paragraph(Tag(trim))))
            }
          } else if(trim.isEmpty){//忽略空行
            parseParagraph(lines.tail,paragraph)
          } else {//如果是ConfigLine,则添加到当前的 `paragraph` 里面
            parseParagraph(lines.tail,paragraph.map(p => p.copy(configs = p.configs :+ new ConfigLine(trim) )))
          }
      }
    }

解析 SBTProperties

这个相对简单, 就是调用 parseParagraph 而已

    @tailrec
    def parseSBTProperties(lines:List[String],current:SBTProperties) : SBTProperties = {
      if lines.isEmpty then
        current
      else
        val (paragraph,tailLine) = parseParagraph(lines,None)
        paragraph match {
          case None =>
            parseSBTProperties(tailLine,current)
          case Some(value) =>
            parseSBTProperties(tailLine,current.copy(paragraphs = current.paragraphs :+ value))
        }
    }

插入/删除配置的操作

我们实现 Paragraph 的插入, 这样 SBTProperties 的插入调用 Paragraph 的方法即可:

case class Paragraph(tag:Tag, configs: List[ConfigLine] = List.empty) {
  /**
   * 插入到 afterName 后面, 如果 afterName 不存在, 则插入到 tag 后面(也就是放在 configs 开头)
   */
  def insert(name:String,value:String,afterName:String) : Paragraph = {
    val newConfigLine =  new ConfigLine(name,value)
    @tailrec
    def go(confs:List[ConfigLine],acc:List[ConfigLine]) : List[ConfigLine] = {
      confs match {
        case Nil => //confs 为空了, 说明没有匹配的 afterName, 添加到 acc 头部, 也就是 tag 后面
          newConfigLine +: acc
        case head::tail => //不为空
          if head.name == afterName.trim then
            (acc :+ head :+ newConfigLine) ++ tail
          else
            go(tail,acc :+ head)
      }
    }
    this.copy(configs = go(configs,List.empty))
  }

  //移除配置,这里简单实现, 直接使用 filterNot 即可
  def remove(name:String) : Paragraph = {
    this.copy(configs = configs.filterNot(_.name == name))
  }
}

实现 SBTProperties 的插入 和 删除, 先按照 tag 搜索 paragraph, 然后调用 paragraph 的删除方法

case class SBTProperties(paragraphs: List[Paragraph]) {
  def insert(tag:String,name:String,value:String,afterName:String) : SBTProperties = {
    val insert = new ConfigLine(name,value)
    @tailrec
    def go(paragraphs:List[Paragraph],acc:List[Paragraph] = List.empty) : List[Paragraph] = {
      paragraphs match {
        case Nil =>
          acc :+ Paragraph(Tag.trim(tag),List(insert))
        case ::(head, next) =>
          if head.tag.is(tag) then
            head.insert(name,value, afterName) +: next
          else
            go(next,acc :+ head)
      }
    }
    SBTProperties(go(this.paragraphs))
  }

  def remove(tag:String,name:String) : SBTProperties = {
    this.copy {
      paragraphs.collect {
        case p if p.tag.is(name) => p.remove(name)
        case p =>  p
      }
    }
  }
}

搜索

语法树出来之后, 搜索其实就是遍历, 当然scala内置了很多函数,直接用就行了

case class SBTProperties(paragraphs: List[Paragraph]) {
  def isEmpty:Boolean = paragraphs.isEmpty

  def queryFirstParagraph(tag:String) : Option[Paragraph] = {
    paragraphs.find(p => p.tag.is(tag))
  }

  def queryFirst(tag:String,name:String):Option[ConfigLine] = {
    queryFirstParagraph(tag).flatMap { paragraph =>
      paragraph.configs.find(_.name == name)
    }
  }
  def queryFirst(tag:String,targetValue:String => Boolean): Option[ConfigLine] = {
    paragraphs.find(p => p.tag.is(tag)).flatMap { paragraph =>
      paragraph.configs.find(config => targetValue(config.value))
    }
  }
}

结论

  1. 递归是算法中常用的方式
  2. 不变的方式往往需要传递值

完整代码

import scala.annotation.tailrec


/**
 * @param line Key: Value 这样的形式
 */
case class ConfigLine(name:String,value:String) {
  def this(line:String) = {
    this(line.trim.takeWhile(_ != ':').trim,line.trim.dropWhile(_ != ':').tail.trim)
  }

  override def toString: String = s"$name: $value"
}

case class Tag private (name:String) {
  def is(_name:String):Boolean = Tag.format(name) == Tag.format(_name)
  override def toString : String = Tag.format(name)
}

object Tag {
  def format(_name:String) : String = s"[${_name.trim.stripMargin('[').stripSuffix("]")}]"
  def trim(string:String):Tag = Tag(format(string))
}

/**
 * 表示配置文件里面的一个段落 <br/>
 * ```
 * [tag] <br/>
 * name : value <br/>
 * name : value <br/>
 * ```
 */
case class Paragraph(tag:Tag, configs: List[ConfigLine] = List.empty) {
  /**
   * 插入到 after 后面, 如果 after 不存在, 则插入到 tag 后面
   */
  def insert(name:String,value:String,afterName:String) : Paragraph = {
    val newConfigLine =  new ConfigLine(name,value)
    @tailrec
    def go(confs:List[ConfigLine],acc:List[ConfigLine]) : List[ConfigLine] = {
      confs match {
        case Nil =>
          newConfigLine +: acc
        case head::tail =>
          if confs.head.name == afterName.trim then
            (acc :+ head :+ newConfigLine) ++ tail
          else
            go(tail,acc :+ head)
      }
    }
    this.copy(configs = go(configs,List.empty))
  }

  def remove(name:String) : Paragraph = {
    this.copy(configs = configs.filterNot(_.name == name))
  }
}

/**
 * 表示一个配置文件
 * @param paragraphs
 */
case class SBTProperties(paragraphs: List[Paragraph]) {
  
  def isEmpty:Boolean = paragraphs.isEmpty
  
  def queryFirstParagraph(tag:String) : Option[Paragraph] = {
    paragraphs.find(p => p.tag.is(tag))
  }
  
  def queryFirst(tag:String,name:String):Option[ConfigLine] = {
    queryFirstParagraph(tag).flatMap { paragraph =>
      paragraph.configs.find(_.name == name)
    }
  }
  def queryFirst(tag:String,targetValue:String => Boolean): Option[ConfigLine] = {
    paragraphs.find(p => p.tag.is(tag)).flatMap { paragraph =>
      paragraph.configs.find(config => targetValue(config.value))
    }
  }

  def insert(tag:String,name:String,value:String,afterName:String) : SBTProperties = {
    val insert = new ConfigLine(name,value)
    @tailrec
    def go(paragraphs:List[Paragraph],acc:List[Paragraph] = List.empty) : List[Paragraph] = {
      paragraphs match {
        case Nil =>
          acc :+ Paragraph(Tag.trim(tag),List(insert))
        case ::(head, next) =>
          if head.tag.is(tag) then
            head.insert(name,value, afterName) +: next
          else
            go(next,acc :+ head)
      }
    }
    SBTProperties(go(this.paragraphs))
  }

  def remove(tag:String,name:String) : SBTProperties = {
    this.copy {
      paragraphs.collect {
        case p if p.tag.is(name) =>
          p.remove(name)
        case p =>
          p
      }
    }
  }
}
object SBTProperties {

  def fromLines(lines:List[String]) : SBTProperties = {

    //解析段落, 没有 tag 部分的配置会被删除
    @tailrec
    def parseParagraph(lines:List[String],paragraph: Option[Paragraph] = None): (Option[Paragraph],List[String]) = {
      lines.headOption match{
        case None =>
          paragraph -> List.empty
        case Some(headLine) =>
          val trim = headLine.trim
          if(trim.startsWith("[") && trim.endsWith("]")) { //tag
            if(paragraph.nonEmpty) {
              paragraph -> lines
            } else {
              parseParagraph(lines.tail,Some(Paragraph(Tag.trim(trim))))
            }
          } else if(trim.isEmpty){
            parseParagraph(lines.tail,paragraph)
          } else { //configLine
            parseParagraph(lines.tail,paragraph.map(p => p.copy(configs = p.configs :+ new ConfigLine(trim) )))
          }
      }
    }

    @tailrec
    def parseSBTProperties(lines:List[String],current:SBTProperties) : SBTProperties = {
      if lines.isEmpty then
        current
      else
        val (paragraph,tailLine) = parseParagraph(lines,None)
        paragraph match {
          case None =>
            parseSBTProperties(tailLine,current)
          case Some(value) =>
            parseSBTProperties(tailLine,current.copy(paragraphs = current.paragraphs :+ value))
        }
    }

    parseSBTProperties(lines,SBTProperties(List.empty))

  }
  
  trait ToStrList[From] {
    extension (from:From)
      def strList:List[String]
  }

  given ToStrList[ConfigLine] with 
    extension (configLine:ConfigLine) 
      def strList:List[String] =
        List(s"    ${configLine.toString}")
        
  given (using ToStrList[ConfigLine]):ToStrList[Paragraph] with 
    extension (paragraph:Paragraph)
      def strList:List[String] =
        List(paragraph.tag.toString) ++
          paragraph.configs.flatMap(_.strList)
  
  given (using ToStrList[Paragraph]): ToStrList[SBTProperties] with 
    extension (props:SBTProperties)
      def strList:List[String] =
        props.paragraphs.flatMap(_.strList)
      
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值