miutti's picture
Source: kernel, desktop app, tools (snapshot of the GitHub repo)
df41178 verified
Raw History Blame Contribute Delete
4.56 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
difflook.py -- なにが変わったかを、行ごとに見せる
2つの見せ方を持つ。
① フォルダの差 … 実行の前と後で、どのファイルが増えた・減った・動いた
② ファイルの中身 … 1枚のテキストの、どの行が変わったか
②は Python が持っている difflib をそのまま使う。
自分で書き直す理由がない(車輪の再発明はしない)。
"""
import difflib, os
# 中身を読んで比べてよい大きさ。これ以上は行数だけ見る
MAX_READ = 2_000_000
TEXT_EXT = {".txt", ".md", ".html", ".htm", ".css", ".js", ".json", ".py",
".csv", ".log", ".xml", ".yml", ".yaml", ".sh", ".ini", ".cfg"}
def is_text(path):
if os.path.splitext(path)[1].lower() in TEXT_EXT:
return True
try:
with open(path, "rb") as f:
head = f.read(2048)
return b"\0" not in head
except OSError:
return False
def read(path):
try:
if os.path.getsize(path) > MAX_READ:
return None
with open(path, encoding="utf-8", errors="replace") as f:
return f.read().splitlines()
except OSError:
return None
def lines(a_lines, b_lines, a_name="前", b_name="後", ctx=3):
"""行の差を [(しるし, 文)] で返す。
しるし: "+" 増えた / "-" 減った / " " そのまま / "@" 区切り
"""
if a_lines is None or b_lines is None:
return [("@", "中身が読めませんでした")]
out = []
for ln in difflib.unified_diff(a_lines, b_lines,
fromfile=a_name, tofile=b_name,
lineterm="", n=ctx):
if ln.startswith("+++") or ln.startswith("---"):
continue
if ln.startswith("@@"):
out.append(("@", ln.strip()))
elif ln.startswith("+"):
out.append(("+", ln[1:]))
elif ln.startswith("-"):
out.append(("-", ln[1:]))
else:
out.append((" ", ln[1:] if ln.startswith(" ") else ln))
if not out:
out = [("@", "中身は変わっていません")]
return out
def files(a_path, b_path, ctx=3):
"""2つのファイルの中身を比べる"""
if not os.path.exists(a_path):
return [("@", f"ありません: {a_path}")]
if not os.path.exists(b_path):
return [("@", f"ありません: {b_path}")]
if not (is_text(a_path) and is_text(b_path)):
sa, sb = os.path.getsize(a_path), os.path.getsize(b_path)
return [("@", f"文字のファイルではありません({sa:,} → {sb:,} バイト)")]
return lines(read(a_path), read(b_path),
os.path.basename(a_path), os.path.basename(b_path), ctx)
def folders(before, after):
"""フォルダの前と後(virtual.simulate の「前」「後」)を突き合わせる。
戻り値: [{"場所", "増えた": [...], "減った": [...], "件数": (前, 後)}]
"""
out = []
for d in sorted(set(before) | set(after)):
b = list(before.get(d, []))
a = list(after.get(d, []))
if b == a:
continue
out.append({"場所": d,
"増えた": [x for x in a if x not in b],
"減った": [x for x in b if x not in a],
"件数": (len(b), len(a))})
return out
def render(rows, limit=200):
"""行の差を、人が読める文字列にする"""
out = []
for i, (mark, text) in enumerate(rows):
if i >= limit:
out.append(f"… ほか {len(rows)-limit} 行")
break
out.append((" " if mark == " " else mark + " ") + text)
return "\n".join(out)
def render_folders(rows):
out = []
for r in rows:
name = os.path.basename(r["場所"]) or r["場所"]
out.append(f"{name}/ {r['件数'][0]} 件 → {r['件数'][1]} 件")
for x in r["減った"]:
out.append(" - " + x)
for x in r["増えた"]:
out.append(" + " + x)
return "\n".join(out) or "変わったところはありません"
if __name__ == "__main__":
import sys
if len(sys.argv) == 3:
print(render(files(sys.argv[1], sys.argv[2])))
else:
a = ["こんにちは", "ここは変わらない", "古い行", "おわり"]
b = ["こんにちは", "ここは変わらない", "新しい行", "足した行", "おわり"]
print(render(lines(a, b)))