如何为统一的diff语法编写解析器

时间:2010-08-24 19:04:53

标签: parsing scala parser-combinators

我应该使用RegexParsers,StandardTokenParsers还是完全适合解析这种语法?可以从here找到语法示例。

4 个答案:

答案 0 :(得分:5)

我会使用正则表达式。它简化了一些事情,并使其余部分成为标准。

def process(src: scala.io.Source) {
  import scala.util.matching.Regex

  val FilePattern = """(.*) ''(.*)''"""
  val OriginalFile = new Regex("--- "+FilePattern, "path", "timestamp")
  val NewFile = new Regex("+++ "+FilePattern, "path", "timestamp")
  val Chunk = new Regex("""@@ -(\d+),(\d+) +(\d+),(\d+) @@""", "orgStarting", "orgSize", "newStarting", "newSize")
  val AddedLine = """+(.*)""".r
  val RemovedLine = """-(.*)""".r
  val UnchangedLine = """ (.*)""".r

  src.getLines() foreach {
    case OriginalFile(path, timestamp) => println("Original file: "+path)
    case NewFile(path, timestamp) => println("New file: "+path)
    case Chunk(l1, s1, l2, s2) => println("Modifying %d lines at line %d, to %d lines at %d" format (s1, l1, s2, l2))
    case AddedLine(line) => println("Adding line "+line)
    case RemovedLine(line) => println("Removing line "+line)
    case UnchangedLine(line) => println("Keeping line "+line)
  }
}

答案 1 :(得分:4)

此格式设计为易于解析,您可以在没有任何正则表达式的情况下执行此操作,也无需对输入进行标记。只需一行一行地查看前几个字符。文件头和块标题需要更多的关注,但是对于split来说,这是你无法做到的。

当然,如果你想学习如何使用一些解析库,那就去吧。

答案 2 :(得分:3)

以下是使用RegexParsers的解决方案。

import scala.util.parsing.combinator.RegexParsers

object UnifiedDiffParser extends RegexParsers {

  // case classes representing the data of the diff
  case class UnifiedDiff(oldFile: File, newFile: File, changeChunks: List[ChangeChunk])
  case class File(name: String, timeStamp: String)
  case class ChangeChunk(rangeInformation: RangeInformation, changeLines: List[String])
  case class RangeInformation(oldOffset: Int, oldLength: Int, newOffset: Int, newLength: Int)

  override def skipWhitespace = false

  def unifiedDiff: Parser[UnifiedDiff] = oldFile ~ newFile ~ rep1(changeChunk) ^^ {
    case of ~ nf ~ l => UnifiedDiff(of, nf, l)
  }   

  def oldFile: Parser[File] = ("--- " ~> filename) ~ ("""\s+""".r ~> timestamp <~ newline) ^^ {
    case f~t => File(f, t)
  }   
  def newFile: Parser[File] = ("+++ " ~> filename) ~ ("""\s+""".r ~> timestamp <~ newline) ^^ {
    case f~t => File(f, t)
  }   
  def filename: Parser[String] = """[\S]+""".r
  def timestamp: Parser[String] = """.*""".r

  def changeChunk: Parser[ChangeChunk] = rangeInformation ~ (newline ~> rep1(lineChange)) ^^ {
    case ri ~ l => ChangeChunk(ri, l)
  }   
  def rangeInformation: Parser[RangeInformation] = ("@@ " ~> "-" ~> number) ~ ("," ~> number) ~ (" +" ~> number) ~ ("," ~> number) <~ " @@" ^^ {
    case a ~ b ~ c ~ d => RangeInformation(a, b, c, d)
  }   

  def lineChange: Parser[String] = contextLine | addedLine | deletedLine
  def contextLine: Parser[String] = """ .*""".r <~ newline
  def addedLine: Parser[String] = """\+.*""".r <~ newline
  def deletedLine: Parser[String] = """-.*""".r <~ newline

  def newline: Parser[String] = """\n""".r
  def number: Parser[Int] = """\d+""".r ^^ {_.toInt}

  def main(args: Array[String]) {
    val reader = { 
      if (args.length == 0) {
        // read from stdin
        Console.in
      } else {
        new java.io.FileReader(args(0))
      }   
    }   
    println(parseAll(unifiedDiff, reader))
  }   
}   

答案 3 :(得分:1)

在寻找为git diff构建Scala解析器时偶然发现了这一点,这是通过运行git diff-tree生成的。这与统一差异很相似,但确实有一些有趣的变体。

我严重依赖上面的答案,最后写了这里包含的解析器。当然,这并不是原始海报的严格意义,但我认为它可能对其他人有用。

import util.parsing.combinator._

object GitDiff {
  // file names have "a/" or "b/" as prefix, need to drop that to compare
  def apply (files: (String,String), op: FileOperation, chunks: List[ChangeChunk]) = {
    def strip(s: String) = s.dropWhile(_ != '/').drop(1)
    new GitDiff( strip( files._1 ), strip( files._2 ), op, chunks )
  }
}

case class GitDiff(oldFile: String, newFile: String, op: FileOperation, chunks: List[ChangeChunk]) {
  val isRename = oldFile != newFile
}

sealed trait FileOperation
case class NewFile(mode: Int) extends FileOperation
case class DeletedFile(mode: Int) extends FileOperation
case object UpdatedFile extends FileOperation

sealed trait LineChange { def line: String }
case class ContextLine(line: String) extends LineChange
case class LineRemoved(line: String) extends LineChange
case class LineAdded(line: String) extends LineChange
case class RangeInformation(oldOffset: Int, oldLength: Int, newOffset: Int, newLength: Int)
case class ChangeChunk(rangeInformation: RangeInformation, changeLines: List[LineChange])

// Code taken from http://stackoverflow.com/questions/3560073/how-to-write-parser-for-unified-diff-syntax
object GitDiffParser extends RegexParsers {

  override def skipWhitespace = false

  def allDiffs: Parser[List[GitDiff]] = rep1(gitDiff)

  def gitDiff: Parser[GitDiff] = filesChanged ~ fileOperation ~ diffChunks ^^ {
    case files ~ op ~ chunks => GitDiff(files, op, chunks)
  }

  def filesChanged: Parser[(String, String)] =
    "diff --git " ~> filename ~ (" " ~> filename) <~ newline ^^ { case f1 ~ f2 => (f1,f2) }

  def fileOperation: Parser[FileOperation] =
    opt(deletedFileMode | newFileMode) <~ index ^^ { _ getOrElse UpdatedFile }

  def index: Parser[Any] = ( "index " ~ hash ~ ".." ~ hash ) ~> opt(" " ~> mode) <~ newline
  def deletedFileMode: Parser[DeletedFile] = "deleted file mode " ~> mode <~ newline ^^ { m => DeletedFile(m) }
  def newFileMode: Parser[NewFile] = "new file mode " ~> mode <~ newline ^^ { m => NewFile(m) }
  def hash: Parser[String] = """[0-9a-f]{7}""".r
  def mode: Parser[Int] = """\d{6}""".r ^^ { _.toInt }

  def diffChunks: Parser[List[ChangeChunk]] = (oldFile ~ newFile) ~> rep1(changeChunk)

  def oldFile: Parser[String] = "--- " ~> filename <~ newline
  def newFile: Parser[String] = "+++ " ~> filename <~ newline
  def filename: Parser[String] = """[\S]+""".r

  def changeChunk: Parser[ChangeChunk] = rangeInformation ~ opt(contextLine) ~ (opt(newline) ~> rep1(lineChange)) ^^ {
    case ri ~ opCtx ~ lines => ChangeChunk(ri, opCtx map (_ :: lines) getOrElse (lines))
  }
  def rangeInformation: Parser[RangeInformation] =
    ("@@ " ~> "-" ~> number) ~ opt("," ~> number) ~ (" +" ~> number) ~ opt("," ~> number) <~ " @@" ^^ {
      case a ~ b ~ c ~ d => RangeInformation(a, b getOrElse 0, c, d getOrElse 0)
    }

  def lineChange: Parser[LineChange] = contextLine | addedLine | deletedLine
  def contextLine: Parser[ContextLine] = " " ~> """.*""".r <~ newline ^^ { l => ContextLine(l) }
  def addedLine: Parser[LineAdded] = "+" ~> """.*""".r <~ newline ^^ { l => LineAdded(l) }
  def deletedLine: Parser[LineRemoved] = "-" ~> """.*""".r <~ newline ^^ { l => LineRemoved(l) }

  def newline: Parser[String] = """\n""".r
  def number: Parser[Int] = """\d+""".r ^^ { _.toInt }

  def parse(str: String) = parseAll(allDiffs, str)

  def main(args: Array[String]) {
    val reader = {
      if (args.length == 0) {
        // read from stdin
        Console.in
      } else {
        new java.io.FileReader(args(0))
      }
    }
    parseAll(allDiffs, reader) match {
      case Success(s,_) => println( s )
      case NoSuccess(msg,_) => sys.error("ERROR: " + msg)
    }
  }
}