Last active
November 8, 2025 19:20
-
-
Save jasonbot/a5e5ec420bc87ec31e9ed20ac865f981 to your computer and use it in GitHub Desktop.
My work-in-progress "illegal character killer"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import collections | |
| import collections.abc | |
| import itertools | |
| import os | |
| import pathlib | |
| import sys | |
| REPLACE_TUPLES = (("/.", "/"),) | |
| REPLACE_CHARS = ':*?"![]' | |
| VERY_REPLACE = {'$': 'S'} | |
| def walk(path: pathlib.Path) -> collections.abc.Generator[pathlib.Path]: | |
| if not path.exists(): | |
| print(path, "went away") | |
| return | |
| for item in list(path.iterdir()): | |
| if item.is_dir(): | |
| yield item | |
| yield from walk(item) | |
| elif item.is_file(): | |
| yield item | |
| def need_rename(filename: pathlib.Path | str): | |
| if not isinstance(filename, pathlib.Path): | |
| filename = pathlib.Path(filename) | |
| if filename.is_dir(): | |
| c = collections.Counter(f.name.lower() for f in filename.parent.iterdir()) | |
| if c[filename.name.lower()] > 1: | |
| newfn = next( | |
| f.name | |
| for f in filename.parent.iterdir() | |
| if f.name.lower() == filename.name.lower() | |
| ) | |
| if filename.name != newfn: | |
| return filename.parent / newfn | |
| elif ( | |
| any(c in str(filename) for c in REPLACE_CHARS) | |
| or any(c in str(filename) for c, _ in REPLACE_TUPLES) | |
| or filename.name.startswith(".") | |
| ): | |
| if filename.name.startswith("."): | |
| filename = filename.parent / ("_" + filename.name.lstrip(".")) | |
| for c in REPLACE_CHARS: | |
| filename = str(filename).replace(c, "_") | |
| for c, v in VERY_REPLACE.items(): | |
| filename = str(filename).replace(c, v) | |
| for old, new in REPLACE_TUPLES: | |
| filename = str(filename).replace(old, new) | |
| filename = str(filename).replace("/.", "/") | |
| np = pathlib.Path(filename) | |
| return np | |
| def prune_empty_dir_up_to(base: pathlib.Path, empty_dir: pathlib.Path, *, depth=0): | |
| if base not in empty_dir.parents: | |
| return | |
| if empty_dir.is_dir() and len(list(empty_dir.iterdir())) == 0: | |
| print(f"{' ' * depth}- Unlinking {empty_dir}") | |
| empty_dir.rmdir() | |
| prune_empty_dir_up_to(base, empty_dir.parent, depth=depth+1) | |
| p = pathlib.Path(".") | |
| doit = "--do-it" in sys.argv | |
| for file in walk(p): | |
| if file.is_dir() and len(list(file.iterdir())) == 0: | |
| print(file, "is an empty dir") | |
| if doit: | |
| prune_empty_dir_up_to(p, file) | |
| elif rename_to := need_rename(file): | |
| print(file, "->", rename_to) | |
| if file.is_file(): | |
| if doit: | |
| try: | |
| os.makedirs(rename_to.parent) | |
| except: | |
| pass | |
| os.rename(file, rename_to) | |
| elif file.is_dir(): | |
| for item_to_move in file.iterdir(): | |
| new_path = rename_to / item_to_move.name | |
| print(" ", item_to_move, "->", new_path) | |
| if doit: | |
| pp = str(new_path) | |
| for ii in range(1, 10): | |
| try: | |
| os.rename(item_to_move, pp) | |
| break | |
| except OSError as e: | |
| pp = f"{str(new_path)}_{ii+1}" | |
| print(f" {e}: trying") | |
| if doit: | |
| os.rmdir(file) | |
| if not doit: | |
| print("If you like this course of events, use --do-it and rerun") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment