compare_dir_files.py ダウンロード/コピー
compare_dir_files.py
compare_dir_files.py
1"""
2ディレクトリ間のファイル構成を比較するスクリプト。
3
4このスクリプトは、特定の除外パターンに合致するファイルを無視し、
52つのディレクトリツリー間でどちらか一方にのみ存在するファイルを報告します。
6主にソースコードの同期状態や、予期せぬファイルの有無を確認する際に役立ちます。
7
8:doc:`compare_dir_files_usage`
9"""
10import argparse
11import os
12import re
13from pathlib import Path
14
15
16# 除外パターンの定義 (パス全体に対して判定)
17exclude_patterns = [
18 r"/__init__\.py$",
19 r"/.*_docstring\.",
20 r"/.*_\d{8}\.",
21 r"/tklib/",
22 r"/cms/",
23 r"/jsap_crystal/",
24 r"/stastical_physics/",
25 ]
26
27def compare_directories(source_dir, target_dir, extension="*.py"):
28 """
29 2つのディレクトリの内容を比較し、一方にのみ存在するファイルを報告する。
30
31 この関数は、ネストされた ``is_excluded`` 関数と ``get_filtered_files`` 関数を使用して、
32 除外パターンにマッチしないファイルを収集します。収集したファイルセットの差分を計算し、
33 結果を標準出力に表示します。パスのセパレータはPOSIX形式 ('/') に統一されます。
34
35 :param source_dir: str 比較元のディレクトリパス。
36 :param target_dir: str 比較先のディレクトリパス。
37 :param extension: str 検索するファイルの拡張子パターン (例: "*.py")。
38 :returns: None 結果は標準出力に表示されるため、明示的な戻り値はない。
39 """
40 source_path = Path(source_dir)
41 target_path = Path(target_dir)
42
43 def is_excluded(rel_path_str):
44 """
45 相対パスが定義済み除外パターンにマッチするかを判定する。
46
47 相対パスの先頭に '/' を付与し、``exclude_patterns`` の各正規表現と照合します。
48 いずれかのパターンにマッチすれば除外対象と判断します。
49
50 :param rel_path_str: str 比較する相対パス文字列 (例: "path/to/file.py")。
51 :returns: bool 除外パターンにマッチした場合はTrue、そうでなければFalse。
52 """
53 # マッチング用に先頭に / をつける
54 test_path = "/" + rel_path_str
55 return any(re.search(pattern, test_path) for pattern in exclude_patterns)
56
57 def get_filtered_files(base_path):
58 """
59 指定されたベースパス以下のファイルをフィルタリングして取得する。
60
61 ``Path.rglob()`` を使用して指定された拡張子パターンにマッチするファイルを再帰的に検索します。
62 各ファイルの相対パスを ``is_excluded`` 関数でチェックし、
63 除外対象でないファイルのみをセットとして収集します。
64 相対パスはPOSIX形式 ('/') に統一されます。
65
66 :param base_path: Path 検索を開始するベースディレクトリの `Path` オブジェクト。
67 :returns: set[str] フィルタリングされたファイルの相対パスのセット。
68 """
69 file_set = set()
70 for p in base_path.rglob(extension):
71 # ベースディレクトリからの相対パスを取得し '/' 区切りに変換
72 rel_path = p.relative_to(base_path).as_posix()
73 if not is_excluded(rel_path):
74 file_set.add(rel_path)
75 return file_set
76
77 source_files = get_filtered_files(source_path)
78 target_files = get_filtered_files(target_path)
79
80 print(f"{'='*60}")
81 print(f" 比較レポート (Separator: '/')")
82 print(f"{'='*60}")
83 print(f"・[A] Source: {source_path.absolute()}")
84 print(f"・[B] Target: {target_path.absolute()}")
85 print(f"{'-'*60}\n")
86
87 # AにあってBにないもの
88 only_in_source = sorted(source_files - target_files)
89 print(f"【 状態: Source [A] にのみ存在 / Target [B] から欠落 】")
90 if only_in_source:
91 for f in only_in_source:
92 print(f" [MISSING in B] -> {f}")
93 else:
94 print(" 該当なし")
95
96 print("\n" + "-"*60 + "\n")
97
98 # BにあってAにないもの
99 only_in_target = sorted(target_files - source_files)
100 print(f"【 状態: Target [B] にのみ存在 / Source [A] から欠落 】")
101 if only_in_target:
102 for f in only_in_target:
103 print(f" [MISSING in A] -> {f}")
104 else:
105 print(" 該当なし")
106
107def main():
108 """
109 コマンドライン引数を解析し、ディレクトリ比較処理を実行する。
110
111 この関数は ``argparse`` を使用して、比較元ディレクトリ、比較先ディレクトリ、
112 検索拡張子パターンを引数として受け取ります。
113 デフォルト値も設定されており、指定されたパスが有効なディレクトリであるかを確認し、
114 ``compare_directories`` 関数を呼び出します。
115
116 :param None: コマンドライン引数は ``argparse`` によって処理されるため、明示的な引数はない。
117 :returns: None 処理結果は標準出力に表示される。
118 """
119 parser = argparse.ArgumentParser(description="特定のディレクトリやファイルパターンを除外して2つのツリーを比較します。")
120
121 parser.add_argument("source",
122 nargs="?",
123 default=r"D:\git\tkProg\tkprog_COE",
124 help="比較元ディレクトリ(A)")
125 parser.add_argument("target",
126 nargs="?",
127 default=r"D:\git\sphinx\tkProg\source",
128 help="比較先ディレクトリ(B)")
129 parser.add_argument("-e", "--ext",
130 default="*.py",
131 help="検索パターン (デフォルト: *.py)")
132
133 args = parser.parse_args()
134
135 if not os.path.isdir(args.source) or not os.path.isdir(args.target):
136 print("エラー: 指定されたパスが存在しないか、ディレクトリではありません。")
137 return
138
139 compare_directories(args.source, args.target, args.ext)
140
141if __name__ == "__main__":
142 main()