|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Đổi tên tất cả các file trong dự án có mẫu "<base> copy.<ext>" thành "<base>.vi.<ext>". |
| 3 | +
|
| 4 | +Yêu cầu cụ thể: |
| 5 | +- Tìm các file chứa chuỗi " copy" ngay trước phần mở rộng cuối cùng. |
| 6 | + Ví dụ: a11y.instructions copy.md -> a11y.instructions.vi.md |
| 7 | +- Hỗ trợ nhiều phần mở rộng kép (vd: .tar.gz) => chỉ chèn ".vi" trước phần mở rộng đầu tiên trong chuỗi mở rộng. |
| 8 | + Quy ước: Dùng phần sau dấu chấm đầu tiên sau " copy" làm phần mở rộng nguyên vẹn. |
| 9 | +- Bỏ qua thư mục ẩn (.git, .venv, node_modules, v.v.) theo một danh sách exclude. |
| 10 | +- Có chế độ dry-run (mặc định) để in ra các cặp rename mà không áp dụng. |
| 11 | +- Tránh ghi đè: nếu tên đích đã tồn tại, bỏ qua và cảnh báo. |
| 12 | +
|
| 13 | +Cách dùng: |
| 14 | + python change_file_name.py # Dry-run (không đổi tên thật) |
| 15 | + python change_file_name.py --apply # Thực hiện đổi tên thật |
| 16 | + python change_file_name.py --root . # Chỉ định thư mục gốc |
| 17 | +
|
| 18 | +""" |
| 19 | +from __future__ import annotations |
| 20 | +import argparse |
| 21 | +from pathlib import Path |
| 22 | +import sys |
| 23 | +from typing import Iterable |
| 24 | + |
| 25 | +EXCLUDE_DIRS = {'.git', '.hg', '.svn', '.venv', 'venv', 'node_modules', '.idea', '.vscode', '__pycache__'} |
| 26 | +TARGET_TOKEN = ' copy' |
| 27 | +INSERT_TOKEN = '.vi' |
| 28 | + |
| 29 | +def iter_files(root: Path) -> Iterable[Path]: |
| 30 | + for p in root.rglob('*'): |
| 31 | + if p.is_dir(): |
| 32 | + # Skip excluded dirs early by not descending into them |
| 33 | + if p.name in EXCLUDE_DIRS: |
| 34 | + # Prevent recursion into excluded |
| 35 | + dirs_to_skip = [d for d in p.iterdir() if d.is_dir()] |
| 36 | + continue |
| 37 | + elif p.is_file(): |
| 38 | + yield p |
| 39 | + |
| 40 | +def build_new_name(file_path: Path) -> Path | None: |
| 41 | + name = file_path.name |
| 42 | + # Tìm vị trí " copy" trước phần mở rộng đầu tiên (tức dấu chấm đầu tiên từ trái sang phải sau token) |
| 43 | + if TARGET_TOKEN not in name: |
| 44 | + return None |
| 45 | + # Tách phần trước và sau token |
| 46 | + prefix, suffix = name.split(TARGET_TOKEN, 1) |
| 47 | + if not suffix: |
| 48 | + return None # Không có phần mở rộng tiếp theo |
| 49 | + # Nếu suffix bắt đầu bằng '.' thì đó là phần mở rộng (có thể phức tạp: .md, .tar.gz ...). Giữ nguyên toàn bộ suffix. |
| 50 | + if not suffix.startswith('.'): |
| 51 | + # Không đúng dạng mong muốn: yêu cầu phải có .ext |
| 52 | + return None |
| 53 | + new_name = f"{prefix}{INSERT_TOKEN}{suffix}" |
| 54 | + if new_name == name: |
| 55 | + return None |
| 56 | + return file_path.with_name(new_name) |
| 57 | + |
| 58 | +def should_skip(path: Path) -> bool: |
| 59 | + # Bỏ qua nếu bất kỳ phần nào trong parts thuộc EXCLUDE_DIRS |
| 60 | + return any(part in EXCLUDE_DIRS for part in path.parts) |
| 61 | + |
| 62 | +def process(root: Path, apply: bool) -> int: |
| 63 | + count = 0 |
| 64 | + for fp in iter_files(root): |
| 65 | + if should_skip(fp): |
| 66 | + continue |
| 67 | + new_path = build_new_name(fp) |
| 68 | + if not new_path: |
| 69 | + continue |
| 70 | + if new_path.exists(): |
| 71 | + print(f"[WARN] Bỏ qua vì đã tồn tại: {new_path}") |
| 72 | + continue |
| 73 | + print(f"{'RENAME' if apply else 'DRY-RUN'}: {fp} -> {new_path}") |
| 74 | + if apply: |
| 75 | + try: |
| 76 | + fp.rename(new_path) |
| 77 | + count += 1 |
| 78 | + except OSError as e: |
| 79 | + print(f"[ERROR] Không thể đổi tên {fp}: {e}", file=sys.stderr) |
| 80 | + else: |
| 81 | + count += 1 |
| 82 | + return count |
| 83 | + |
| 84 | +def parse_args(): |
| 85 | + parser = argparse.ArgumentParser(description='Đổi tên các file chứa " copy" thành thêm hậu tố .vi trước phần mở rộng.') |
| 86 | + parser.add_argument('--root', type=Path, default=Path('.'), help='Thư mục gốc để quét (mặc định: .)') |
| 87 | + parser.add_argument('--apply', action='store_true', help='Thực hiện đổi tên thật (mặc định chỉ dry-run).') |
| 88 | + return parser.parse_args() |
| 89 | + |
| 90 | +def main(): |
| 91 | + args = parse_args() |
| 92 | + root = args.root.resolve() |
| 93 | + if not root.exists() or not root.is_dir(): |
| 94 | + print(f"Thư mục không hợp lệ: {root}", file=sys.stderr) |
| 95 | + sys.exit(1) |
| 96 | + apply = args.apply |
| 97 | + total = process(root, apply) |
| 98 | + print(f"Tổng số file {'đã đổi tên' if apply else 'sẽ đổi tên'}: {total}") |
| 99 | + |
| 100 | +if __name__ == '__main__': |
| 101 | + main() |
0 commit comments