前に、仕事でJavaのプロパティファイルを比較するのに、Groovyで簡単なスクリプトを作りました。
プロパティファイルにいろいろ環境に依存した設定が書かれていて、なおかつその内容がExcelとかで管理されていたので実際のソースコードとその内容が一致しているかどうかを確認したかった、というのが作った動機です。
そもそも、Excelで管理されていたんなら自動生成すればいいんじゃあ…?とかいうツッコミもあろうかとは思いますが、いったん置いておきまして…。
スクリプトに求めたのは、以下の挙動です。
- 比較したプロパティファイルに差分があれば、diffコマンドっぽく出力する
- 出力結果がUnicodeエスケープされていた形だと見づらくてしかたがないので、java.util.Propertiesを使って元の形に戻す
- キーの出現順は、Propertiesを使用するので、保障しない
- ファイル名が「.utf8」で終わっていた場合は、勝手にnative2asciiコマンドをかけた上で比較する
とすると、こんなスクリプトになりました。
properties_file_comparator.groovy
native2AsciiCommand = 'native2ascii -encoding %s %s' propertiesFileEncoding = 'UTF-8' propertiesFileSuffix = '.utf8' def path1 = args[0] def path2 = args[1] def propertiesFile1 = autoNative2Ascii(path1) def propertiesFile2 = autoNative2Ascii(path2) compareProperties(propertiesFile1.clone(), propertiesFile2.clone()) def compareProperties(properties1, properties2) { if (properties1.isEmpty()) { for (entry2 in properties2) { def prop2 = new Property(key: entry2.key, value: properties2.remove(entry2.key)) printDifference(new Property(), prop2) } } else if (properties2.isEmpty()) { for (entry1 in properties1) { def prop1 = new Property(key: entry1.key, value: properties1.remove(entry1.key)) printDifference(prop1, new Property()) } } else { def key1 = properties1.firstKey() def key2 = properties2.firstKey() if (key1 > key2) { def prop2 = new Property(key: key2, value: properties2.remove(key2)) printDifference(new Property(), prop2) compareProperties(properties1, properties2) } else if (key1 < key2) { def prop1 = new Property(key: key1, value: properties1.remove(key1)) printDifference(prop1, new Property()) compareProperties(properties1, properties2) } else { def value1 = properties1.remove(key1) def value2 = properties2.remove(key2) if (value1 != value2) { def prop1 = new Property(key: key1, value: value1) def prop2 = new Property(key: key2, value: value2) printDifference(prop1, prop2) } compareProperties(properties1, properties2) } } } def printDifference(prop1, prop2) { if (prop1.hasKey()) { println("< $prop1.key = ${prop1.printableValue()}") } if (prop2.hasKey()) { println("> $prop2.key = ${prop2.printableValue()}") } println("---") } def requireNative2Ascii(fileName) { propertiesFileSuffix != null && fileName.endsWith(propertiesFileSuffix) } def autoNative2Ascii(fileName) { if (requireNative2Ascii(fileName)) { def command = String.format(native2AsciiCommand, propertiesFileEncoding, fileName) def proc = command.execute() def outputStream = new ByteArrayOutputStream() proc.consumeProcessOutput(outputStream, new ByteArrayOutputStream()) def retVal = proc.waitFor() if (retVal != 0) { throw new IllegalArgumentException("native2ascii ret = [${retVal}]") } def properties = new Properties() new ByteArrayInputStream(outputStream.toByteArray()).withStream { properties.load(it) } new TreeMap(properties) } else { def properties = new Properties() new File(fileName).withInputStream { properties.load(it) } new TreeMap(properties) } } class Property { def key def value def hasKey() { key != null } def printableValue() { if (value == null) { "" } else { def builder = new StringBuilder() for (v in value) { switch (v) { case '\n': builder.append("\\n") break case '\r': builder.append("\\r") break case '\t': builder.append("\\t") break default: builder.append(v) break } } builder.toString() } } }
あれ、貼ってみると、思ったより長い…。キーごとの比較を簡単にするために、TreeMapを使いました。
動作確認のために、以下のようなプロパティファイルを用意。
<<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
実行すると、それぞれこんな結果になります。
$ groovy properties_file_comparator.groovy a.properties b.properties > bar = hello --- < foo = bar > foo = boo --- < fuga = var --- < hoge = 日本語 > hoge = 英語 --- $ groovy properties_file_comparator.groovy a.properties.utf8 b.properties.utf8 > bar = hello --- < foo = bar > foo = boo --- < fuga = var --- < hoge = 日本語 > hoge = 英語 ---
一致しているものは出力されていないので、意図通り動いています。
なお、上記のスクリプトは今日即興で作ったもので、実際に仕事で使ったものはサブディレクトリ内も含めた形で比較できるように作ってました。
properties_file_comparator.groovy
import groovy.io.FileType autoNative2ascii = System.getProperty('auto.n2a', 'true').toBoolean() native2ascii = System.getProperty('n2a.path', 'native2ascii') propertiesEncoding = System.getProperty('n2a.encoding', 'UTF-8') propertiesEncodeSuffix = System.getProperty('n2a.suffix', '.utf8') tabFormatEnable = System.getProperty('tab.format', 'false').toBoolean() consoleEncoding = System.getProperty('console.encoding', 'UTF-8') if (args.size() != 2) { println("Required 2 Arguments: dir1 dir2") System.exit(1) } dirOrFile1 = new File(args[0]) dirOrFile2 = new File(args[1]) requiredExists(dirOrFile1) requiredExists(dirOrFile2) def dir1Properties = new TreeMap() def dir2Properties = new TreeMap() traverseDir(dir1Properties, dirOrFile1) traverseDir(dir2Properties, dirOrFile2) def dir1PropertiesClone = dir1Properties.clone() def dir2PropertiesClone = dir2Properties.clone() messageBuffer = [] if (dir1PropertiesClone.size() == 1 && dir2PropertiesClone.size() == 1) { def prop1Path = dir1PropertiesClone.firstKey() def prop2Path = dir2PropertiesClone.firstKey() compareProperties(prop1Path, dir1PropertiesClone.remove(prop1Path), prop2Path, dir2PropertiesClone.remove(prop2Path)) flushMessageBuffer(prop1Path, prop2Path) } else { comparePropertiesMap(dir1PropertiesClone, dir2PropertiesClone) } def comparePropertiesMap(prop1Map, prop2Map) { if (prop1Map.isEmpty()) { if (!prop2Map.isEmpty()) { prop2Map.each { key, value -> printOnlyFileRight(dirOrFile2, key) } } return } else if (prop2Map.isEmpty()) { if (!prop1Map.isEmpty()) { prop1Map.each { key, value -> printOnlyFileLeft(dirOrFile1, key) } } return } def key1 = prop1Map.firstKey() def key2 = prop2Map.firstKey() if (key1 == key2) { def prop1Path = dirOrFile1.path + key1 def prop2Path = dirOrFile2.path + key2 compareProperties(prop1Path, prop1Map.remove(key1), prop2Path, prop2Map.remove(key2)) flushMessageBuffer(prop1Path, prop2Path) } else if (key1 < key2) { printOnlyFileLeft(dirOrFile1, key1) prop1Map.remove(key1) } else if (key2 < key1) { printOnlyFileRight(dirOrFile2, key2) prop2Map.remove(key2) } comparePropertiesMap(prop1Map, prop2Map) } def compareProperties(prop1Path, prop1, prop2Path, prop2) { if (prop1.isEmpty()) { if (!prop2.isEmpty()) { prop2.each { key, value -> printOnlyContentsRight(prop2Path, ["key": key, "value": value]) } } return } else if (prop2.isEmpty()) { if (!prop1.isEmpty()) { prop1.each { key, value -> printOnlyContentsLeft(prop1Path, ["key": key, "value": value]) } } return } def key1 = prop1.firstKey() def key2 = prop2.firstKey() if (key1 == key2) { def value1 = prop1.remove(key1) def value2 = prop2.remove(key2) if (value1 != value2) { printDifferContents(prop1Path, ["key": key1, "value": value1], prop2Path, ["key": key2, "value": value2]) } } else if (key1 < key2) { printOnlyContentsLeft(prop1Path, ["key": key1, "value": prop1.remove(key1)]) } else if (key1 > key2) { printOnlyContentsRight(prop2Path, ["key": key2, "value": prop2.remove(key2)]) } compareProperties(prop1Path, prop1, prop2Path, prop2) } def flushMessageBuffer(prop1Path, prop2Path) { if (!messageBuffer.isEmpty()) { if (tabFormatEnable == false) { println("--------------- ファイル[${prop1Path}] と ファイル[${prop2Path}] に差異があります ---------------") } else { //print("${prop1Path}\t${prop2Path}\t") } messageBuffer.each { println(it) } messageBuffer = [] } } def printOnlyFileLeft(dir, filePath) { if (tabFormatEnable == false) { printOnlyFile(dir, filePath) } else { println(dir.path + filePath + "\t\t\t\t\t") } } def printOnlyFileRight(dir, filePath) { if (tabFormatEnable == false) { printOnlyFile(dir, filePath) } else { println("\t" + dir.path + filePath + "\t\t\t\t") } } def printOnlyFile(dir, filePath) { println("--------------- ${dir} のみに存在するファイル => ${filePath} ---------------") } def printDifferContents(prop1Path, prop1, prop2Path, prop2) { if (tabFormatEnable == false) { messageBuffer.add("****** 相違 *****") messageBuffer.add("< ${prop1.key} = ${escapePropertyValue(prop1.value)}") messageBuffer.add("> ${prop2.key} = ${escapePropertyValue(prop2.value)}") } else { messageBuffer.add("${prop1Path}\t${prop2Path}\t${prop1.key}\t${escapePropertyValue(prop1.value)}\t${escapePropertyValue(prop2.value)}") } } def printOnlyContentsLeft(propPath, prop) { if (tabFormatEnable == false) { printOnlyContents(propPath, prop) } else { messageBuffer.add("${propPath}\t\t${prop.key}\t${escapePropertyValue(prop.value)}\t") } } def printOnlyContentsRight(propPath, prop) { if (tabFormatEnable == false) { printOnlyContents(propPath, prop) } else { messageBuffer.add("\t${propPath}\t${prop.key}\t\t${prop.value}") } } def printOnlyContents(propPath, prop) { messageBuffer.add("***** ${propPath} のみに存在 *****") messageBuffer.add("${prop.key} = ${escapePropertyValue(prop.value)}") } def traverseDir(propertiesMap, targetDirOrFile) { def entryProps = { dir, file -> def inputStream def fileKey if (autoNative2ascii && file.path.endsWith("${propertiesEncodeSuffix}")) { def command = "${native2ascii} -encoding ${propertiesEncoding} ${file.path}" def proc = command.execute() def outStream = new ByteArrayOutputStream() proc.consumeProcessOutput(outStream, new ByteArrayOutputStream()) def returnCode = proc.waitFor() if (returnCode != 0) { println("[${command}] return code = [${returnCode}]") } inputStream = new ByteArrayInputStream(outStream.toByteArray()) def path = relativePath(dir, file) fileKey = path.substring(0, path.length() - propertiesEncodeSuffix.length()) } else { inputStream = file.newInputStream() fileKey = relativePath(dir, file) } inputStream.withStream { def props = new Properties() props.load(it) propertiesMap.put(fileKey, new TreeMap(props)) } } def traverseProps traverseProps = { dir, file -> file.eachDir(traverseProps.curry(dir)) file.eachFileMatch(FileType.FILES, ~/.*\.properties(\.[^.]+)?$/, entryProps.curry(dir)) } if (targetDirOrFile.directory) { traverseProps(targetDirOrFile, targetDirOrFile) } else { entryProps(null, targetDirOrFile) } } def escapePropertyValue(value) { if (value == null) { null } else { def builder = new StringBuilder() for (v in value) { switch (v) { case '\n': builder.append("\\n") break case '\r': builder.append("\\r") break case '\t': builder.append("\\t") break //case '\\': // builder.append("\\") // break default: builder.append(v) break } } builder.toString() } } def relativePath(baseDir, targetFile) { if (baseDir == null) { targetFile.name } else { targetFile.path.substring(baseDir.path.length()) } } def requiredExists(dirOrFile) { if (!dirOrFile.exists()) { println("[${dirOrFile}]が存在しません") System.exit(1) } }
こちらは、業務都合で出力形式とかに細工がしてあります(笑)。