[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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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())
|
||||
Reference in New Issue
Block a user