"""ta_batch_rename.py - batch-rename texture files to a naming convention. The finished artifact from The Technical Artist's "First Pipeline Script" tutorial: https://thetechnicalartist.example/python/first-pipeline-script.html Usage: python ta_batch_rename.py D:/textures # dry run (prints the plan) python ta_batch_rename.py D:/textures --apply # actually rename python ta_batch_rename.py D:/textures --prefix T_ --apply What it does: old: "rock albedo 2.png" new: "T_Rock_Albedo_02.png" - lowercases-then-titlecases words, joins with underscores - zero-pads trailing numbers to two digits - adds the texture prefix if missing - refuses to run if two files would collide on the same new name - writes a rename log next to the folder so every run is reversible No dependencies beyond the standard library. Python 3.8+. """ import argparse import datetime import re import sys from pathlib import Path def convention(name: str, prefix: str = "T_") -> str: """Convert an arbitrary filename (no extension) to the convention.""" # split on spaces, dashes, underscores, and camelCase boundaries words = re.split(r"[\s\-_]+", re.sub(r"(?<=[a-z0-9])(?=[A-Z])", " ", name)) words = [w for w in words if w] # zero-pad a trailing number to two digits if words and words[-1].isdigit(): words[-1] = f"{int(words[-1]):02d}" new = "_".join(w if w.isdigit() else w.title() for w in words) if not new.startswith(prefix): new = prefix + new return new def plan_renames(folder: Path, prefix: str): """Build (old, new) pairs. Raises if two files would collide.""" pairs = [] for f in sorted(folder.iterdir()): if not f.is_file(): continue new_name = convention(f.stem, prefix) + f.suffix.lower() if new_name != f.name: pairs.append((f, f.with_name(new_name))) targets = [b.name for _, b in pairs] dupes = {t for t in targets if targets.count(t) > 1} if dupes: raise SystemExit(f"COLLISION: multiple files map to {sorted(dupes)} - fix by hand first.") return pairs def main() -> int: ap = argparse.ArgumentParser(description="Batch-rename textures to the studio convention.") ap.add_argument("folder", type=Path, help="folder of textures to rename") ap.add_argument("--prefix", default="T_", help="type prefix (default: T_)") ap.add_argument("--apply", action="store_true", help="actually rename (default is a dry run)") args = ap.parse_args() if not args.folder.is_dir(): print(f"error: {args.folder} is not a folder", file=sys.stderr) return 2 pairs = plan_renames(args.folder, args.prefix) if not pairs: print("nothing to do - everything already matches the convention.") return 0 for old, new in pairs: print(f"{old.name:<40} -> {new.name}") if not args.apply: print(f"\n{len(pairs)} file(s) would be renamed. Re-run with --apply to do it.") return 0 log = args.folder / f"_rename_log_{datetime.date.today()}.txt" with log.open("a", encoding="utf-8") as fh: for old, new in pairs: old.rename(new) fh.write(f"{old.name} -> {new.name}\n") print(f"\nrenamed {len(pairs)} file(s). Log: {log}") return 0 if __name__ == "__main__": sys.exit(main())