Сравнение папок¶
Аналог Synchronize Dirs в Double Commander, но из командной строки
import os
import argparse
from pathlib import Path
from colorama import Fore, Style
- Argparse Tutorial:
- Argparse API:
Аргументы командной строки
parser = argparse.ArgumentParser(description="Compare folders.")
parser.add_argument("-V", "--version", action="version", version="2023-11-25")
parser.add_argument("alias1", help="1st path to compare. Relalive to home")
parser.add_argument("alias2", help="2nd path to compare. Relalive to home")
parser.add_argument("-home", default=".", help="Home path")
parser.add_argument("-rel", default="", help="Constant path, relative from aliases")
parser.add_argument("-ext", default="*", help="Extension of files to compare")
parser.add_argument("-exclude", help="Exclude file")
parser.add_argument("-patch", help="Folder with patches")
parser.add_argument("-verbose", action="store_true", help="Verbose mode")
Перевести имена файлов в Path
args = parser.parse_args()
folder1 = os.path.join(args.home, args.alias1, args.rel)
folder2 = os.path.join(args.home, args.alias2, args.rel)
folder1 = Path(folder1).absolute()
folder2 = Path(folder2).absolute()
print(f"Folder 1: {args.alias1}")
print(f"Folder 2: {args.alias2}")
Проверить существование папок
if not os.path.exists(folder1):
print(f"[ERROR] Folder 1 not found: {folder1}")
exit()
if not os.path.exists(folder2):
print(f"[ERROR] Folder 2 not found: {folder1}")
exit()
Загрузить из файла список exclude с окончаниями имен файлов, которые мы исключим из сравнения
exclude = None
if args.exclude is not None:
with open(args.exclude, "r") as file:
exclude = [line.strip() for line in file]
print(f"Exclude: {exclude}")
Загрузить список патчей из папки patch. Для того, чтобы файл попал в список, в одной из подпапок должна существовать пара filename.ext / filename.ext.patch.
def find_patch_files(base_folder):
matched_files = []
for root, dirs, files in os.walk(base_folder):
for file in files:
if file.endswith(".patch"):
original_file = file[:-6] # Remove '.patch' extension
if original_file in files:
# Construct the relative path
relative_path = os.path.join(
root[len(base_folder) + 1 :], original_file
)
# os.path.relpath(root, base_folder), original_file)
# print(f'{relative_path} : {original_file}')
matched_files.append(relative_path)
return matched_files
patches = None
if args.patch is not None:
patches = find_patch_files('patch')
print(f"Patches: {patches}")
file is pathlib.PosixPath. Convert it to string with the relative path to current folder.
def has_patch(file):
rel_path = os.path.relpath(str(file))
result = rel_path in patches
# print(f"{result} : {rel_path}")
return result
def printf(file):
ch = "+" if has_patch(file) else "-"
print(f" {ch} {file}")
Начало программы
print("-----------------------")
Проверить, что строка имеет одно из окончаний в массиве.
def ends_with_any(s, suffix_list):
return any(s.endswith(suffix) for suffix in suffix_list)
Сравнить 2 файла по строчкам, убрать пробелы в начале и в конце строки.
def compare_files(file1_path, file2_path):
if args.verbose:
print("compare_files:")
print(f" file1: {file1_path}")
print(f" file2: {file2_path}")
with open(file1_path, "r") as file1, open(file2_path, "r") as file2:
for line1, line2 in zip(file1, file2):
if line1.strip() != line2.strip():
return False
# Check if one file still has more lines left
return not (next(file1, None) or next(file2, None))
ANSI-цвета для вывода в консоль
def red(s):
return Fore.RED + Style.BRIGHT + s + Style.RESET_ALL
def green(s):
return Fore.GREEN + s + Style.RESET_ALL
def blue(s):
return Fore.BLUE + Style.BRIGHT + s + Style.RESET_ALL
Инициализация массивов для результатов
equal_files = []
not_equal_files = []
only_in_folder1 = []
only_in_folder2 = []
excluded_files = []
Get all text files from both directories including subdirectories
files_in_folder1 = {f for f in folder1.rglob("*." + args.ext)}
files_in_folder2 = {f for f in folder2.rglob("*." + args.ext)}
for file1 in files_in_folder1:
relative_path = file1.relative_to(folder1)
file2 = folder2 / relative_path
file1_str = str(file1.absolute())
if exclude is not None and ends_with_any(file1_str, exclude):
excluded_files.append(relative_path)
continue
file1_patch = file1
if has_patch(relative_path):
file1_patch = Path('patch') / relative_path
print(f"Patch: {file1_patch}")
if file2 in files_in_folder2:
if compare_files(file1_patch, file2):
equal_files.append(relative_path)
else:
not_equal_files.append(relative_path)
files_in_folder2.remove(file2)
else:
only_in_folder1.append(relative_path)
Any remaining files in files_in_folder2 are only in folder2
only_in_folder2.extend([f.relative_to(folder2) for f in files_in_folder2])
def filter_excluded(only_in_folder2):
result = []
for file2 in only_in_folder2:
if exclude is not None and ends_with_any(file2.name, exclude):
excluded_files.append(file2)
continue
else:
result.append(file2)
return result
only_in_folder2 = filter_excluded(only_in_folder2)
Print the report
print(f"Equal Files: {len(equal_files)}")
# for f in equal_files:
# printf(f)
if exclude is not None:
print(f"Excluded Files: {len(excluded_files)}")
print(red(f"\nNot Equal Files: {len(not_equal_files)}"))
not_equal_files.sort()
for f in not_equal_files:
printf(f)
print(green(f"\nOnly in {args.alias1}: {len(only_in_folder1)}"))
only_in_folder1.sort()
for f in only_in_folder1:
printf(f)
print(blue(f"\nOnly in {args.alias2}: {len(only_in_folder2)}"))
only_in_folder2.sort()
for f in only_in_folder2:
printf(f)
Конец программы
print("-----------------------")