compare_file_diff.py ダウンロード/コピー

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()