This commit is contained in:
2026-07-28 15:51:21 +02:00
55 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` | | `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_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_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_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>` | | `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_ | | `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) ### Typische Verarbeitungskette (Omniflo-DXF)
1. `set_attributs.py` - Attribute zu Quell-DXFs hinzufuegen 1. `set_einfuegepkt.py` - Einfuegepunkte berechnen und setzen
2. `set_einfuegepkt.py` - Einfuegepunkte berechnen und setzen 2. `set_koords.py` - Koordinatensysteme (K1-K4) setzen
3. `set_koords.py` - Koordinatensysteme (K1-K4) setzen 3. `export_sivas.py` / `export_csv.py` - Export aus Zeichnung
4. `export_sivas.py` / `export_csv.py` - Export aus Zeichnung
## Entwicklungsstand ## 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" %*
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+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) - [--number SIVASNR](#--number-sivasnr-1)
- [Uebersicht K-Zuordnung](#uebersicht-k-zuordnung) - [Uebersicht K-Zuordnung](#uebersicht-k-zuordnung)
- [Umgebungsvariablen](#umgebungsvariablen-1) - [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 # 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_DATA` | Pfad zu `data/` (JSON + DXF Quelldateien) |
| `DXFM_RESULTS` | Pfad zu `results/` (Ausgabe) | | `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())