diff --git a/CLAUDE.md b/CLAUDE.md index 7ad4ed3..ac09df2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,17 +150,21 @@ Abhaengigkeiten: `ezdxf`, Standard-Library. Benoetigen Umgebungsvariablen (`DXFM | `omniflo_utils.py` | Gemeinsame Hilfsfunktionen fuer Omniflo-Skripte: `ROW_GROUPS` (Reihen-Gruppierung), `SWITCH_FILTERS` (Schalter-Filter), `build_row_layout`, `import_element_as_block`, `draw_cross`, `load_omniflo_data` | | `set_einfuegepkt.py` | Setzt `$INSBASE` (Einfuegepunkt) in Omniflo-DXF-Dateien. Schalter je Typ: `--boegen`, `--weichen45`, `--weichen90`, `--weichenkoerper`, `--weichen-parallel`, `--delta`, `--dreifachweiche`, `--sternweiche`, `--show-omniflo` | | `set_koords.py` | Setzt Koordinatensystem-Bloecke (K1-K4) in Omniflo-DXF-Dateien. Schalter: `--k1set` bis `--k4set`, `--set-all`, `--force`, `--show-omniflo`, `--test`, `--number` | -| `set_attributs.py` | Fuegt ATTDEF-Attribute zu Omniflo-DXF-Dateien hinzu. Liest Konfiguration aus `cfg/attradd.cfg`. Schalter: `--number`, `--dry-run` | | `export_sivas.py` | Erzeugt Sivas-Export-CSV mit Summierungszeilen (ILS Automation, Omniflo Sum). Aufruf: `python export_sivas.py ` | | `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py ` | | `split_block_library.py` | Teilt `ils_library.dxf` in einzelne DXF-Dateien pro Block auf (mit Abhaengigkeiten). Export-Prefixe: Vario_, Staustrecke_, _3D_AS_, _3D_ES_ | +### Entwickler-Tools (`lib/`) - kein ezdxf, reine Text-/AutoLISP-Verarbeitung + +| Skript | Beschreibung | +|---|---| +| `dbg2lsp.py` | Instrumentiert AutoLISP-Funktionen automatisch mit `ssg_dbg.lsp` (`dbgf`/`dbg`/`dbgreturn`/`dbgopen`/`dbgclose`). Schalter: `--method` (Funktion instrumentieren), `--recursive` (Aufrufgraph mitverfolgen), `--add-open DATEI METHODE` (Debug-Datei oeffnen/schliessen), `--dry-run`. Details: `doc/dbg2lsp.md` | + ### Typische Verarbeitungskette (Omniflo-DXF) -1. `set_attributs.py` - Attribute zu Quell-DXFs hinzufuegen -2. `set_einfuegepkt.py` - Einfuegepunkte berechnen und setzen -3. `set_koords.py` - Koordinatensysteme (K1-K4) setzen -4. `export_sivas.py` / `export_csv.py` - Export aus Zeichnung +1. `set_einfuegepkt.py` - Einfuegepunkte berechnen und setzen +2. `set_koords.py` - Koordinatensysteme (K1-K4) setzen +3. `export_sivas.py` / `export_csv.py` - Export aus Zeichnung ## Entwicklungsstand diff --git a/bin/dbg2lsp.bat b/bin/dbg2lsp.bat new file mode 100644 index 0000000..1b40db3 --- /dev/null +++ b/bin/dbg2lsp.bat @@ -0,0 +1,9 @@ +@echo off +REM ================================================================ +REM Instrumentiert AutoLISP-Funktionen mit ssg_dbg.lsp +REM (dbgf/dbg/dbgreturn/dbgopen/dbgclose) - siehe doc/dbg2lsp.md +REM ================================================================ + +call "%~dp0setenv.bat" + +python "%DXFM_LIB%\dbg2lsp.py" %* diff --git a/doc/dbg2lsp.md b/doc/dbg2lsp.md new file mode 100644 index 0000000..996631a --- /dev/null +++ b/doc/dbg2lsp.md @@ -0,0 +1,206 @@ +# dbg2lsp + +Instrumentiert AutoLISP-Funktionen (`.lsp`) automatisch mit den `ssg_dbg.lsp`-Routinen +(`dbgf`, `dbg`, `dbgreturn`, `dbgopen`, `dbgclose`) - so wie es beim manuellen Debuggen +bereits von Hand gemacht wird (siehe z.B. `Lisp/KreiselInsert.lsp::ils-eckrad-insert`). +Erspart das muehsame und fehleranfaellige manuelle Einfuegen dieser Aufrufe in +lange Aufrufketten (Kreisel/VarioFoerderer/Gefaellestrecke/Separator-Scanner o.ae.), +bevor ein Testlauf in BricsCAD gestartet wird. + +- [Aufruf](#aufruf) +- [Was wird eingefuegt](#was-wird-eingefuegt) + - [--method](#--method) + - [--recursive](#--recursive) + - [--add-open](#--add-open) +- [Schalter](#schalter) +- [Typischer Ablauf](#typischer-ablauf) +- [Beispiele](#beispiele) +- [Funktionsweise / Aufloesung](#funktionsweise--aufloesung) +- [Grenzen](#grenzen) +- [Idempotenz](#idempotenz) + +## Aufruf + +``` +bin\dbg2lsp.bat [--method NAME ...] [--recursive] [--add-open DATEI METHODE] [Schalter] +``` + +Das Batch-Skript ruft `setenv.bat` auf und startet `lib/dbg2lsp.py` mit den +uebergebenen Argumenten. `` dient als Anker (ihr Verzeichnis wird immer +mitdurchsucht) - die gesuchte Funktion muss nicht zwingend in genau dieser Datei +stehen, siehe [Funktionsweise](#funktionsweise--aufloesung). + +## Was wird eingefuegt + +### --method + +Fuer die angegebene Funktion (mehrfach angebbar) wird direkt als erste Rumpf-Form + +```lisp +(dbgf "funktionsname") +(dbg 'parameter1) +(dbg 'parameter2) +... +``` + +eingefuegt (ein `dbg`-Aufruf je Eingabeparameter - die Locals nach `/` in der +Parameterliste NICHT, das sind keine Eingabewerte), und die **letzte Form des +Funktionsrumpfs** wird eingeklammert: + +```lisp +(dbgreturn ) +``` + +`dbgreturn` loggt und liefert den Rueckgabewert unveraendert weiter (siehe +`Lisp/ssg_dbg.lsp`) - das Verhalten der Funktion aendert sich dadurch nicht. + +### --recursive + +Verfolgt zusaetzlich den Aufrufgraphen: jede Funktion, die im (Original-)Rumpf +aufgerufen wird - auch beliebig verschachtelt, z.B. in einem `if`/`progn`, aber +NICHT innerhalb einer `'quotierten Liste` (die ist reine Datenliste, z.B. +`'(("OSMODE") ("CECOLOR"))`) - wird ebenfalls mit `--method`-Logik instrumentiert, +sofern sich ihr `(defun ...)` in einem der durchsuchten Verzeichnisse findet. + +AutoLISP-/BricsCAD-Builtins (`princ`, `command`, `strcat`, `rtos`, `entmake`, `vla-*`, ...) +haben dort naturgemaess kein `defun` und werden dadurch automatisch **nicht** angefasst - +es gibt keine Builtin-Blacklist zu pflegen. Die `ssg_dbg.lsp`-Funktionen selbst +(`dbgf`, `dbg`, `dbgp`, `dbgmsg`, `dbgreturn`, `dbgopen`, `dbgclose`, `dbgflush`, `dbgon`, `dbgoff` +und ihre internen `dbg-*`-Helfer) sind fest ausgeschlossen, damit eine bereits +instrumentierte Funktion nicht dazu fuehrt, dass das Debug-Framework sich selbst +instrumentiert. + +Bereits instrumentierte Funktionen werden uebersprungen (siehe [Idempotenz](#idempotenz)), +ihr Rumpf wird aber trotzdem nach weiteren, noch nicht instrumentierten Aufrufen +durchsucht - die Rekursion bricht dadurch nicht an einer schon bearbeiteten Stelle ab. + +### --add-open + +Fuegt in der angegebenen Methode (typischerweise ein `c:BEFEHL`-Kommando, das +einen ganzen Testlauf umspannt) ein: + +```lisp +(dbgopen "" "") +``` + +als erste Rumpf-Form, sowie + +```lisp +(dbgclose) +``` + +vor **jedem** `(exit)`-Aufruf im Rumpf (auch verschachtelt) und zusaetzlich am +Ende des Rumpfs - ausser die letzte Rumpf-Form ist bereits selbst ein `(exit)` +(dann waere ein zweites `dbgclose` ohnehin unerreichbarer Code). + +`--add-open DATEI METHODE` ist unabhaengig von `--method`/`--recursive` nutzbar +und mehrfach angebbar; alle drei Mechanismen lassen sich in einem Aufruf kombinieren. + +## Schalter + +| Schalter | Beschreibung | +|---|---| +| `datei` (Pflicht, Position 1) | Anker-`.lsp`-Datei; ihr Verzeichnis wird immer mitdurchsucht | +| `--method NAME` | Funktion mit `dbgf`/`dbg`/`dbgreturn` instrumentieren (mehrfach angebbar) | +| `--recursive` | Auch alle im Rumpf aufgerufenen projekteigenen Funktionen instrumentieren | +| `--search-dir DIR` | Weiteres Verzeichnis nach `.lsp`-Dateien durchsuchen (mehrfach angebbar). Default: `Lisp/` und das Verzeichnis von `datei` | +| `--max-depth N` | Maximale Rekursionstiefe fuer `--recursive` (Default: `3`) | +| `--exclude NAME` | Funktionsname von `--recursive` ausschliessen (mehrfach angebbar) | +| `--add-open DATEI METHODE` | `dbgopen`/`dbgclose` in `METHODE` (aus `DATEI`) einfuegen (mehrfach angebbar) | +| `--dbg-file NAME` | Dateiname fuer `(dbgopen ...)`. Default: aus dem `--add-open`-Methodennamen abgeleitet (`c:TEST_MUBEA` -> `test_mubea.dbg`) | +| `--dbg-envvar NAME` | Umgebungsvariable fuer `(dbgopen ...)` (Default: `DXFM_LOG`) | +| `--force` | Auch instrumentieren, wenn bereits `(dbgf ...)`/`(dbgopen ...)` vorhanden zu sein scheint | +| `--dry-run` | Nur ein Diff anzeigen, keine Dateien schreiben | + +## Typischer Ablauf + +1. **Immer zuerst `--dry-run`** laufen lassen und das Diff pruefen (mehrere Dateien + koennen betroffen sein, siehe `--recursive`). +2. Ohne `--dry-run` erneut aufrufen - die Dateien werden direkt (in-place) geschrieben. +3. In BricsCAD `(load ...)` neu laden bzw. den Testlauf starten. +4. Die `.dbg`-Datei (per `--add-open` geoeffnet, Pfad = `%DXFM_LOG%\<--dbg-file>`) + auswerten. +5. Da die Dateien in-place geschrieben werden und ueber Git versioniert sind: nach + dem Debuggen die Debug-Aufrufe entweder manuell entfernen oder die Datei(en) + per `git checkout -- ` verwerfen (das Werkzeug selbst hat keinen + "--remove"-Schalter). + +## Beispiele + +Eine einzelne Funktion instrumentieren: + +``` +bin\dbg2lsp.bat tests\test_mubea.lsp --method mubea:build-separator-one --dry-run +``` + +Eine Funktion UND alles, was sie aufruft (bis Tiefe 3, Default): + +``` +bin\dbg2lsp.bat tests\test_mubea.lsp --method mubea:build-kreisel --recursive --dry-run +``` + +Mehrere Einstiegspunkte in einem Lauf, plus Oeffnen/Schliessen der Debug-Datei +im umgebenden Testbefehl - das entspricht genau der Instrumentierung, die fuer +den urspruenglichen Scanner-Bug in `TEST_MUBEA` von Hand gemacht wurde: + +``` +bin\dbg2lsp.bat tests\test_mubea.lsp ^ + --method mubea:build-kreisel ^ + --method mubea:build-vario-one ^ + --method mubea:build-gf-one ^ + --method mubea:build-separator-one ^ + --recursive ^ + --add-open tests\test_mubea.lsp c:TEST_MUBEA +``` + +Bestimmte generische Helfer von der Rekursion ausschliessen (z.B. um `ssg-start`/`ssg-end` +nicht mit anzufassen): + +``` +bin\dbg2lsp.bat tests\test_mubea.lsp --method mubea:build-kreisel --recursive ^ + --exclude ssg-start --exclude ssg-end +``` + +## Funktionsweise / Aufloesung + +- `datei` wird zuerst relativ zum aktuellen Arbeitsverzeichnis, sonst relativ zur + Projektwurzel (erkannt an `.git/` bzw. `Lisp/`, ausgehend von `datei` nach oben + gesucht) aufgeloest. +- Durchsucht werden: das Verzeichnis von `datei`, `Lisp/` (relativ zur Projektwurzel), + jedes `--search-dir` sowie das Verzeichnis jeder `--add-open`-Datei - jeweils + rekursiv nach `*.lsp`. +- Aus all diesen Dateien wird EIN Index `Funktionsname -> (defun ...)` aufgebaut + (case-insensitiv, wie AutoLISP-Symbole zur Laufzeit). `--method`/`--add-open` + muessen daher nicht in `datei` selbst stehen, sondern werden ueberall gefunden. +- Klammer-/String-/Kommentar-Erkennung ist zeichenweise implementiert (kein + vollstaendiger Lisp-Reader) - genuegt aber, um Top-Level-`(defun ...)`-Bloecke, + ihre Parameterliste und die letzte Rumpf-Form zuverlaessig zu erkennen, auch bei + mehrzeiligen Parameterlisten mit Locals nach `/`. +- Alle Aenderungen werden zunaechst als Liste von reinen Text-Einfuegungen + gesammelt (keine Loeschungen) und erst am Ende je Datei angewandt - Positionen + bleiben dadurch unabhaengig von der Bearbeitungsreihenfolge gueltig. + +## Grenzen + +- **Nur die letzte Rumpf-Form wird zu `dbgreturn`.** Fruehe `(exit)`-Ausstiege + MITTEN im Rumpf werden von `--method` nicht mit einem eigenen `dbgreturn` + versehen (nur `--add-open` behandelt `(exit)`-Stellen explizit, dort aber nur + mit `dbgclose`, nicht mit einem geloggten Rueckgabewert). +- **Kein "--remove".** Rueckbau erfolgt manuell oder per `git checkout -- `. +- **`--recursive` kann viele Dateien anfassen** - projekteigene Helfer wie + `ssg-start`/`ssg-val`/`ssg-attrib-merge` werden mitinstrumentiert, sobald sie im + Aufrufgraphen auftauchen. `--max-depth` und `--exclude` begrenzen das gezielt; + ohne `--dry-run`-Kontrolle vorher nicht blind auf grosse Funktionen mit hoher + `--max-depth` loslassen. +- **Mehrfache Definitionen desselben Namens** in unterschiedlichen Dateien: es + gewinnt der erste beim Durchsuchen gefundene (Dateireihenfolge nicht garantiert + sortiert-stabil ueber Verzeichnisgrenzen) - in dieser Codebasis kommt das + ueblicherweise nicht vor. + +## Idempotenz + +Vor dem Einfuegen prueft das Werkzeug, ob die erste Rumpf-Form bereits `(dbgf ...)` +(fuer `--method`) bzw. `(dbgopen ...)` (fuer `--add-open`) ist, und ueberspringt die +Funktion in dem Fall mit einer `[skip]`-Meldung. Mit `--force` wird trotzdem erneut +eingefuegt (fuehrt bei wiederholtem `--force`-Aufruf zu doppelten `dbgf`/`dbg`-Zeilen - +in dem Fall vorher besser die Datei zuruecksetzen). diff --git a/doc/tools.md b/doc/tools.md index 519e38e..92efe5d 100644 --- a/doc/tools.md +++ b/doc/tools.md @@ -35,18 +35,6 @@ Diese Werkzeuge ermöglichen die massenhafte Aufbereitung von einzelnen dxf Date - [--number SIVASNR](#--number-sivasnr-1) - [Uebersicht K-Zuordnung](#uebersicht-k-zuordnung) - [Umgebungsvariablen](#umgebungsvariablen-1) -- [set\_attributs](#set_attributs) - - [Aufruf](#aufruf-2) - - [Datenquellen](#datenquellen-2) - - [Ablauf](#ablauf-2) - - [Konfiguration (attradd.cfg)](#konfiguration-attraddcfg) - - [Attribute Omniflo Kurve (Bogen)](#attribute-omniflo-kurve-bogen) - - [Attribute Omniflo Weiche](#attribute-omniflo-weiche) - - [CSV-Export-Felder (Merkmale)](#csv-export-felder-merkmale) - - [Schalter](#schalter-2) - - [--number SIVASNR / -n SIVASNR](#--number-sivasnr---n-sivasnr) - - [--dry-run / -d](#--dry-run---d) - - [Umgebungsvariablen](#umgebungsvariablen-2) # set_einfuegepunkt @@ -274,153 +262,3 @@ Nur die angegebene 9-stellige Sivasnr verarbeiten. Kombinierbar mit jedem Schalt |---|---| | `DXFM_DATA` | Pfad zu `data/` (JSON + DXF Quelldateien) | | `DXFM_RESULTS` | Pfad zu `results/` (Ausgabe) | - ---- - -# set_attributs - -Fuegt Attribute (ATTDEF) zu allen Omniflo DXF-Dateien hinzu und verpackt die -gesamte Geometrie in eine benannte Blockdefinition. Die Attribut-Tags und deren -Werte werden ueber eine Konfigurationsdatei gesteuert. - -**Voraussetzung:** Die Quelldateien in `data/omniflo/` sollten zuvor durch -`set_einfuegepkt.py` verarbeitet worden sein, damit `$INSBASE` korrekt gesetzt ist. - -## Aufruf - -``` -python lib/set_attributs.py [--number SIVASNR] [--dry-run] -``` - -## Datenquellen - -| Pfad | Inhalt | -|---|---| -| `cfg/attradd.cfg` | Attribut-Konfiguration (Tags und Quellfelder je Elementtyp) | -| `data/json/omniflo_boegen.json` | Liste aller Boegen mit Metadaten | -| `data/json/omniflo_weichen.json` | Liste aller Weichen mit Metadaten | -| `data/omniflo/*.dxf` | DXF-Quelldateien (Basis: Ausgabe von `set_einfuegepkt.py`) | - -## Ablauf - -1. Konfiguration aus `cfg/attradd.cfg` lesen -2. JSON-Daten laden und Lookup-Tabelle (Sivasnr -> Typ + Eintrag) erstellen -3. Fuer jede Sivasnr die DXF-Datei oeffnen, bestehende ATTDEFs entfernen -4. Neue ATTDEFs gemaess Config einfuegen (Layer `ATTRIB`, unterhalb der Geometrie) -5. Alle Entities (Geometrie + ATTDEFs) in eine Blockdefinition verschieben: - - Blockname = Sivasnummer (identisch mit dem Dateinamen ohne `.dxf`) - - Basispunkt der Blockdefinition = `$INSBASE` aus dem DXF-Header -6. Im Modelspace einen INSERT auf den neuen Block einfuegen, mit ATTRIB-Entities - befuellt aus den ATTDEF-Standardwerten (ermoeglicht Attributanzeige per Klick in BricsCAD) -7. Ergebnis nach `results/omniflo/` speichern - -## Ausgabestruktur (DXF) - -``` -BLOCKS - BLOCK "" (Basispunkt = $INSBASE) - - ATTDEF BESCHR = "..." - ATTDEF ARTINR = "..." - ... - ENDBLK - -ENTITIES (Modelspace) - INSERT "" at (0, 0) - ATTRIB BESCHR = "..." - ATTRIB ARTINR = "..." - ... - SEQEND -``` - -Der INSERT im Modelspace traegt die konkreten Attributwerte und ist in BricsCAD -per Klick editierbar. Die Blockdefinition mit ATTDEFs dient als Vorlage fuer -den Block-Import via `import_element_as_block` (omniflo_utils.py). - -## Konfiguration (attradd.cfg) - -Die Config-Datei ist im INI-Format mit Abschnitten je Elementtyp. -Quellwerte koennen sein: -- **JSON-Feldname** (z.B. `ProfilTyp`) - Wert wird aus dem JSON-Eintrag gelesen -- **Fester Text** in Anfuehrungszeichen (z.B. `"Omniflo Kurve"`) -- **Platzhalter** `{sivasnr}` - wird durch die Sivasnr ersetzt - -### Attribute Omniflo Kurve (Bogen) - -| ATTDEF-Tag | Quelle | Beschreibung | -| --- | --- | --- | -| `BESCHR` | `ProfilTyp` | Profilbezeichnung aus JSON | -| `ARTINR` | `Sivasnr` | Sivas-Artikelnummer | -| `TEILEART` | `"Omniflo Kurve"` | Fester Typname | -| `RADIUS` | `Radius` | Kurvenradius in mm | -| `WINKEL` | `KurvenWinkel` | Kurvenwinkel in Grad | -| `SIVASNR_TEF` | `SivasnrTEF` | Sivas-Nr. des TEF-Antriebs (leer = kein Antrieb) | -| `LAYER` | *(aus [layer])* | Layer-Name gemaess Kategorisierung | - -### Attribute Omniflo Weiche - -| ATTDEF-Tag | Quelle | Beschreibung | -| --- | --- | --- | -| `BESCHR` | `ProfilTyp` | Profilbezeichnung aus JSON | -| `ARTINR` | `Sivasnr` | Sivas-Artikelnummer | -| `TEILEART` | `"Omniflo Weiche"` | Fester Typname | -| `WEICHENTYP` | `WeichenTyp` | Weichentyp (Einzelweiche, Doppelweiche, Dreiwegeweiche, ...) | -| `WINKEL` | `KurvenWinkel` | Kurvenwinkel in Grad | -| `RICHTUNG` | `KurvenRichtung` | Richtungskennung (1=Links, 2=Rechts, 3=Beide, 7=Dreiweg) | -| `SIVASNR_TEF` | `SivasnrTEF` | Sivas-Nr. des TEF-Antriebs (leer = kein Antrieb) | -| `LAYER` | *(aus [layer])* | Layer-Name gemaess Kategorisierung | - -### CSV-Export-Felder (Merkmale) - -Die Attribute werden beim CSV-Export (`export_csv.py`, aufgerufen ueber `c:EXPORTCSV`) wie folgt in CSV-Merkmale ueberfuehrt. -CSV-Format: `Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale` - -**Omniflo Kurve:** - -| Merkmal | Quelle | Typ | -| --- | --- | --- | -| `Kurvenwinkel` | JSON `KurvenWinkel` | float | -| `Radius` | JSON `Radius` | float | -| `Höhe` | Attribut `HOEHE` / Z-Koordinate | string | -| `Drehung` | Attribut `DREHUNG` / CAD-Rotation | float | -| `SivasNummer` | JSON `Sivasnr` | string | - -**Omniflo Weiche:** - -| Merkmal | Quelle | Typ | -| --- | --- | --- | -| `Weichentyp` | JSON `WeichenTyp` | string | -| `Richtung` | JSON `KurvenRichtung` | string | -| `Weichenwinkel` | JSON `KurvenWinkel` | float | -| `Höhe` | Attribut `HOEHE` / Z-Koordinate | string | -| `Drehung` | Attribut `DREHUNG` / CAD-Rotation | float | -| `Antrieb Kurve` | `SivasnrTEF` != null | bool | -| `SivasNummer` | JSON `Sivasnr` | string | - - -Höhe und Drehung werden beim Export selbst aus den Blockinformationen entnommen und dann ins json geschrieben. - -| ATTDEF-Tag | Quelle | Beschreibung | -| --- | --- | --- | -| `HOEHE` | `"2000"` | Montagehoehe in mm (wird beim Export aus Z-Koordinate aktualisiert) | -| `DREHUNG` | `"0"` | Drehwinkel in Grad (wird beim Export aus CAD-Rotation aktualisiert) | - -## Schalter - -### --number SIVASNR / -n SIVASNR - -Nur die angegebene Sivasnr verarbeiten. - -### --dry-run / -d - -Nur anzeigen welche Attribute gesetzt wuerden, ohne DXF-Dateien zu schreiben. - -## Umgebungsvariablen - -| Variable | Verwendung | -|---|---| -| `DXFM_DATA` | Pfad zu `data/` (JSON + DXF Quelldateien) | -| `DXFM_CFG` | Pfad zu `cfg/` (attradd.cfg) | -| `DXFM_RESULTS` | Pfad zu `results/` (Ausgabe nach `results/omniflo/`) | - -Alle drei haben Fallback auf relative Pfade vom Projektverzeichnis. diff --git a/lib/dbg2lsp.py b/lib/dbg2lsp.py new file mode 100644 index 0000000..cc21d74 --- /dev/null +++ b/lib/dbg2lsp.py @@ -0,0 +1,725 @@ +""" +Instrumentiert AutoLISP-Funktionen (.lsp) automatisch mit den ssg_dbg.lsp- +Routinen (dbgf, dbg, dbgreturn, dbgopen, dbgclose) - so wie es beim manuellen +Debuggen (siehe z.B. Lisp/KreiselInsert.lsp::ils-eckrad-insert) bereits +gemacht wird. + +Fuer eine gegebene Funktion (--method) wird eingefuegt: + - (dbgf "funktionsname") direkt als erste Form im Rumpf + - (dbg 'parameter) je Eingabeparameter (die Locals nach "/" NICHT) + - (dbgreturn ) die letzte Form des Rumpfs wird + eingeklammert, damit ihr Rueckgabe- + wert mitgeloggt wird + +Mit --recursive wird zusaetzlich jede Funktion instrumentiert, die im Rumpf +(auch verschachtelt, aber NICHT innerhalb von 'quotierten Listen) aufgerufen +wird - vorausgesetzt, ihr (defun ...) laesst sich in einem der --search-dir- +Verzeichnisse finden (Bibliotheks-/AutoLISP-Builtins wie princ, command, +strcat etc. haben dort kein defun und werden dadurch automatisch NICHT +angefasst). Der Aufrufgraph wird rekursiv weiterverfolgt (mit --max-depth +begrenzt), einmal besuchte Funktionen werden nicht doppelt instrumentiert. + +Mit --add-open DATEI METHODE wird in der angegebenen Methode zusaetzlich + - (dbgopen "" "") als erste Rumpf-Form + - (dbgclose) vor jedem (exit)-Aufruf im Rumpf + UND am Ende des Rumpfs (falls die + letzte Form nicht bereits (exit) ist) +eingefuegt - fuer den Einstiegspunkt (typischerweise ein c:BEFEHL-Kommando), +der die .dbg-Datei fuer einen ganzen Testlauf oeffnen/schliessen soll. +--add-open ist unabhaengig von --method/--recursive nutzbar und mehrfach +angebbar. + +Aufruf-Beispiele: + + python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-separator-one + + python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-kreisel --recursive + + python dbg2lsp.py tests/test_mubea.lsp --add-open tests/test_mubea.lsp c:TEST_MUBEA + + python dbg2lsp.py tests/test_mubea.lsp --method mubea:build-kreisel --recursive \\ + --add-open tests/test_mubea.lsp c:TEST_MUBEA --dry-run + +Details, Grenzen und weitere Beispiele: siehe doc/dbg2lsp.md +""" + +import argparse +import difflib +import os +import sys + +# --------------------------------------------------------------------------- +# ssg_dbg.lsp-Funktionen, die NIE instrumentiert oder in den Aufrufgraphen +# aufgenommen werden duerfen - sonst wuerde --recursive versuchen, dbgf +# selbst mit dbgf zu instrumentieren, sobald es auf eine bereits (manuell +# oder durch einen frueheren Lauf) instrumentierte Funktion trifft. +# --------------------------------------------------------------------------- +DBG_FRAMEWORK_NAMES = { + "dbgf", "dbg", "dbgp", "dbgmsg", "dbgreturn", "dbgopen", "dbgclose", + "dbgflush", "dbgon", "dbgoff", + "dbg-flush", "dbg-bufwrite", "dbg-tabs", "dbg-tostring", "dbg-list-tostring", +} + +DEFAULT_DBG_ENVVAR = "DXFM_LOG" + + +# --------------------------------------------------------------------------- +# Zeichenweises Scannen mit String-/Kommentar-Bewusstsein (identische Logik +# wie beim manuellen Klammer-Balance-Check waehrend der Mubea-Instrumentierung) +# --------------------------------------------------------------------------- + +class LispSyntaxError(Exception): + pass + + +def _skip_string(text, i): + """i zeigt auf das oeffnende '\"'. Gibt den Index NACH dem schliessenden '\"' zurueck.""" + n = len(text) + i += 1 + while i < n: + c = text[i] + if c == "\\": + i += 2 + continue + if c == '"': + return i + 1 + i += 1 + raise LispSyntaxError("Nicht geschlossener String ab Position %d" % i) + + +def _skip_line_comment(text, i): + nl = text.find("\n", i) + return len(text) if nl == -1 else nl + 1 + + +def find_matching_paren(text, open_idx): + """open_idx zeigt auf '('. Gibt den Index der zugehoerigen ')' zurueck.""" + n = len(text) + if text[open_idx] != "(": + raise LispSyntaxError("Position %d ist keine '('" % open_idx) + depth = 0 + i = open_idx + while i < n: + c = text[i] + if c == '"': + i = _skip_string(text, i) + continue + if c == ";": + i = _skip_line_comment(text, i) + continue + if c == "(": + depth += 1 + elif c == ")": + depth -= 1 + if depth == 0: + return i + i += 1 + raise LispSyntaxError("Keine schliessende Klammer fuer '(' ab Position %d gefunden" % open_idx) + + +def _skip_ws_and_comments(text, i): + n = len(text) + while i < n: + c = text[i] + if c in " \t\r\n": + i += 1 + continue + if c == ";": + i = _skip_line_comment(text, i) + continue + break + return i + + +_TOKEN_STOP_CHARS = set(" \t\r\n()\";'") + + +def read_next_token(text, i, limit): + """Liest ab i (nach Whitespace/Kommentaren) das naechste Token (Symbol/Zahl) + bis zum naechsten Trennzeichen. Gibt (token, end_index) oder (None, i) zurueck, + wenn an Position i eine '(' / '"' / EOF folgt (kein einfaches Token).""" + i = _skip_ws_and_comments(text, i) + if i >= limit: + return None, i + if text[i] in "(\"'": + return None, i + start = i + while i < limit and text[i] not in _TOKEN_STOP_CHARS: + i += 1 + if i == start: + return None, i + return text[start:i], i + + +def iter_top_level_spans(text, start, end): + """Liefert (form_start, form_end) fuer jede balancierte Top-Level-Form + zwischen start und end (end exklusiv, typischerweise die Position der + schliessenden ')' des umgebenden defun). Reine Atome (Symbole/Zahlen ohne + umschliessende Klammer) werden ebenfalls als Form erkannt.""" + spans = [] + i = start + while i < end: + i = _skip_ws_and_comments(text, i) + if i >= end: + break + c = text[i] + if c == "(": + close = find_matching_paren(text, i) + spans.append((i, close + 1)) + i = close + 1 + elif c == "'": + # Quotiertes Atom/Liste - als eine Form ab dem Apostroph behandeln. + j = i + 1 + if j < end and text[j] == "(": + close = find_matching_paren(text, j) + spans.append((i, close + 1)) + i = close + 1 + else: + tok, tend = read_next_token(text, j, end) + spans.append((i, tend)) + i = tend + elif c == '"': + send = _skip_string(text, i) + spans.append((i, send)) + i = send + else: + tok, tend = read_next_token(text, i, end) + if tok is None: + i += 1 + else: + spans.append((i, tend)) + i = tend + return spans + + +# --------------------------------------------------------------------------- +# (defun ...) - Erkennung +# --------------------------------------------------------------------------- + +class DefunInfo: + def __init__(self, name, path, defun_start, defun_end, + arglist_start, arglist_end, body_start, body_end, + params, locals_): + self.name = name + self.path = path + self.defun_start = defun_start + self.defun_end = defun_end # Position NACH der schliessenden ')' des defun + self.arglist_start = arglist_start + self.arglist_end = arglist_end + self.body_start = body_start + self.body_end = body_end # Position der schliessenden ')' des defun (exklusiv) + self.params = params + self.locals = locals_ + + def __repr__(self): + return "DefunInfo(%r in %s)" % (self.name, self.path) + + +def _parse_arglist(text, arglist_start, arglist_end): + """arglist_start zeigt auf '(' der Parameterliste, arglist_end auf die + zugehoerige ')'. Gibt (params, locals) zurueck - locals ist alles nach + einem "/"-Trenner (leer, falls keiner vorhanden).""" + params = [] + locals_ = [] + seen_slash = False + i = arglist_start + 1 + while i < arglist_end: + tok, tend = read_next_token(text, i, arglist_end) + if tok is None: + # z.B. eine verschachtelte Liste als Defaultwert (in AutoLISP + # eigentlich nicht ueblich) - ueberspringen statt abzubrechen. + i = _skip_ws_and_comments(text, i) + if i < arglist_end and text[i] == "(": + i = find_matching_paren(text, i) + 1 + continue + i += 1 + continue + if tok == "/": + seen_slash = True + elif seen_slash: + locals_.append(tok) + else: + params.append(tok) + i = tend + return params, locals_ + + +def find_defuns_in_text(text, path): + """Findet alle (defun NAME (...) ...) Top-Level-Definitionen im Text.""" + defuns = [] + i = 0 + n = len(text) + while i < n: + c = text[i] + if c == '"': + i = _skip_string(text, i) + continue + if c == ";": + i = _skip_line_comment(text, i) + continue + if c == "(": + close = find_matching_paren(text, i) + head_tok, head_end = read_next_token(text, i + 1, close) + if head_tok is not None and head_tok.lower() == "defun": + name_tok, name_end = read_next_token(text, head_end, close) + if name_tok: + al_start = _skip_ws_and_comments(text, name_end) + if al_start < close and text[al_start] == "(": + al_end = find_matching_paren(text, al_start) + params, locals_ = _parse_arglist(text, al_start, al_end) + body_start = al_end + 1 + defuns.append(DefunInfo( + name=name_tok, path=path, + defun_start=i, defun_end=close + 1, + arglist_start=al_start, arglist_end=al_end, + body_start=body_start, body_end=close, + params=params, locals_=locals_, + )) + i = close + 1 + continue + i += 1 + return defuns + + +def build_defun_index(paths): + """path -> Dateitext (einmal eingelesen) und name.lower() -> DefunInfo + (erster Treffer gewinnt; mehrfach definierte Namen sind in dieser + Code-Basis nicht vorgesehen).""" + file_texts = {} + index = {} + for path in paths: + with open(path, "r", encoding="utf-8") as f: + text = f.read() + file_texts[path] = text + for d in find_defuns_in_text(text, path): + index.setdefault(d.name.lower(), d) + return file_texts, index + + +# --------------------------------------------------------------------------- +# Aufruf-Erkennung (fuer --recursive und --add-open/exit-Suche) +# --------------------------------------------------------------------------- + +def scan_calls(text, start, end): + """Liefert eine Liste von (head_name, call_start, call_end) fuer jede + NICHT quotierte Aufruf-Form (...) im Bereich [start, end) - beliebig + verschachtelt, aber nicht innerhalb von 'quotierten Listen (deren Inhalt + ist reine Datenliste, dort kann nichts "aufgerufen" werden).""" + calls = [] + + def walk(s, e): + i = s + while i < e: + c = text[i] + if c == '"': + i = _skip_string(text, i) + continue + if c == ";": + i = _skip_line_comment(text, i) + continue + if c == "'": + j = i + 1 + if j < e and text[j] == "(": + # quotierte Liste: reine Daten, nicht weiter absteigen + close = find_matching_paren(text, j) + i = close + 1 + continue + i = j + continue + if c == "(": + close = find_matching_paren(text, i) + head_tok, head_end = read_next_token(text, i + 1, close) + if head_tok and head_tok[0] not in "0123456789.\"'": + calls.append((head_tok, i, close + 1)) + walk(i + 1, close) + i = close + 1 + continue + i += 1 + + walk(start, end) + return calls + + +# --------------------------------------------------------------------------- +# Hilfsfunktionen fuer Einfuegetexte +# --------------------------------------------------------------------------- + +def _line_indent(text, pos): + """Fuehrender Whitespace der Zeile, in der pos liegt (fuer die Einrueckung + neu eingefuegter Debug-Zeilen).""" + nl = text.rfind("\n", 0, pos) + line_start = nl + 1 + i = line_start + while i < pos and text[i] in " \t": + i += 1 + indent = text[line_start:i] + return indent if indent else " " + + +def _body_indent(text, body_start, body_end): + """Einrueckung der ERSTEN echten Rumpf-Form. NICHT einfach _line_indent + auf body_start anwenden: bei mehrzeiligen Parameterlisten (z.B. + "(defun foo (a b /\n c d)" - "/"-Locals auf eigener Zeile) liegt + body_start mitten in der Fortsetzungszeile der Parameterliste, deren + Ausrichtung nichts mit der eigentlichen Rumpf-Einrueckung zu tun hat.""" + first = _skip_ws_and_comments(text, body_start) + if first >= body_end: + return " " + return _line_indent(text, first) + + +def _already_has_call(text, body_start, body_end, head_name): + spans = iter_top_level_spans(text, body_start, body_end) + if not spans: + return False + fs, fe = spans[0] + if text[fs] != "(": + return False + tok, _ = read_next_token(text, fs + 1, fe) + return bool(tok) and tok.lower() == head_name.lower() + + +# --------------------------------------------------------------------------- +# Edit-Sammlung: (position, prioritaet, text) - reine Einfuegungen, angewandt +# in absteigender Position (und bei Gleichstand nach Prioritaet), damit +# fruehere Positionen gueltig bleiben. +# --------------------------------------------------------------------------- + +class EditSet: + def __init__(self): + self.edits = {} # path -> list[(pos, prio, text)] + + def insert(self, path, pos, prio, text): + self.edits.setdefault(path, []).append((pos, prio, text)) + + def apply(self, file_texts): + """Gibt {path: neuer_text} fuer alle Dateien mit mindestens einer + Einfuegung zurueck.""" + result = {} + for path, items in self.edits.items(): + text = file_texts[path] + # Bei gleicher Position: kleinere Prioritaet zuerst im Ergebnis- + # text (prio dient als "vor/nach"-Reihenfolge an derselben Stelle). + items_sorted = sorted(items, key=lambda t: (t[0], t[1])) + out = [] + last = 0 + # Gruppieren nach Position, damit mehrere Einfuegungen an + # derselben Stelle in Prioritaets-Reihenfolge zusammenkommen. + grouped = {} + order = [] + for pos, prio, ins_text in items_sorted: + if pos not in grouped: + grouped[pos] = [] + order.append(pos) + grouped[pos].append(ins_text) + for pos in order: + out.append(text[last:pos]) + out.extend(grouped[pos]) + last = pos + out.append(text[last:]) + result[path] = "".join(out) + return result + + +# --------------------------------------------------------------------------- +# Instrumentierung +# --------------------------------------------------------------------------- + +def instrument_method(defun, file_texts, edits, force=False, log=print): + """Fuegt (dbgf ...), (dbg 'param) je Parameter und (dbgreturn ...) um die + letzte Rumpf-Form ein.""" + text = file_texts[defun.path] + + if not force and _already_has_call(text, defun.body_start, defun.body_end, "dbgf"): + log(" [skip] %s (%s) ist bereits instrumentiert (dbgf vorhanden) - --force zum Erzwingen" % + (defun.name, defun.path)) + return False + + indent = _body_indent(text, defun.body_start, defun.body_end) + lines = ['(dbgf "%s")' % defun.name] + for p in defun.params: + lines.append("(dbg '%s)" % p) + # Kein Trailing-Newline/-Indent: body_start hat im Original bereits sein + # eigenes "\n" vor der ersten Rumpf-Form, das bleibt unveraendert + # stehen und schliesst direkt an die letzte hier eingefuegte Zeile an. + insertion = "\n" + "\n".join(indent + l for l in lines) + edits.insert(defun.path, defun.body_start, 10, insertion) + + spans = iter_top_level_spans(text, defun.body_start, defun.body_end) + if spans: + last_start, last_end = spans[-1] + edits.insert(defun.path, last_start, 20, "(dbgreturn ") + edits.insert(defun.path, last_end, 21, ")") + else: + log(" [warn] %s (%s) hat einen leeren Rumpf - kein dbgreturn eingefuegt" % + (defun.name, defun.path)) + + log(" [ok] %s (%s): dbgf + %d x dbg + dbgreturn" % + (defun.name, defun.path, len(defun.params))) + return True + + +def instrument_add_open(defun, file_texts, edits, dbg_file, dbg_envvar, force=False, log=print): + """Fuegt (dbgopen "dbg_file" "dbg_envvar") als erste Rumpf-Form ein sowie + (dbgclose) vor jedem (exit)-Aufruf und (sofern die letzte Rumpf-Form nicht + selbst ein (exit) ist) am Ende des Rumpfs.""" + text = file_texts[defun.path] + + if not force and _already_has_call(text, defun.body_start, defun.body_end, "dbgopen"): + log(" [skip] %s (%s) hat bereits (dbgopen ...) - --force zum Erzwingen" % + (defun.name, defun.path)) + return False + + indent = _body_indent(text, defun.body_start, defun.body_end) + # Kein Trailing-Newline/-Indent (siehe instrument_method): die naechste + # Einfuegung an derselben Position (z.B. dbgf durch --method) oder das + # Original selbst liefert das fuehrende "\n" bereits mit. + open_line = "\n" + indent + '(dbgopen "%s" "%s")' % (dbg_file, dbg_envvar) + edits.insert(defun.path, defun.body_start, 5, open_line) + + calls = scan_calls(text, defun.body_start, defun.body_end) + exit_calls = [c for c in calls if c[0].lower() == "exit"] + for _, cstart, _cend in exit_calls: + cindent = _line_indent(text, cstart) + edits.insert(defun.path, cstart, 15, "(dbgclose)\n" + cindent) + + spans = iter_top_level_spans(text, defun.body_start, defun.body_end) + last_is_exit = False + if spans: + last_start, last_end = spans[-1] + tok, _ = read_next_token(text, last_start + 1, last_end) if text[last_start] == "(" else (None, 0) + last_is_exit = bool(tok) and tok.lower() == "exit" + if not last_is_exit: + edits.insert(defun.path, defun.body_end, 25, indent + "(dbgclose)\n") + + log(" [ok] %s (%s): dbgopen + dbgclose (%d exit-Stellen + Rumpfende)" % + (defun.name, defun.path, len(exit_calls))) + return True + + +# --------------------------------------------------------------------------- +# Rekursion ueber den Aufrufgraphen +# --------------------------------------------------------------------------- + +def instrument_recursive(start_defun, index, file_texts, edits, max_depth, exclude, force, log): + visited = set() + + def recurse(defun, depth): + key = defun.name.lower() + if key in visited: + return + visited.add(key) + + instrument_method(defun, file_texts, edits, force=force, log=log) + + if depth >= max_depth: + log(" [info] max-depth (%d) bei %s erreicht - keine weiteren Aufrufe verfolgt" % + (max_depth, defun.name)) + return + + text = file_texts[defun.path] + calls = scan_calls(text, defun.body_start, defun.body_end) + seen_here = set() + for head, _s, _e in calls: + head_l = head.lower() + if head_l in seen_here: + continue + seen_here.add(head_l) + if head_l in DBG_FRAMEWORK_NAMES or head_l in exclude: + continue + callee = index.get(head_l) + if callee is None or callee.name.lower() == key: + continue + recurse(callee, depth + 1) + + recurse(start_defun, 0) + + +# --------------------------------------------------------------------------- +# Pfad-/Umgebungshilfen +# --------------------------------------------------------------------------- + +def find_repo_root(start_path): + cur = os.path.abspath(start_path) + if os.path.isfile(cur): + cur = os.path.dirname(cur) + while True: + if os.path.isdir(os.path.join(cur, ".git")) or os.path.isdir(os.path.join(cur, "Lisp")): + return cur + parent = os.path.dirname(cur) + if parent == cur: + return None + cur = parent + + +def resolve_file(path, repo_root): + if os.path.isfile(path): + return os.path.abspath(path) + if repo_root: + candidate = os.path.join(repo_root, path) + if os.path.isfile(candidate): + return os.path.abspath(candidate) + return None + + +def collect_lsp_files(dirs): + paths = [] + seen = set() + for d in dirs: + if not os.path.isdir(d): + continue + for root, _dirnames, filenames in os.walk(d): + for fn in filenames: + if fn.lower().endswith(".lsp"): + full = os.path.abspath(os.path.join(root, fn)) + if full not in seen: + seen.add(full) + paths.append(full) + return paths + + +def dbg_file_name_for(method_name): + """Leitet einen .dbg-Dateinamen aus einem Methodennamen ab, z.B. + "c:TEST_MUBEA" -> "test_mubea.dbg", "mubea:build-kreisel" -> "build-kreisel.dbg".""" + base = method_name.split(":")[-1] + return base.lower() + ".dbg" + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main(argv=None): + parser = argparse.ArgumentParser( + prog="dbg2lsp.py", + description="Instrumentiert AutoLISP-Funktionen mit ssg_dbg.lsp (dbgf/dbg/dbgreturn/dbgopen/dbgclose).", + ) + parser.add_argument("file", help="Anker-.lsp-Datei (Verzeichnis wird immer mit durchsucht)") + parser.add_argument("--method", action="append", default=[], + metavar="NAME", + help="Funktionsname, der mit dbgf/dbg/dbgreturn instrumentiert wird. Mehrfach angebbar.") + parser.add_argument("--recursive", action="store_true", + help="Auch alle im Rumpf aufgerufenen projekteigenen Funktionen instrumentieren (rekursiv)") + parser.add_argument("--search-dir", action="append", default=[], + metavar="DIR", + help="Weiteres Verzeichnis nach .lsp-Dateien durchsuchen (fuer --recursive). " + "Default: Lisp/ und das Verzeichnis von FILE.") + parser.add_argument("--max-depth", type=int, default=3, + help="Maximale Rekursionstiefe fuer --recursive (Default: 3)") + parser.add_argument("--exclude", action="append", default=[], + metavar="NAME", + help="Funktionsname von --recursive ausschliessen (mehrfach angebbar)") + parser.add_argument("--add-open", nargs=2, action="append", default=[], + metavar=("DATEI", "METHODE"), + help="(dbgopen ...)/(dbgclose) in METHODE (aus DATEI) einfuegen. Mehrfach angebbar.") + parser.add_argument("--dbg-file", default=None, + help="Dateiname fuer (dbgopen ...) (Default: aus dem --add-open-Methodennamen abgeleitet, z.B. test_mubea.dbg)") + parser.add_argument("--dbg-envvar", default=DEFAULT_DBG_ENVVAR, + help='Umgebungsvariable fuer (dbgopen ...) (Default: "%s")' % DEFAULT_DBG_ENVVAR) + parser.add_argument("--force", action="store_true", + help="Auch instrumentieren, wenn bereits (dbgf ...)/(dbgopen ...) vorhanden zu sein scheint") + parser.add_argument("--dry-run", action="store_true", + help="Nur ein Diff anzeigen, keine Dateien schreiben") + args = parser.parse_args(argv) + + repo_root = find_repo_root(args.file) or find_repo_root(".") + main_file = resolve_file(args.file, repo_root) + if main_file is None: + print("FEHLER: Datei nicht gefunden: %s" % args.file, file=sys.stderr) + return 1 + + search_dirs = [os.path.dirname(main_file)] + if repo_root: + search_dirs.append(os.path.join(repo_root, "Lisp")) + for d in args.search_dir: + resolved = d if os.path.isdir(d) else os.path.join(repo_root or ".", d) + search_dirs.append(resolved) + for f, _m in args.add_open: + resolved = resolve_file(f, repo_root) + if resolved: + search_dirs.append(os.path.dirname(resolved)) + + lsp_files = set(collect_lsp_files(search_dirs)) + lsp_files.add(main_file) + lsp_files = sorted(lsp_files) + + file_texts, index = build_defun_index(lsp_files) + + if not args.method and not args.add_open: + print("FEHLER: mindestens --method oder --add-open angeben.", file=sys.stderr) + return 1 + + edits = EditSet() + exclude = {n.lower() for n in args.exclude} + touched_any = False + + for method_name in args.method: + defun = index.get(method_name.lower()) + if defun is None: + print("FEHLER: Funktion '%s' nicht gefunden (durchsucht: %s)" % + (method_name, ", ".join(lsp_files)), file=sys.stderr) + continue + print("== --method %s ==" % method_name) + if args.recursive: + instrument_recursive(defun, index, file_texts, edits, + max_depth=args.max_depth, exclude=exclude, + force=args.force, log=print) + else: + instrument_method(defun, file_texts, edits, force=args.force, log=print) + touched_any = True + + for file_arg, method_name in args.add_open: + resolved = resolve_file(file_arg, repo_root) + if resolved is None: + print("FEHLER: Datei nicht gefunden: %s" % file_arg, file=sys.stderr) + continue + # Falls die Datei noch nicht im Index ist (z.B. ausserhalb der + # Search-Dirs), jetzt einlesen und mit aufnehmen. + if resolved not in file_texts: + with open(resolved, "r", encoding="utf-8") as f: + file_texts[resolved] = f.read() + for d in find_defuns_in_text(file_texts[resolved], resolved): + index.setdefault(d.name.lower(), d) + defun = index.get(method_name.lower()) + if defun is None or defun.path != resolved: + # ggf. gleichnamige Funktion aus anderer Datei ignorieren - + # explizit in der genannten Datei suchen. + local_defuns = find_defuns_in_text(file_texts[resolved], resolved) + defun = next((d for d in local_defuns if d.name.lower() == method_name.lower()), None) + if defun is None: + print("FEHLER: Funktion '%s' nicht in %s gefunden" % (method_name, file_arg), file=sys.stderr) + continue + print("== --add-open %s %s ==" % (file_arg, method_name)) + dbg_file = args.dbg_file or dbg_file_name_for(method_name) + instrument_add_open(defun, file_texts, edits, dbg_file=dbg_file, dbg_envvar=args.dbg_envvar, + force=args.force, log=print) + touched_any = True + + if not touched_any: + return 1 + + changed = edits.apply(file_texts) + if not changed: + print("Keine Aenderungen vorgenommen.") + return 0 + + for path, new_text in changed.items(): + old_text = file_texts[path] + if args.dry_run: + rel = os.path.relpath(path, repo_root) if repo_root else path + diff = difflib.unified_diff( + old_text.splitlines(keepends=True), + new_text.splitlines(keepends=True), + fromfile=rel, tofile=rel, + ) + sys.stdout.writelines(diff) + else: + with open(path, "w", encoding="utf-8", newline="") as f: + f.write(new_text) + print("Geschrieben: %s" % path) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())