compare_file_diff.py ダウンロード/コピー
compare_file_diff.py
compare_file_diff.py
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4"""
5概要:
6 2つのテキストファイルの差分を比較して表示するプログラムです。
7
8詳細説明:
9 compare_update_diff.py から、2つのテキストファイルのdiff機能だけを抜き出しています。
10 第1位置引数に old_file、第2位置引数に new_file を指定します。
11 例として python compare_file_diff.py old.py new.py や
12 python compare_file_diff.py old.py new.py --context 3 のように実行します。
13"""
14
15import argparse
16import difflib
17from pathlib import Path
18
19
20def read_text_lines(path):
21 """
22 概要:
23 テキストファイルを読み込み、行リストと文字コードを返します。
24
25 詳細説明:
26 UTF-8を優先し、日本語Windows環境を考慮してcp932も試します。
27 改行コードは universal newline で正規化するため、
28 CRLFとLFの違いだけではdiffになりません。
29
30 引数:
31 :param path: 読み込むテキストファイルのパス。
32 :type path: str または pathlib.Path
33
34 戻り値:
35 :returns: ファイルの行リストと判定された文字コードのタプル。
36 :rtype: tuple
37 """
38 encodings = ("utf-8-sig", "utf-8", "cp932")
39
40 for enc in encodings:
41 try:
42 with open(path, "r", encoding=enc) as f:
43 return f.readlines(), enc
44 except UnicodeDecodeError:
45 pass
46
47 with open(path, "r", encoding="utf-8", errors="replace") as f:
48 return f.readlines(), "utf-8(replace)"
49
50
51def make_compact_diff(old_path, new_path, context=2):
52 """
53 概要:
54 コンパクトな差分リストを作成します。
55
56 詳細説明:
57 実際の変更行とその前後のコンテキスト行だけを出力します。
58 unified diffのヘッダや行番号情報などは表示しません。
59 離れた変更箇所はドット3つで区切ります。
60
61 引数:
62 :param old_path: 比較元のファイルパス。
63 :type old_path: str または pathlib.Path
64 :param new_path: 比較先のファイルパス。
65 :type new_path: str または pathlib.Path
66 :param context: 変更行の前後に表示するコンテキスト行数。
67 :type context: int
68
69 戻り値:
70 :returns: 差分行のリスト、比較元ファイルの文字コード、比較先ファイルの文字コードのタプル。
71 :rtype: tuple
72 """
73 old_lines, old_enc = read_text_lines(old_path)
74 new_lines, new_enc = read_text_lines(new_path)
75
76 diff_lines = list(
77 difflib.unified_diff(
78 old_lines,
79 new_lines,
80 fromfile=str(old_path),
81 tofile=str(new_path),
82 n=max(0, int(context)),
83 lineterm="\n",
84 )
85 )
86
87 compact = []
88 seen_hunk = False
89
90 for line in diff_lines:
91 if line.startswith("--- ") or line.startswith("+++ "):
92 continue
93
94 if line.startswith("@@ "):
95 if seen_hunk and compact and compact[-1] != "...":
96 compact.append("...")
97 seen_hunk = True
98 continue
99
100 if line.startswith("\\ No newline at end of file"):
101 continue
102
103 if line.endswith("\n"):
104 line = line[:-1]
105
106 compact.append(line)
107
108 while compact and compact[0] == "...":
109 compact.pop(0)
110 while compact and compact[-1] == "...":
111 compact.pop()
112
113 return compact, old_enc, new_enc
114
115
116def main():
117 """
118 概要:
119 コマンドライン引数を解析し、差分比較処理を実行します。
120
121 詳細説明:
122 argparseを用いて old_file と new_file、および context オプションを受け取ります。
123 指定されたファイルが存在するか確認し、make_compact_diff関数を呼び出して結果を標準出力に表示します。
124 """
125 parser = argparse.ArgumentParser(
126 description="Show a compact text diff between two files."
127 )
128 parser.add_argument(
129 "old_file",
130 help="Old/reference file",
131 )
132 parser.add_argument(
133 "new_file",
134 help="New/current file",
135 )
136 parser.add_argument(
137 "--context", "-c",
138 type=int,
139 default=2,
140 help="Number of unchanged context lines around each change (default: 2)",
141 )
142
143 args = parser.parse_args()
144
145 old_path = Path(args.old_file).expanduser().resolve()
146 new_path = Path(args.new_file).expanduser().resolve()
147
148 if not old_path.is_file():
149 parser.error(f"old_file is not found: {old_path}")
150
151 if not new_path.is_file():
152 parser.error(f"new_file is not found: {new_path}")
153
154 diff_lines, old_enc, new_enc = make_compact_diff(
155 old_path,
156 new_path,
157 context=max(0, args.context),
158 )
159
160 print(f"old: {old_path} ({old_enc})")
161 print(f"new: {new_path} ({new_enc})")
162 print(f"context: {max(0, args.context)}")
163 print("---")
164
165 if diff_lines:
166 print("\n".join(diff_lines))
167 else:
168 print("(no text difference found)")
169
170
171if __name__ == "__main__":
172 main()