Compare commits

5 Commits

Author SHA1 Message Date
f64aeaf31c feat: Füge .gitignore hinzu, um Testskripte von der Versionskontrolle auszuschließen 2026-04-01 10:20:53 +02:00
c444a3f6ac fix: Verwende io.open() für bessere Encoding-Unterstützung in der get-Funktion 2026-04-01 10:03:31 +02:00
2ed4a107b9 fix: Encoding-Fallback und Ausgabeformat verbessert
- get(): Komplette Datei lesen statt nur 2048 Bytes, um UTF-8/ISO-8859-1 Fallback korrekt auszulösen
- Ausgabespalte für Tabellennamen von 40 auf 55 Zeichen erweitert
- Trennlinie auf 87 Zeichen angepasst (12+55+20)
- Ausgabeformat-Variable aus den Schleifen herausgezogen (einmalige Definition)
2026-04-01 08:29:46 +02:00
b2959bebdb Verbessere Fehlerbehandlung und Dateikodierung in der get-Funktion 2026-04-01 08:21:00 +02:00
6bccca2864 Dateien nach "/" hochladen 2026-04-01 08:03:32 +02:00
3 changed files with 331 additions and 256 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Testskripte werden nicht versioniert (enthalten ggf. sensible Produktionsskripte)
testskripte/

View File

@@ -0,0 +1,74 @@
# RepoVizChecker
Ein Python-Werkzeug zum Abgleich von **RepoViz-Annotationen** mit dem tatsächlich verwendeten SQL-Code in Kornshell-Skripten (`.ksh`).
---
## Beschreibung
KSH-Skripte enthalten häufig RepoViz-Metadaten in Form von Kommentar-Annotationen:
```ksh
#@modul: mein_skript.ksh
#@quelle: TABELLE_A, TABELLE_B
#@ziel: TABELLE_Z
```
`repovizcheck.py` liest diese Annotationen aus und vergleicht sie mit den Tabellennamen, die im Code über `$SCHEMA.<TABELLE>` bzw. `${SCHEMA}.<TABELLE>` tatsächlich referenziert werden.
---
## Features
- ✅ Liest `#@modul:`, `#@quelle:` und `#@ziel:` Annotationen aus dem KSH-Skript
- ✅ Erkennt verwendete Tabellen per Regex (`$SCHEMA.*` / `${SCHEMA}.*`)
- ✅ Vergleich **Repo → SQL**: Sind alle dokumentierten Tabellen im Code vorhanden?
- ✅ Vergleich **SQL → Repo**: Sind alle Code-Tabellen in den Annotationen dokumentiert?
- ✅ Prüft, ob der Skriptname in `#@modul:` eingetragen ist
- ✅ Encoding-Fallback: UTF-8 → ISO-8859-1
- ✅ Temporäre Tabellen (`TMP_*`) werden automatisch ignoriert
---
## Voraussetzungen
- Python 3.x
- Keine zusätzlichen Pakete erforderlich (nur Standardbibliothek)
---
## Aufruf
```powershell
python repovizcheck.py <skript.ksh>
```
### Beispiel
```powershell
python repovizcheck.py C:\projekte\etl_job.ksh
```
---
## Ausgabe (Übersicht)
| Abschnitt | Beschreibung |
|---|---|
| **RepoViz-Informationen** | Sortierte Ausgabe von Modul, Quell- und Zieltabellen |
| **Modul-Check** | Ist der Skriptname in `#@modul:` eingetragen? |
| **Repo → SQL** | Welche dokumentierten Tabellen sind im Code vorhanden / fehlen? |
| **SQL → Repo** | Welche Code-Tabellen sind dokumentiert / nicht dokumentiert? |
---
## Hinweise
- Es werden ausschließlich `.ksh`-Dateien akzeptiert (case-insensitive).
- Tabellennamen werden für alle Vergleiche in **Großbuchstaben** normalisiert.
---
## Lizenz
Internes Werkzeug Deutsche Telekom AG

View File

@@ -1,256 +1,255 @@
""" #!/usr/bin/env python
repovizcheck.py # -*- coding: utf-8 -*-
"""
Kurzbeschreibung: repovizcheck.py
Dieses Skript vergleicht RepoViz-Metadaten (Annotationen in Kornshell-Skripten: "#@modul:", "#@quelle:", "#@ziel:")
mit tatsächlich im Skript verwendeten Tabellen (Erkennung über $SCHEMA.<TABELLE> oder ${SCHEMA}<TABELLE>). Es Kurzbeschreibung:
ermöglicht einen schnellen Abgleich, welche Tabellen in RepoViz dokumentiert sind und welche im Code auftauchen. Dieses Skript vergleicht RepoViz-Metadaten (Annotationen in Kornshell-Skripten: "#@modul:", "#@quelle:", "#@ziel:")
mit tatsächlich im Skript verwendeten Tabellen (Erkennung über $SCHEMA.<TABELLE> oder ${SCHEMA}<TABELLE>). Es
Aufruf: ermöglicht einen schnellen Abgleich, welche Tabellen in RepoViz dokumentiert sind und welche im Code auftauchen.
python repovizcheck.py <skript.ksh>
Aufruf:
Wichtige Hinweise: python repovizcheck.py <skript.ksh>
- Akzeptierte Dateiendung: .ksh (case-insensitive).
- Kodierung: versucht zunächst UTF-8, bei Fehlern Fallback auf ISO-8859-1. Wichtige Hinweise:
- Tabellennamen werden für Vergleiche normalisiert (Uppercase); Regex-Parsing wurde robustifiziert. - Akzeptierte Dateiendung: .ksh (case-insensitive).
""" - Kodierung: versucht zunächst UTF-8, bei Fehlern Fallback auf ISO-8859-1.
- Tabellennamen werden für Vergleiche normalisiert (Uppercase); Regex-Parsing wurde robustifiziert.
from __future__ import print_function """
import sys
import re from __future__ import print_function
import os.path as p import sys
import argparse import re
import os.path as p
print(r" ____ __ ___ ____ _ _ ") import argparse
print(r"| _ \ ___ _ __ ___ \ \ / (_)____ / ___| |__ ___ ___| | _____ _ __ ") import io
print(r"| |_) / _ \ '_ \ / _ \ \ \ / /| |_ / | | | '_ \ / _ \/ __| |/ / _ \ '__|")
print(r"| _ < __/ |_) | (_) | \ V / | |/ / | |___| | | | __/ (__| < __/ | ") print(r" ____ __ ___ ____ _ _ ")
print(r"|_| \_\___| .__/ \___/ \_/ |_/___| \____|_| |_|\___|\___|_|\_\___|_| ") print(r"| _ \ ___ _ __ ___ \ \ / (_)____ / ___| |__ ___ ___| | _____ _ __ ")
print(r" |_| ") print(r"| |_) / _ \ '_ \ / _ \ \ \ / /| |_ / | | | '_ \ / _ \/ __| |/ / _ \ '__|")
print(r"| _ < __/ |_) | (_) | \ V / | |/ / | |___| | | | __/ (__| < __/ | ")
print(r"|_| \_\___| .__/ \___/ \_/ |_/___| \____|_| |_|\___|\___|_|\_\___|_| ")
such_quelle = "#@quelle:" # print(r" |_| ")
such_ziel = "#@ziel:" #
such_modul = "#@modul:" #
such_quelle = "#@quelle:" #
allezeilen = [] # Das Skript wird zeilenwiese in diese Liste geschrieben, eine Zeile gleich ein Element der Liste such_ziel = "#@ziel:" #
kommentarzeilen = [] # Ein Teil der nicht benoetigten Zeilen kommt in diese Liste such_modul = "#@modul:" #
codezeilen = [] # Zeilen mit Code kommen in diese Liste
# Liste der gueltigen Dateiendungen (case-insensitive) allezeilen = [] # Das Skript wird zeilenwiese in diese Liste geschrieben, eine Zeile gleich ein Element der Liste
perm_file_suffix = ['.ksh'] # Liste der gueltigen Dateiendungen kommentarzeilen = [] # Ein Teil der nicht benoetigten Zeilen kommt in diese Liste
codezeilen = [] # Zeilen mit Code kommen in diese Liste
# Verwende argparse für robusten CLI-Aufruf und Help # Liste der gueltigen Dateiendungen (case-insensitive)
parser = argparse.ArgumentParser(description='Check RepoViz annotations against SQL in a ksh script') perm_file_suffix = ['.ksh'] # Liste der gueltigen Dateiendungen
parser.add_argument('sourcefile', help='Kornshell script to check (must end with .ksh)')
args = parser.parse_args() # Verwende argparse für robusten CLI-Aufruf und Help
sourcefile = args.sourcefile parser = argparse.ArgumentParser(description='Check RepoViz annotations against SQL in a ksh script')
parser.add_argument('sourcefile', help='Kornshell script to check (must end with .ksh)')
# Dateiendung prüfen (case-insensitive) args = parser.parse_args()
file_suffix = p.splitext(sourcefile)[1].lower() sourcefile = args.sourcefile
if file_suffix not in perm_file_suffix:
print() # Gib eine leere Zeile aus # Dateiendung prüfen (case-insensitive)
print("Only Kornshell scripts can be processed.") # Gib diese Meldung aus file_suffix = p.splitext(sourcefile)[1].lower()
print() if file_suffix not in perm_file_suffix:
sys.exit(1) print() # Gib eine leere Zeile aus
print("Only Kornshell scripts can be processed.") # Gib diese Meldung aus
print()
# Funktion zum Oeffnen einer Datei mit Pruefung sys.exit(1)
def get(name):
'''Funktion zum Oeffnen einer Datei mit Pruefung (prüft Encoding)'''
# Versuche zuerst UTF-8, lese die gesamte Datei zur Validierung. # Funktion zum Oeffnen einer Datei mit Pruefung
try: def get(name):
f = open(name, "r", encoding='utf-8') '''Funktion zum Oeffnen einer Datei mit Pruefung (prüft Encoding)'''
f.read() # komplette Datei lesen, um Decode-Fehler sicher zu erkennen # io.open() wird verwendet, da es in Python 2 und 3 den encoding-Parameter unterstuetzt.
f.seek(0) # Versuche zuerst UTF-8, lese die gesamte Datei zur Validierung.
return f try:
except UnicodeDecodeError: f = io.open(name, "r", encoding='utf-8')
# UTF-8 passt nicht — versuche ISO-8859-1 f.read() # komplette Datei lesen, um Decode-Fehler sicher zu erkennen
try: f.seek(0)
f.close() return f
return open(name, "r", encoding='ISO-8859-1') except UnicodeDecodeError:
except IOError: # UTF-8 passt nicht — versuche ISO-8859-1
print("") try:
print("The File", name, "can't be opened with fallback encoding!") f.close()
print("") return io.open(name, "r", encoding='ISO-8859-1')
sys.exit(1) except IOError:
except IOError: print("")
print("") print("The File", name, "can't be opened with fallback encoding!")
print("The File", name, "doesn't exist or can't be opened!") print("")
print("") sys.exit(1)
sys.exit(1) except IOError:
except Exception: print("")
# unerwarteter Fehler beim Lesen print("The File", name, "doesn't exist or can't be opened!")
try: print("")
f.close() sys.exit(1)
except Exception: except Exception:
pass # unerwarteter Fehler beim Lesen
print("") try:
print("Error reading the file", name) f.close()
print("") except Exception:
sys.exit(1) pass
return None print("")
print("Error reading the file", name)
print("")
# Funktion zum Erstellen einer Tabellenliste aus den RepoViz Informationen sys.exit(1)
def erstelle_liste(datei, typ): # 2 Parameter return None
'''Erstelle eine Liste aus der übergebenen Datei für den Typ der übergeben wurde'''
laenge = len(typ)
tabellenliste = [] # Funktion zum Erstellen einer Tabellenliste aus den RepoViz Informationen
fobj_in = get(datei) def erstelle_liste(datei, typ): # 2 Parameter
for line in fobj_in: '''Erstelle eine Liste aus der übergebenen Datei für den Typ der übergeben wurde'''
# entferne nur führende Leerzeichen vor der Prüfung laenge = len(typ)
content = line.lstrip() tabellenliste = []
if content.startswith(typ): fobj_in = get(datei)
zeile = content[len(typ):].strip() for line in fobj_in:
# entferne ein mögliches abschließendes Komma # entferne nur führende Leerzeichen vor der Prüfung
if zeile.endswith(','): content = line.lstrip()
zeile = zeile[:-1] if content.startswith(typ):
liste = [it.strip() for it in zeile.split(',') if it.strip()] zeile = content[len(typ):].strip()
for item in liste: # entferne ein mögliches abschließendes Komma
# normalisiere auf Großbuchstaben für spätere Vergleiche if zeile.endswith(','):
tabellenliste.append(item.strip().upper()) zeile = zeile[:-1]
fobj_in.close() liste = [it.strip() for it in zeile.split(',') if it.strip()]
return tabellenliste for item in liste:
# normalisiere auf Großbuchstaben für spätere Vergleiche
tabellenliste.append(item.strip().upper())
def trennzeile(typ): # Funktion zum Ausgeben einer 80 Zeichen breiten Trennzeile. fobj_in.close()
''' return tabellenliste
Erstellt eine 80 zeichenbreite Zeile mit dem übergebenen Zeichen
'''
print(typ * 80) # Das Trennzeichen ist variabel und wird der Funktion als Parameter uebergeben. def trennzeile(typ): # Funktion zum Ausgeben einer Trennzeile.
'''
Erstellt eine 87 zeichenbreite Zeile mit dem übergebenen Zeichen
modulliste = erstelle_liste(sourcefile, such_modul) # Erstelle Liste mit den Modulen '''
quelleliste = erstelle_liste(sourcefile, such_quelle) # Erstelle Liste mit den Quellen print(typ * 87) # Das Trennzeichen ist variabel und wird der Funktion als Parameter uebergeben.
zielliste = erstelle_liste(sourcefile, such_ziel) # Erstelle Liste mit den Zielen
datei = get(sourcefile) # Oeffnen der Datei mit einer Funktion, die auch prueft ob die Datei existiert modulliste = erstelle_liste(sourcefile, such_modul) # Erstelle Liste mit den Modulen
allezeilen = datei.readlines() # Lesen aller Zeilen in eine Liste quelleliste = erstelle_liste(sourcefile, such_quelle) # Erstelle Liste mit den Quellen
datei.close() zielliste = erstelle_liste(sourcefile, such_ziel) # Erstelle Liste mit den Zielen
# Versuch die Kommentarzeilen loszuwerden datei = get(sourcefile) # Oeffnen der Datei mit einer Funktion, die auch prueft ob die Datei existiert
for zeile in allezeilen: allezeilen = datei.readlines() # Lesen aller Zeilen in eine Liste
s = zeile.strip() datei.close()
if not s:
# leere Zeilen überspringen # Versuch die Kommentarzeilen loszuwerden
continue for zeile in allezeilen:
if s.startswith('#@modul:'): s = zeile.strip()
codezeilen.append(s) if not s:
continue # leere Zeilen überspringen
# Gruppiere typische Kommentar-/Meta-Zeilen (case-insensitive) continue
up = s.upper() if s.startswith('#@modul:'):
if s.startswith('#') or s.startswith('--') or up.startswith('J000') or up.startswith('SQLFILE') \ codezeilen.append(s)
or up.startswith('FMELDUNG') or up.startswith('ALTER') or up.startswith('F_TRUNCATE') \ continue
or up == 'FI' or up == 'IF' or up.startswith('F_RUNSTATS'): # Gruppiere typische Kommentar-/Meta-Zeilen (case-insensitive)
kommentarzeilen.append(s) up = s.upper()
else: if s.startswith('#') or s.startswith('--') or up.startswith('J000') or up.startswith('SQLFILE') \
codezeilen.append(s) or up.startswith('FMELDUNG') or up.startswith('ALTER') or up.startswith('F_TRUNCATE') \
or up == 'FI' or up == 'IF' or up.startswith('F_RUNSTATS'):
kommentarzeilen.append(s)
trennzeile("+") else:
print("Sorted output of RepoViz information.") codezeilen.append(s)
print("")
ausgabe = "{:10}{:}" # Definition AusgabeFormat 1.Feld begrenzt auf 10 Zeichen, das 2.Feld unbegrenzt
quelleliste.sort() # Sortiere Liste <quelleliste> trennzeile("+")
zielliste.sort() # Sortiere Liste <zielliste> print("Sorted output of RepoViz information.")
print(ausgabe.format(such_modul, modulliste)) # Ausgabe <modulliste> print("")
print(ausgabe.format(such_quelle, quelleliste)) # Ausgabe <quelleliste> ausgabe = "{:10}{:}" # Definition AusgabeFormat 1.Feld begrenzt auf 10 Zeichen, das 2.Feld unbegrenzt
print(ausgabe.format(such_ziel, zielliste)) # Ausgabe <zielliste> quelleliste.sort() # Sortiere Liste <quelleliste>
zielliste.sort() # Sortiere Liste <zielliste>
# Vergleiche sourcefile mit modulliste hier einfuegen print(ausgabe.format(such_modul, modulliste)) # Ausgabe <modulliste>
# Eventuell anpassen, es steht nur der Skriptname in den RepoVizInfos print(ausgabe.format(such_quelle, quelleliste)) # Ausgabe <quelleliste>
trennzeile("~") print(ausgabe.format(such_ziel, zielliste)) # Ausgabe <zielliste>
trennzeile("")
print("Comparison of given script name with RepoVizInformation") # Vergleiche sourcefile mit modulliste hier einfuegen
# Vergleiche nur einmal, normalisiere auf Großbuchstaben # Eventuell anpassen, es steht nur der Skriptname in den RepoVizInfos
script_basename = p.basename(sourcefile).upper() trennzeile("~")
if script_basename in [m.strip().upper() for m in modulliste]: trennzeile("")
print("Script", p.basename(sourcefile), "is in the RepoViz information!") print("Comparison of given script name with RepoVizInformation")
else: # Vergleiche nur einmal, normalisiere auf Großbuchstaben
print("Script", p.basename(sourcefile), "is missing in the RepoViz information!") script_basename = p.basename(sourcefile).upper()
if script_basename in [m.strip().upper() for m in modulliste]:
# Suche Objekte anhand der RepoVizInformationen print("Script", p.basename(sourcefile), "is in the RepoViz information!")
trennzeile("~") else:
print("Script", p.basename(sourcefile), "is missing in the RepoViz information!")
print("")
print("Create a list of tables from the existing code: list <allezeilen>") # Suche Objekte anhand der RepoVizInformationen
# Der Code steht schon in der Liste <codezeilen> trennzeile("~")
neue_liste = []
# verbessertes Regex-Parsing: capture-Gruppe für Tabellennamen, compile einmal print("")
regex = re.compile(r"(?:\$SCHEMA\.|\${SCHEMA}\.)\s*([A-Z0-9_]+)", re.IGNORECASE) print("Create a list of tables from the existing code: list <allezeilen>")
# nutze Set für eindeutige Sammlung # Der Code steht schon in der Liste <codezeilen>
neue_set = set() neue_liste = []
for line in allezeilen: # verbessertes Regex-Parsing: capture-Gruppe für Tabellennamen, compile einmal
for match in regex.findall(line): regex = re.compile(r"(?:\$SCHEMA\.|\${SCHEMA}\.)\s*([A-Z0-9_]+)", re.IGNORECASE)
tabname = match.strip() # nutze Set für eindeutige Sammlung
if not tabname: neue_set = set()
continue for line in allezeilen:
# ignoriere Variablen oder temporäre Tabellen for match in regex.findall(line):
if tabname.startswith('$'): tabname = match.strip()
continue if not tabname:
tabname_norm = re.sub(r"[^A-Z0-9_]", "", tabname.upper()) continue
if tabname_norm and not tabname_norm.startswith('TMP_'): # ignoriere Variablen oder temporäre Tabellen
neue_set.add(tabname_norm) if tabname.startswith('$'):
neue_liste = sorted(neue_set) continue
tabname_norm = re.sub(r"[^A-Z0-9_]", "", tabname.upper())
print("The list contains:", len(neue_liste), "entries") if tabname_norm and not tabname_norm.startswith('TMP_'):
trennzeile("~") neue_set.add(tabname_norm)
neue_liste = sorted(neue_set)
# Vergleich repo -> SQL print("The list contains:", len(neue_liste), "entries")
# 2 Teile trennzeile("~")
# Quelle - quelleliste
# nehme Liste "quelleliste" und suche damit in Liste "neue_liste"
# trennzeile("#") # Vergleich repo -> SQL
print("Are the tables of the list", such_quelle, "included in the SQL?") # 2 Teile
trennzeile("~") # Quelle - quelleliste
for item in quelleliste: # nehme Liste "quelleliste" und suche damit in Liste "neue_liste"
# quelleliste wurde in erstelle_liste bereits normalisiert (upper) # trennzeile("#")
if item.upper() in neue_liste: ausgabe = "{:12}{:55}{:20}" # Ausgabeformat: Label(12), Tabellenname(55), Status(20)
ausgabe = "{:12}{:40}{:20}" print("Are the tables of the list", such_quelle, "included in the SQL?")
print(ausgabe.format("The Table", item, "is available")) trennzeile("~")
else: for item in quelleliste:
ausgabe = "{:12}{:40}{:20}" # quelleliste wurde in erstelle_liste bereits normalisiert (upper)
print(ausgabe.format("The Table", item, "is not available")) if item.upper() in neue_liste:
print(ausgabe.format("The Table", item, "is available"))
# Quelle - zielliste else:
# nehme Liste "zielliste" und suche damit in Liste "neue_liste" print(ausgabe.format("The Table", item, "is not available"))
trennzeile("~")
print("Are the tables of the list", such_ziel, "included in the SQL?") # Quelle - zielliste
trennzeile("~") # nehme Liste "zielliste" und suche damit in Liste "neue_liste"
for item in zielliste: trennzeile("~")
if item.upper() in neue_liste: print("Are the tables of the list", such_ziel, "included in the SQL?")
ausgabe = "{:12}{:40}{:20}" trennzeile("~")
print(ausgabe.format("The Table", item, "is available")) for item in zielliste:
else: if item.upper() in neue_liste:
ausgabe = "{:12}{:40}{:20}" print(ausgabe.format("The Table", item, "is available"))
print(ausgabe.format("The Table", item, "is not available")) else:
print(ausgabe.format("The Table", item, "is not available"))
trennzeile("~")
print("Create unique sorted list from ", such_quelle, "and", such_ziel) trennzeile("~")
q_z_liste = sorted({t.upper() for t in (quelleliste + zielliste)}) print("Create unique sorted list from ", such_quelle, "and", such_ziel)
q_z_liste = sorted({t.upper() for t in (quelleliste + zielliste)})
trennzeile("~")
trennzeile("~")
# Vergleich SQL -> repo
# Quelle - neue_liste # Vergleich SQL -> repo
# nehme Liste "quelleliste" und suche damit in Liste "neue_liste" # Quelle - neue_liste
trennzeile("~") # nehme Liste "quelleliste" und suche damit in Liste "neue_liste"
print("Are the tables from the SQL included in the RepoViz information?") trennzeile("~")
print("Note: I've merged <#@quelle> and <#@ziel> into one list ") print("Are the tables from the SQL included in the RepoViz information?")
trennzeile("~") print("Note: I've merged <#@quelle> and <#@ziel> into one list ")
for item in neue_liste: trennzeile("~")
if item in q_z_liste: for item in neue_liste:
ausgabe = "{:12}{:40}{:20}" if item in q_z_liste:
print(ausgabe.format("The Table", item, "is available")) print(ausgabe.format("The Table", item, "is available"))
else: else:
ausgabe = "{:12}{:40}{:20}" print(ausgabe.format("The Table", item, "is not available"))
print(ausgabe.format("The Table", item, "is not available"))
trennzeile("+")
trennzeile("+") print(r" _____ _ _____ _ ")
print(r" _____ _ _____ _ ") print(r"|_ _| |__ ___ | ____|_ __ __| |")
print(r"|_ _| |__ ___ | ____|_ __ __| |") print(r" | | | '_ \ / _ \ | _| | '_ \ / _` |")
print(r" | | | '_ \ / _ \ | _| | '_ \ / _` |") print(r" | | | | | | __/ | |___| | | | (_| |")
print(r" | | | | | | __/ | |___| | | | (_| |") print(r" |_| |_| |_|\___| |_____|_| |_|\__,_|")
print(r" |_| |_| |_|\___| |_____|_| |_|\__,_|") print("")
print("")