前回のエントリのScala版です。勢いで書いてみました。
スクリプトの動作仕様には、変更ありません。
- 比較したプロパティファイルに差分があれば、diffコマンドっぽく出力する
- 出力結果がUnicodeエスケープされていた形だと見づらくてしかたがないので、java.util.Propertiesを使って元の形に戻す
- キーの出現順は、Propertiesを使用するので、保障しない
- ファイル名が「.utf8」で終わっていた場合は、勝手にnative2asciiコマンドをかけた上で比較する
書いたスクリプトは、こんな感じ。
properties_file_comparator.scala
import scala.annotation.tailrec import scala.collection.JavaConverters._ import scala.collection.immutable.TreeMap import scala.sys.process._ import java.io.ByteArrayInputStream import java.nio.file.{Files, Paths} import java.util.Properties val Native2AsciiCommand = "native2ascii -encoding %s %s" val PropertiesFileEncoding = "UTF-8" val PropertiesFileSuffix = ".utf8" val path1 = args(0) val path2 = args(1) val propertiesFile1 = autoNative2Ascii(path1) val propertiesFile2 = autoNative2Ascii(path2) compareProperties(propertiesFile1, propertiesFile2) @tailrec def compareProperties(properties1: TreeMap[String, String], properties2: TreeMap[String, String]): Unit = { (properties1.nonEmpty, properties2.nonEmpty) match { case (true, true) => val key1 = properties1.firstKey val key2 = properties2.firstKey if (key1 > key2) { printDifference(property2 = Property(Option(key2), properties2.get(key2))) compareProperties(properties1, properties2 - key2) } else if (key1 < key2) { printDifference(property1 = Property(Option(key1), properties1.get(key1))) compareProperties(properties1 - key1, properties2) } else { (properties1.get(key1), properties2.get(key2)) match { case (Some(value1), Some(value2)) if value1 == value2 => case (v1 @ Some(value1), v2 @ Some(value2)) => printDifference(Property(Option(key1), v1), Property(Option(key2), v2)) case other => } compareProperties(properties1 - key1, properties2 - key2) } case (false, _) => properties2 foreach { case (k, v) => printDifference(property2 = Property(Option(k), Option(v))) } case (_, false) => properties1 foreach { case (k, v) => printDifference(property1 = Property(Option(k), Option(v))) } } } def printDifference(property1: Property = Property(None, None), property2: Property = Property(None, None)): Unit = { property1.key.foreach { key => println("< %s = %s".format(key, property1.printableValue)) } property2.key.foreach { key => println("> %s = %s".format(key, property2.printableValue)) } println("---") } def requireNative2Ascii(fileName: String): Boolean = PropertiesFileSuffix != null && fileName.endsWith(PropertiesFileSuffix) def autoNative2Ascii(fileName: String): TreeMap[String, String] = if (requireNative2Ascii(fileName)) { val result = Native2AsciiCommand.format(PropertiesFileEncoding, fileName) !! val properties = new Properties properties.load(new ByteArrayInputStream(result.getBytes)) new TreeMap[String, String] ++ properties.asScala } else { val properties = new Properties properties.load(new ByteArrayInputStream(Files.readAllBytes(Paths.get(fileName)))) new TreeMap[String, String] ++ properties.asScala } case class Property(key: Option[String], value: Option[String]) { def printableValue: String = value.getOrElse("") match { case null => "" case v => v.map { case '\n' => "\\n" case '\r' => "\\r" case '\t' => "\\t" case other => other }.mkString } }
TreeMapを使った基本コンセプトは変わらない…というか、単にScalaに読み替えただけ、かな…。
比較対象も変更なく。
<<a.properties>> foo=bar hoge=\u65e5\u672c\u8a9e fuga=var propKey=propValue <<a.properties.utf8>> foo=bar hoge=日本語 fuga=var propKey=propValue <<b.properties>> foo=boo hoge=\u82f1\u8a9e propKey=propValue bar=hello <<b.properties.utf8>> foo=boo hoge=英語 propKey=propValue bar=hello
実行結果も変わりなく。
$ scala properties_file_comparator.scala a.properties b.properties > bar = hello --- < foo = bar > foo = boo --- < fuga = var --- < hoge = 日本語 > hoge = 英語 --- $ scala properties_file_comparator.scala a.properties.utf8 b.properties.utf8 > bar = hello --- < foo = bar > foo = boo --- < fuga = var --- < hoge = 日本語 > hoge = 英語 ---
なお、Scala版のディレクトリ内も比較可能なバージョンは、ちょっとマジメに頑張って書いてみました。でもまあ、もうちょっと整理して書いた方がよかったかなぁ…。
これはただ足跡的に貼っているだけですので…。
build.sbt
name := "properties-file-comparator" version := "0.0.1" scalaVersion := "2.9.2" organization := "littlewings"
src/main/scala/PropertiesFileComparator.scala
import scala.annotation.tailrec import scala.collection.SortedMap import java.nio.file.{Files, Path, Paths} object PropertiesFileComparator { def main(args: Array[String]): Unit = { val (leftTarget, rightTarget) = args toList match { case left :: right :: Nil => (left, right) case other => usage() } val leftPath = Paths get leftTarget match { case left if Files.exists(left) => left case left => noSuchFileOrDirectoryExit(left) } val rightPath = Paths get rightTarget match { case right if Files.exists(right) => right case right => noSuchFileOrDirectoryExit(right) } val propertiesFileComparator = (Files.isDirectory(leftPath) && Files.isDirectory(rightPath)) || (!Files.isDirectory(leftPath) && !Files.isDirectory(leftPath)) match { case true => new PropertiesFileComparator(leftPath, rightPath) case false => fileTypeNotMatchExit(leftPath, rightPath) } propertiesFileComparator.compare.foreach { case Equals(_, _) => case d @ Difference(leftPath, rightPath, _) => printf("Found Difference %s <-> %s%n", leftPath, rightPath) println(d.message) case of @ OnlyFile(_) => println(of.message) } } private def usage(): Nothing = { val message = """|This Tool Required 2 Arguments | 1: Compare Properties File or Directory | 2: Compare Propertirs File or Directory""".stripMargin println(message) sys.exit(0) } private def noSuchFileOrDirectoryExit(path: Path): Nothing = { printf("No Such File or Directory [%s]%n", path) sys.exit(1) } private def fileTypeNotMatchExit(left: Path, right: Path): Nothing = { val leftMessage = if (Files.isDirectory(left)) "Directory" else "File" val rightMessage = if (Files.isDirectory(right)) "Directory" else "File" printf("%s is %s, But %s is %s%n", left, leftMessage, right, rightMessage) sys.exit(1) } } class PropertiesFileComparator(leftPath: Path, rightPath: Path) { def compare(): List[CompareResult] = { val propertiesFileFinder = new PropertiesFileFinder (propertiesFileFinder.find(leftPath), propertiesFileFinder.find(rightPath)) match { case (leftPropertiesFile :: Nil, rightPropertiesFile :: Nil) => List(compareProperties(leftPropertiesFile, rightPropertiesFile)) case (leftPropertiesFiles, rightPropertiesFiles) => compareFiles(leftPropertiesFiles, rightPropertiesFiles, Nil) } } @tailrec private def compareFiles(leftFiles: List[PropertiesFile], rightFiles: List[PropertiesFile], results: List[CompareResult]): List[CompareResult] = { (leftFiles, rightFiles) match { case (leftFile :: leftRest, rightFile :: rightRest) if leftFile.path.toFile.getName > rightFile.path.toFile.getName => compareFiles(leftFiles, rightRest, OnlyFile(rightFile.path) :: results) case (leftFile :: leftRest, rightFile :: rightRest) if leftFile.path.toFile.getName < rightFile.path.toFile.getName => compareFiles(leftRest, rightFiles, OnlyFile(leftFile.path) :: results) case (leftFile :: leftRest, rightFile :: rightRest) => compareFiles(leftRest, rightRest, compareProperties(leftFile, rightFile) :: results) case (Nil, Nil) => results reverse case (leftFile :: leftRest, Nil) => compareFiles(Nil, Nil, OnlyFile(leftFile.path) :: results) case (Nil, rightFile :: rightRest) => compareFiles(Nil, Nil, OnlyFile(rightFile.path) :: results) } } private def compareProperties(leftFile: PropertiesFile, rightFile: PropertiesFile): CompareResult = { val buffer = new DifferenceBuffer(leftFile.path, rightFile.path) @tailrec def comparePropertiesInner(leftProperties: SortedMap[String, String], rightProperties: SortedMap[String, String]): CompareResult = { if (leftProperties.nonEmpty && rightProperties.nonEmpty) { val leftKey = leftProperties.firstKey val rightKey = rightProperties.firstKey val lkOption = Some(leftKey) val rkOption = Some(rightKey) if (leftKey > rightKey) { buffer += DifferencePart(None, None, rkOption, rightProperties.get(rightKey)) comparePropertiesInner(leftProperties, rightProperties - rightKey) } else if (leftKey < rightKey) { buffer += DifferencePart(lkOption, leftProperties.get(leftKey), None, None) comparePropertiesInner(leftProperties - leftKey, rightProperties) } else { // (leftKey == rightKey) { (leftProperties.get(leftKey), rightProperties.get(rightKey)) match { case (Some(lv), Some(rv)) if lv == rv => case (l @ Some(lv), r @ Some(rv)) if lv != rv => buffer += DifferencePart(lkOption, l, rkOption, r) case other => /* case (l @ None, r @ Some(_)) => buffer += DifferencePart(lkOption, l, rkOption, r) case (l @ Some(_), r @ None) => buffer += DifferencePart(lkOption, l, rkOption, r) case (None, None) => */ } comparePropertiesInner(leftProperties - leftKey, rightProperties - rightKey) } } else if (leftProperties.isEmpty) { if (rightProperties.nonEmpty) { for ((k, v) <- rightProperties) buffer += DifferencePart(None, None, Some(k), Option(v)) } buffer toDifferenceOrEquals } else if (rightProperties.isEmpty) { if (leftProperties.nonEmpty) { for ((k, v) <- leftProperties) buffer += DifferencePart(Some(k), Option(v), None, None) } buffer toDifferenceOrEquals } else { buffer toDifferenceOrEquals } } comparePropertiesInner(leftFile.properties, rightFile.properties) } } case class PropertiesFile(path: Path, properties: SortedMap[String, String]
src/main/scala/CompareResult.scala
import scala.collection.mutable.ListBuffer import java.nio.file.Path trait CompareResult { def message: String protected def escapeValue(value: String): String = value match { case null => "" case other => other.map { case '\n' => "\\n" case '\r' => "\\r" case '\t' => "\\t" case other => other }.mkString } } class DifferenceBuffer(leftPath: Path, rightPath: Path) { private[this] val differencePartBuffer: ListBuffer[DifferencePart] = new ListBuffer def +=(difference: DifferencePart): this.type = { differencePartBuffer += difference this } def toDifferenceOrEquals: CompareResult = differencePartBuffer toList match { case Nil => Equals(leftPath, rightPath) case differences => Difference(leftPath, rightPath, differences) } } case class Equals(leftPath: Path, rightPath: Path) extends CompareResult { def message: String = "" } case class Difference(leftPath: Path, rightPath: Path, differences: List[DifferencePart]) extends CompareResult { def message: String = differences .map(_.message) .mkString(System.lineSeparator + "---" + System.lineSeparator) } case class DifferencePart(leftKey: Option[String], leftValue: Option[String], rightKey: Option[String], rightValue: Option[String]) extends CompareResult { def message: String = List((leftKey, leftValue), (rightKey, rightValue)).zip(List("< %s = %s", "> %s = %s")) .filter { case (c, _) => c._1.isDefined } .map { case (c, f) => f.format(c._1.get, escapeValue(c._2.getOrElse(""))) } .mkString(System.lineSeparator) } case class OnlyFile(path: Path) extends CompareResult { def message: String = "Onln in %s: %s".format(path.getParent, path.getFileName) }
src/main/scala/PropertiesFileFinder.scala
import scala.collection.SortedMap import scala.collection.JavaConverters._ import scala.collection.immutable.TreeMap import scala.collection.mutable.ListBuffer import scala.sys.process._ import java.io.ByteArrayInputStream import java.nio.file.{Files, FileVisitResult, Path, Paths, SimpleFileVisitor} import java.nio.file.attribute.BasicFileAttributes import java.util.Properties object PropertiesFileFinder { val Native2AsciiCommand = "native2ascii -encoding %s %s" } class PropertiesFileFinder(encoding: Option[String] = Some("UTF-8"), suffix: Option[String] = Some(".utf8"), autoNative2Ascii: Boolean = true) { def find(path: Path): List[PropertiesFile] = { val visitor = new PropertiesFileVisitor(path, toProperties) Files.walkFileTree(path, visitor) visitor.propertiesFiles } private def toProperties(file: Path): PropertiesFile = { val applyNative2Ascii = suffix exists { s => file.toString.endsWith(s) && autoNative2Ascii } val props = new Properties val bytes = applyNative2Ascii match { case true => val enc = encoding.getOrElse(throw new IllegalArgumentException("Not Set Encoding!")) val result = PropertiesFileFinder.Native2AsciiCommand.format(enc, file) !! // execute command result.getBytes(enc) case false => Files.readAllBytes(file) } props.load(new ByteArrayInputStream(bytes)) if (applyNative2Ascii) { val fileName = file.toFile.getName val parent = Option(file.getParent) val newFile = Paths.get(fileName.substring(0, fileName.size - suffix.get.size)) PropertiesFile( parent.getOrElse(Paths.get("")).resolve(newFile), new TreeMap[String, String] ++ props.asScala ) } else { PropertiesFile(file, new TreeMap[String, String] ++ props.asScala) } } } private class PropertiesFileVisitor(path: Path, toProperties: (Path => PropertiesFile)) extends SimpleFileVisitor[Path] { private[this] val propertiesFilesBuffer: ListBuffer[PropertiesFile] = new ListBuffer override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = file toFile match { case f => // case f if f.getName.indexOf(".properties") != -1 => val propertiesFile = toProperties(file) propertiesFilesBuffer += PropertiesFile(propertiesFile.path, propertiesFile.properties) FileVisitResult.CONTINUE // case other => FileVisitResult.CONTINUE } def propertiesFiles: List[PropertiesFile] = propertiesFilesBuffer toList }