from __future__ import annotations import os from pathlib import Path def existing_input_paths(args: list[str], *, print_missing: bool = False) -> list[Path]: paths: list[Path] = [] for arg in args: path = Path(arg).expanduser() if path.exists(): paths.append(path.resolve()) elif print_missing: print(f"Skipping missing path: {arg}") return paths def determine_working_dir(args: list[str]) -> Path: paths = existing_input_paths(args) if not paths: raise RuntimeError("No existing input paths were provided.") if len(paths) == 1: path = paths[0] return path if path.is_dir() else path.parent try: common = Path(os.path.commonpath([str(path) for path in paths])) except ValueError as exc: raise RuntimeError("Input paths do not have a common ancestor.") from exc if str(common) == common.anchor: raise RuntimeError("Input paths only share the filesystem root; choose items closer together.") return common def collect_files( args: list[str], *, allowed_exts: set[str] | None = None, print_missing: bool = False, sort_by_name: bool = False, ) -> list[Path]: files: list[Path] = [] seen: set[Path] = set() for arg in args: path = Path(arg).expanduser() if path.is_file(): candidates = [path] if allowed_exts is None or path.suffix.lower() in allowed_exts else [] elif path.is_dir(): candidates = [ child for child in path.rglob("*") if child.is_file() and (allowed_exts is None or child.suffix.lower() in allowed_exts) ] else: if print_missing: print(f"Skipping missing path: {arg}") continue for candidate in candidates: resolved = candidate.resolve() if resolved not in seen: seen.add(resolved) files.append(resolved) if sort_by_name: return sorted(files, key=lambda path: path.name.lower()) return sorted(files, key=lambda path: str(path).lower()) def first_file_from_path(path: Path) -> Path | None: path = path.expanduser() if path.is_file(): return path.resolve() if path.is_dir(): for child in sorted(path.rglob("*"), key=lambda item: str(item).lower()): if child.is_file(): return child.resolve() return None def cleanup_roots_from_args(args: list[str]) -> list[Path]: roots: list[Path] = [] seen: set[Path] = set() for arg in args: path = Path(arg).expanduser() if path.is_dir(): resolved = path.resolve() if resolved not in seen: seen.add(resolved) roots.append(resolved) return roots def unique_path(path: Path) -> Path: if not path.exists(): return path for index in range(1, 10000): candidate = path.with_name(f"{path.stem}-{index}{path.suffix}") if not candidate.exists(): return candidate raise RuntimeError(f"Could not find an available name for {path}") def unique_existing_target(path: Path, planned_sources: set[Path]) -> Path: if not path.exists() or path.resolve() in planned_sources: return path for index in range(1, 10000): candidate = path.with_name(f"{path.stem}-{index}{path.suffix}") if not candidate.exists() or candidate.resolve() in planned_sources: return candidate raise RuntimeError(f"Could not find available target name for {path}") def remove_empty_dirs(cleanup_roots: list[Path], keep_dirs: set[Path]) -> list[Path]: removed: list[Path] = [] candidates: set[Path] = set() for root in cleanup_roots: if not root.exists() or not root.is_dir(): continue for directory, subdirs, _files in os.walk(root, topdown=False): path = Path(directory).resolve() candidates.add(path) for subdir in subdirs: child = (Path(directory) / subdir).resolve() if child.exists() and child.is_dir(): candidates.add(child) for directory in sorted(candidates, key=lambda path: len(path.parts), reverse=True): if directory in keep_dirs: continue try: directory.rmdir() removed.append(directory) except OSError: pass return removed def display_path_from_working_dir(path: Path, working_dir: Path) -> str: try: relative = path.resolve().relative_to(working_dir.resolve()) except ValueError: return str(path) return "." if str(relative) == "." else str(relative)