[ADD] dbg2lsp.py: AutoLISP-Funktionen automatisch mit ssg_dbg instrumentieren

Neues Entwickler-Tool (bin/dbg2lsp.bat + lib/dbg2lsp.py), das das manuelle
Einfuegen von dbgf/dbg/dbgreturn/dbgopen/dbgclose automatisiert:
- --method NAME: dbgf + dbg je Parameter + dbgreturn um die letzte Rumpf-Form
- --recursive: verfolgt den Aufrufgraphen ueber Dateigrenzen hinweg (Builtins
  werden automatisch uebersprungen, da sie kein defun im Suchpfad haben)
- --add-open DATEI METHODE: dbgopen/dbgclose fuer einen Einstiegspunkt
  (z.B. ein c:BEFEHL-Kommando), inkl. aller (exit)-Stellen
Dokumentiert in doc/dbg2lsp.md, Kurzeintrag in CLAUDE.md.

Ausserdem: set_attributs.py aus doc/tools.md und CLAUDE.md entfernt - das
Skript existiert nicht mehr in lib/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 15:04:45 +02:00
parent 1d5d7c141e
commit d7bcfc3edb
5 changed files with 949 additions and 167 deletions
+9 -5
View File
@@ -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_raw.json> <data_dir> <output.csv>` |
| `export_csv.py` | Erzeugt einfache Item-Liste als CSV (ohne Summierung). Aufruf: `python export_csv.py <export_raw.json> <data_dir> <output.csv>` |
| `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
+9
View File
@@ -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" %*
+206
View File
@@ -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 <datei.lsp> [--method NAME ...] [--recursive] [--add-open DATEI METHODE] [Schalter]
```
Das Batch-Skript ruft `setenv.bat` auf und startet `lib/dbg2lsp.py` mit den
uebergebenen Argumenten. `<datei.lsp>` 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 <letzte Rumpf-Form>)
```
`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 "<dbg-datei>" "<envvar>")
```
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 -- <datei>` 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 -- <datei>`.
- **`--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).
-162
View File
@@ -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 "<sivasnr>" (Basispunkt = $INSBASE)
<Geometrie-Entities>
ATTDEF BESCHR = "..."
ATTDEF ARTINR = "..."
...
ENDBLK
ENTITIES (Modelspace)
INSERT "<sivasnr>" 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.
+725
View File
@@ -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 <letzte Rumpf-Form>) 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 "<dbg-datei>" "<envvar>") 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<indent>" 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<indent>" 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())