#!/usr/bin/env python3 import os import re import shlex # Characters we'll allow directly in the *basename* (before .pk3) ALLOWED_BASE = set("abcdefghijklmnopqrstuvwxyz0123456789_-") def sanitize_base(name_base: str) -> str: # Lowercase and replace anything not in ALLOWED_BASE with underscore s = [] for ch in name_base.lower(): if ch in ALLOWED_BASE: s.append(ch) else: s.append("_") base = "".join(s) # Collapse multiple underscores base = re.sub(r"_+", "_", base) # Strip leading/trailing underscores base = base.strip("_") return base or "map" def main(): # Work in current directory; restrict to .pk3 files all_names = [f for f in os.listdir(".") if os.path.isfile(f)] pk3 = [n for n in all_names if n.lower().endswith(".pk3")] # 1) Find case-insensitive collisions groups = {} for name in pk3: key = name.lower() groups.setdefault(key, []).append(name) collision_groups = {k: v for k, v in groups.items() if len(v) > 1} # 2) Find names with "weird" characters weird = [] allowed_full = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") for name in pk3: if any(ch not in allowed_full for ch in name): weird.append(name) to_fix = set(weird) for vs in collision_groups.values(): to_fix.update(vs) if not to_fix: print("Nothing to fix: no weird characters or case-colliding names found.") return print(f"# Found {len(pk3)} .pk3 files") print(f"# {len(weird)} names with non-trivial characters") print(f"# {len(collision_groups)} case-insensitive collision groups") print(f"# Will propose renames for {len(to_fix)} files\n") # Names that are *not* being touched stay as-is, so we must not collide with them unaffected = set(pk3) - to_fix used = set(unaffected) rename_map = {} # Helper to allocate a unique name def allocate(base: str, ext: str) -> str: candidate = f"{base}{ext}" if candidate not in used: used.add(candidate) return candidate # Try with suffixes i = 2 while True: candidate = f"{base}_{i}{ext}" if candidate not in used: used.add(candidate) return candidate i += 1 # 3) Handle collision groups first, so they get clean, related names for key, vs in sorted(collision_groups.items()): sample = vs[0] if "." in sample: base_part, ext_part = sample.rsplit(".", 1) ext_part = "." + ext_part else: base_part, ext_part = sample, "" sani_base = sanitize_base(base_part) ext_lower = ext_part.lower() # Prefer already-lowercase names first vs_sorted = sorted(vs, key=lambda n: (not n.islower(), n)) for idx, name in enumerate(vs_sorted): new_name = allocate(sani_base, ext_lower) rename_map[name] = new_name # 4) Handle "weird-only" names (that weren't in a collision group) weird_only = sorted(to_fix - set(rename_map.keys())) for name in weird_only: if "." in name: base_part, ext_part = name.rsplit(".", 1) ext_part = "." + ext_part else: base_part, ext_part = name, "" sani_base = sanitize_base(base_part) ext_lower = ext_part.lower() new_name = allocate(sani_base, ext_lower) rename_map[name] = new_name # 5) Print the mv commands (dry-run by default) print("# Review these commands carefully before running them.") print("# If they look good, you can save them to a shell script and execute.") for old, new in sorted(rename_map.items()): if old == new: continue print("mv -v -- " + shlex.quote(old) + " " + shlex.quote(new)) if __name__ == "__main__": main()