這是diff系列文章的第四部分。在前一篇中,我們深入討論了myers diff的線性空間優化版本。在本篇文章中,我們將在線性myers算法的基礎上構建一個完整的命令行程序,它可以輸出兩個文件的diff。
完整的代碼倉庫見此處: myers-diff, 可以在該倉庫根目錄下執行以下命令查看其效果:
moon update
moon build --target native
./target/native/release/build/main/main.exe tests/old1.txt tests/new1.txt
輸出如下:
--- tests/old1.txt
+++ tests/new1.txt
1 1 aaaaaa
2 2 bbb
3 3 cccccccccc
+ 4 dddddd
構建好的可執行文件位於target/native/release/build/main/main.exe, 你可以將它的名稱修改為diff.exe並放到某個PATH目錄下使用。
獲取命令行參數
目前moonbitlang/x庫已經提供了獲取命令行參數的API@moonbitlang/x/sys.get_cli_args,它的返回值是一個裝有所有命令行參數的數組,不過在不同後端下其行為會有些許變化,此處暫時選用native後端。在native後端下,命令行參數數組的第一個元素是MoonBit程序編譯出的可執行文件的路徑,後續元素都是用户自己傳遞的命令行參數。
一般來説,處理命令行參數用一些專門的解析庫會比較方便(MoonBit現在也有這樣的庫, 如Yoorkin/ArgParser),但對於我們這個比較簡單的diff程序,使用MoonBit最近新增的is運算符和guard語句結合就可以輕鬆處理了。
guard args is [executable, oldfile, newfile] else {
println("wrong arguments: \{args}")
println("Usage: \{args[0]} <file1> <file2>")
}
讀取文件
moonbitlang/x下的fs包提供了一些常見的文件IO函數,在這裏直接使用read_file_to_string即可,它的文件編碼參數encoding的默認值是utf8。
read_file_to_string在讀取文件失敗的情況下(很常見的一種情況是參數裏的文件名不小心打錯了,或者沒有文件的讀權限)會拋出Error, 為簡化錯誤處理,此處直接將讀取失敗的文件名打印出來然後調用panic()。
let old =
try {
(@diff.lines(@fs.read_file_to_string!(oldfile)))[:]
} catch {
_ => {
println("\{executable}: failed to load file \{oldfile}")
panic()
}
}
let new =
try {
(@diff.lines(@fs.read_file_to_string!(newfile)))[:]
} catch {
_ => {
println("\{executable}: failed to load file \{newfile}")
panic()
}
}
渲染
在終端中使用一些特定的控制字符可以讓輸出的文本帶有顏色,嘗試運行這段MoonBit代碼
test {
println("\u001B[35m Hello MoonBit \u001B[39m")
}
它會輸出紫色的Hello MoonBit.
雖然不使用其他外部依賴也能達到輸出彩色文本的效果,但是手動拼接字符串比較乏味,而且很容易出錯,好在MoonBit社區已經有可用的終端渲染庫:Lampese/moonbit-chalk
在輸出diff時,常見的渲染選項是將插入渲染為綠色,刪除渲染為紅色,讓我們分別新建兩個名叫ins和del的chalk對象,並分別設置顏色
let ins = @chalk.chalk().color(Green)
let del = @chalk.chalk().color(Red)
在輸出diff之前,最好也提醒一下用户所比較的是哪兩個文件,文件名為達到醒目的效果應該加粗輸出。
let title = @chalk.chalk().modifier(Bold) // 加粗
println("") // 留空一行
println(title.render("--- \{oldfile}"))
println(title.render("+++ \{newfile}"))
然後遍歷diff算法計算出的編輯序列並進行渲染
for edit in result.iter() {
match edit {
Insert(_) => println(ins.render(@diff.pprint_edit(edit)))
Delete(_) => println(del.render(@diff.pprint_edit(edit)))
Equal(_) => println(@diff.pprint_edit(edit))
}
}