#!/usr/bin/env python3 """ © 2025 Adam Skotarczak (adam@skotarczak.net) v0.3.1 (01.05.2025) A standalone directory scanner that can also be integrated as a module. In its current version, simply place the file into any directory and execute it with python scanner.py. The script generates a tree.txt file containing the complete directory structure in ASCII format. I developed this tool to conveniently document directory trees, particularly for use in Markdown READMEs. usage: scanner.py [-h] [-n MAX_FILES_PER_DIR] [-d MAX_DEPTH] [--no-align-comments] [-l {de,en}] [--ignore IGNORE] [-o OUTPUT] [root_path] Generiert eine ASCII-Baumstruktur eines Verzeichnisses. positional arguments: root_path Pfad des Stammverzeichnisses (default: aktuelles Verzeichnis). options: -h, --help show this help message and exit -n, --max-files-per-dir MAX_FILES_PER_DIR Maximale Anzahl Dateien pro Verzeichnis (default: 2). -d, --max-depth MAX_DEPTH Maximale Rekursionstiefe; unbegrenzt, wenn nicht gesetzt. --no-align-comments Deaktiviert das Ausrichten der Kommentare am Zeilenende. -l, --language {de,en} Sprache der Programmausgabe (de oder en). --ignore, -x IGNORE Verzeichnisse, die ignoriert werden sollen (z.B. .git, __pycache__). Mehrfach verwendbar. -o, --output OUTPUT Pfad und Name der Ausgabedatei (Standard: tree.txt) Original File: """ import os import argparse import time from typing import Optional, List class TreeScannerConfig: """Konfigurationsklasse für den TreeScanner. Attributes: root_path (str): Pfad des Stammverzeichnisses. folder_icon (str): Icon für Ordner. file_icon (str): Icon für Dateien und Platzhalter. max_files_per_dir (int): Maximale Anzahl Dateien pro Verzeichnis. max_depth (Optional[int]): Maximale Rekursionstiefe. align_comments (bool): Kommentare am Zeilenende ausrichten. language (str): Sprache der Programmausgabe (de oder en). output_file (str): Pfad und Name der Ausgabedatei. ignored_dirs (Optional[List[str]]): Liste von Verzeichnissen, die ignoriert werden sollen. """ def __init__( self, root_path: str = ".", folder_icon: str = "\U0001F4C1", file_icon: str = "\U0001F4C4", max_files_per_dir: int = 100, max_depth: Optional[int] = None, align_comments: bool = True, language: str = "de", output_file: str = "tree.txt", ignored_dirs: Optional[List[str]] = None ): self.root_path = root_path self.folder_icon = folder_icon self.file_icon = file_icon self.max_files_per_dir = max_files_per_dir self.max_depth = max_depth self.align_comments = align_comments self.language = language self.output_file = output_file self.ignored_dirs = ignored_dirs or [] class TreeScanner: """Klasse zum Scannen von Verzeichnissen und Erzeugen einer ASCII-Baumstruktur.""" def __init__(self, config: TreeScannerConfig): """Initialisiert den TreeScanner. Args: config (TreeScannerConfig): Konfiguration für den Scanner. """ self.last_output = time.time() self.config = config self.folder_count = 0 self.file_count = 0 self.messages = { "de": { "summary": "Es wurden {folders} Verzeichnisse und {files} Dateien gescannt. Datei {file} geschrieben." }, "en": { "summary": "Scanned {folders} folders and {files} files. File {file} written." } } def scan_directory(self, path: str, depth: int = 0, prefix: str = "") -> List[str]: """Scannt ein Verzeichnis und gibt eine Liste von ASCII-Zeilen zurück. Args: path (str): Pfad des zu scannenden Verzeichnisses. depth (int): Aktuelle Rekursionstiefe. prefix (str): Präfix für Einrückung und Connectoren. Returns: List[str]: Zeilen der Baumstruktur. """ lines: List[str] = [] try: entries = sorted(os.listdir(path)) except PermissionError: return [f"{prefix}└── [Zugriff verweigert] {path}"] folders = [e for e in entries if os.path.isdir(os.path.join(path, e)) and e not in self.config.ignored_dirs] files = [e for e in entries if os.path.isfile(os.path.join(path, e))] for idx, folder in enumerate(folders): self.folder_count += 1 folder_path = os.path.join(path, folder) connector = "├── " if idx < len(folders) - 1 or files else "└── " lines.append(f"{prefix}{connector}{self.config.folder_icon} {folder}") if self.config.max_depth is None or depth < self.config.max_depth: extension = "│ " if idx < len(folders) - 1 or files else " " lines.extend(self.scan_directory(folder_path, depth + 1, prefix + extension)) visible_files = files[: self.config.max_files_per_dir] remaining = len(files) - len(visible_files) combined = visible_files.copy() if remaining > 0: combined.append(f"") for idx, name in enumerate(combined): if not name.startswith("= 5: print(f"[Info] {self.folder_count + self.file_count} Einträge gescannt...", flush=True) self.last_output = time.time() connector = "├── " if idx < len(combined) - 1 else "└── " lines.append(f"{prefix}{connector}{self.config.file_icon} {name}") return lines def align_lines_with_comments(self, lines: List[str]) -> List[str]: """Richtet alle Zeilen so aus, dass Kommentare am gleichen Spaltenindex stehen. Args: lines (List[str]): Liste der Baumstruktur-Zeilen ohne Kommentare. Returns: List[str]: Zeilen mit ausgerichteten Kommentar-Platzhaltern (#). """ max_length = max(len(line.rstrip()) for line in lines) aligned: List[str] = [] for line in lines: text = line.rstrip() padding = " " * (max_length - len(text) + 2) aligned.append(text + padding + "# ") return aligned def generate_tree(self) -> str: """Generiert den vollständigen Verzeichnisbaum als String. Returns: str: Mehrzeiliger String mit Ordner-Icon, Ordner- und Dateienstruktur. """ root_name = os.path.basename(os.path.abspath(self.config.root_path)) or self.config.root_path lines = [f"{self.config.folder_icon} {root_name}/"] lines += self.scan_directory(self.config.root_path) if self.config.align_comments: lines = self.align_lines_with_comments(lines) return "\n".join(lines) def print_summary(self, output_file: str) -> None: """Gibt eine Zusammenfassung des Scanlaufs aus. Args: output_file (str): Name der geschriebenen Ausgabedatei. """ message_template = self.messages.get(self.config.language, self.messages["de"]) print(message_template["summary"].format(folders=self.folder_count, files=self.file_count, file=output_file)) def main(): """Standalone-Ausführung mit CLI-Parameter-Unterstützung.""" parser = argparse.ArgumentParser(description="Generiert eine ASCII-Baumstruktur eines Verzeichnisses.") parser.add_argument("root_path", nargs="?", default=".", help="Pfad des Stammverzeichnisses (default: aktuelles Verzeichnis).") parser.add_argument("-n", "--max-files-per-dir", type=int, default=2, help="Maximale Anzahl Dateien pro Verzeichnis (default: 2).") parser.add_argument("-d", "--max-depth", type=int, help="Maximale Rekursionstiefe; unbegrenzt, wenn nicht gesetzt.") parser.add_argument("--no-align-comments", action="store_false", dest="align_comments", help="Deaktiviert das Ausrichten der Kommentare am Zeilenende.") parser.add_argument("-l", "--language", type=str, default="de", choices=["de", "en"], help="Sprache der Programmausgabe (de oder en).") parser.add_argument( "--ignore", "-x", action="append", help="Verzeichnisse, die ignoriert werden sollen (z.B. .git, __pycache__). Mehrfach verwendbar." ) parser.add_argument("-o", "--output", type=str, help="Pfad und Name der Ausgabedatei (Standard: tree.txt)") args = parser.parse_args() output_file = args.output if args.output else "tree.txt" ignored_dirs = args.ignore if args.ignore else [] # Pfad validieren if not os.path.isdir(args.root_path): print(f"Fehler: Der angegebene Pfad '{args.root_path}' ist kein gültiges Verzeichnis oder anderer falscher Parameter.") return output_dir = os.path.dirname(output_file) if output_dir: os.makedirs(output_dir, exist_ok=True) config = TreeScannerConfig( root_path=args.root_path, max_files_per_dir=args.max_files_per_dir, max_depth=args.max_depth, align_comments=args.align_comments, language=args.language, output_file=output_file, ignored_dirs=ignored_dirs ) scanner = TreeScanner(config) tree_output = scanner.generate_tree() with open(config.output_file, "w", encoding="utf-8") as f: f.write(tree_output + "\n") scanner.print_summary(config.output_file) if __name__ == "__main__": main()