diff --git a/repovizcheck.py b/repovizcheck.py index 3c0daab..c7a0248 100644 --- a/repovizcheck.py +++ b/repovizcheck.py @@ -1,14 +1,32 @@ +""" +repovizcheck.py + +Kurzbeschreibung: +Dieses Skript vergleicht RepoViz-Metadaten (Annotationen in Kornshell-Skripten: "#@modul:", "#@quelle:", "#@ziel:") +mit tatsächlich im Skript verwendeten Tabellen (Erkennung über $SCHEMA. oder ${SCHEMA}). Es +ermöglicht einen schnellen Abgleich, welche Tabellen in RepoViz dokumentiert sind und welche im Code auftauchen. + +Aufruf: + python repovizcheck.py + +Wichtige Hinweise: +- 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 import os.path as p +import argparse -print(" ____ __ ___ ____ _ _ ") -print("| _ \ ___ _ __ ___ \ \ / (_)____ / ___| |__ ___ ___| | _____ _ __ ") -print("| |_) / _ \ '_ \ / _ \ \ \ / /| |_ / | | | '_ \ / _ \/ __| |/ / _ \ '__|") -print("| _ < __/ |_) | (_) | \ V / | |/ / | |___| | | | __/ (__| < __/ | ") -print("|_| \_\___| .__/ \___/ \_/ |_/___| \____|_| |_|\___|\___|_|\_\___|_| ") -print(" |_| ") +print(r" ____ __ ___ ____ _ _ ") +print(r"| _ \ ___ _ __ ___ \ \ / (_)____ / ___| |__ ___ ___| | _____ _ __ ") +print(r"| |_) / _ \ '_ \ / _ \ \ \ / /| |_ / | | | '_ \ / _ \/ __| |/ / _ \ '__|") +print(r"| _ < __/ |_) | (_) | \ V / | |/ / | |___| | | | __/ (__| < __/ | ") +print(r"|_| \_\___| .__/ \___/ \_/ |_/___| \____|_| |_|\___|\___|_|\_\___|_| ") +print(r" |_| ") such_quelle = "#@quelle:" # @@ -18,59 +36,88 @@ such_modul = "#@modul:" # allezeilen = [] # Das Skript wird zeilenwiese in diese Liste geschrieben, eine Zeile gleich ein Element der Liste kommentarzeilen = [] # Ein Teil der nicht benoetigten Zeilen kommt in diese Liste codezeilen = [] # Zeilen mit Code kommen in diese Liste +# Liste der gueltigen Dateiendungen (case-insensitive) perm_file_suffix = ['.ksh'] # Liste der gueltigen Dateiendungen -# Aufruf ohne Parameter abfangen -try: - sourcefile = sys.argv[1] # Schreibe den 1.Parameter in eine Variable - # print(sourcefile) # Ausgabe dateiname -except IndexError: - print() - print("Missing Parameter: Filename") - print() - sys.exit(0) +# Verwende argparse für robusten CLI-Aufruf und Help +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)') +args = parser.parse_args() +sourcefile = args.sourcefile -file_suffix = p.splitext(sourcefile)[1] # Dateiendung ermitteln -if file_suffix not in perm_file_suffix: # In Liste Ja/Nein - print("") # Gib eine leere Zeile aus +# Dateiendung prüfen (case-insensitive) +file_suffix = p.splitext(sourcefile)[1].lower() +if file_suffix not in perm_file_suffix: + print() # Gib eine leere Zeile aus print("Only Kornshell scripts can be processed.") # Gib diese Meldung aus - print("") # Gib eine leere Zeile aus - sys.exit(0) # Beende das Programm + print() + sys.exit(1) # Funktion zum Oeffnen einer Datei mit Pruefung def get(name): + '''Funktion zum Oeffnen einer Datei mit Pruefung (prüft Encoding)''' + # Versuche zuerst UTF-8, lese einen kleinen Block zur Validierung. try: - # return open(name, "r", encoding='ISO-8859-1') # oeffne die Datei zum Lesen - return open(name, "r") # oeffne die Datei zum Lesen - # except FileNotFoundError: # Bei einem Fehler ... Python3 - except IOError: # Bei einem Fehler ... Python2 - print("") # Gib eine leere Zeile aus - print("The File ", name, "doesn't exists!") # Gib diese Meldung aus - print("") # Gib eine leere Zeile aus - sys.exit(0) # Beende das Programm + f = open(name, "r", encoding='utf-8') + except IOError: + print("") + print("The File", name, "doesn't exist or can't be opened!") + print("") + sys.exit(1) + + try: + # Lese ein kleines Stück, um mögliche Decode-Fehler sofort zu erzeugen + f.read(2048) + f.seek(0) + return f + except UnicodeDecodeError: + # UTF-8 scheint nicht zu passen — versuche ISO-8859-1 + try: + f.close() + return open(name, "r", encoding='ISO-8859-1') + except IOError: + print("") + print("The File", name, "can't be opened with fallback encoding!") + print("") + sys.exit(1) + except Exception: + # unerwarteter Fehler beim Lesen + f.close() + print("") + print("Error reading the file", name) + print("") + sys.exit(1) return None # Funktion zum Erstellen einer Tabellenliste aus den RepoViz Informationen def erstelle_liste(datei, typ): # 2 Parameter - laenge = len(typ) # bestimme die Laenge - tabellenliste = [] # erstelle leere Liste - fobj_in = get(datei) # uebergebe den Dateinamen an die Funktion - for line in fobj_in: # gehe zeilenwiese durch die Quelldatei - if line[0:laenge] == typ: # Erstelle Liste mit den Quelltabellen - zeile = (line[laenge:]) - if zeile[-2] == ",": # ist ein Komma am Zeilenende - zeile = zeile[0:-2] # entferne Komma am Zeilenende - liste = zeile.split(",") # Teile die Zeile am Komma auf und speichere die Elemente in einer Liste - for item in liste: # gehe durch die Liste - tabellenliste.append(item.strip()) # entferne Leerzeichen und fuege die Elemente der Liste hinzu - fobj_in.close() # schliesse Quelldatei - return(tabellenliste) # gebe die fertige Liste zurueck + '''Erstelle eine Liste aus der übergebenen Datei für den Typ der übergeben wurde''' + laenge = len(typ) + tabellenliste = [] + fobj_in = get(datei) + for line in fobj_in: + # entferne nur führende Leerzeichen vor der Prüfung + content = line.lstrip() + if content.startswith(typ): + zeile = content[len(typ):].strip() + # entferne ein mögliches abschließendes Komma + if zeile.endswith(','): + zeile = zeile[:-1] + liste = [it.strip() for it in zeile.split(',') if it.strip()] + for item in liste: + # normalisiere auf Großbuchstaben für spätere Vergleiche + tabellenliste.append(item.strip().upper()) + fobj_in.close() + return tabellenliste def trennzeile(typ): # Funktion zum Ausgeben einer 80 Zeichen breiten Trennzeile. - return(print(typ * 80)) # Das Trennzeichen ist variabel und wird der Funktion als Parameter uebergeben. + ''' + Erstellt eine 80 zeichenbreite Zeile mit dem übergebenen Zeichen + ''' + print(typ * 80) # Das Trennzeichen ist variabel und wird der Funktion als Parameter uebergeben. modulliste = erstelle_liste(sourcefile, such_modul) # Erstelle Liste mit den Modulen @@ -79,26 +126,25 @@ zielliste = erstelle_liste(sourcefile, such_ziel) # Erstelle Liste mit den datei = get(sourcefile) # Oeffnen der Datei mit einer Funktion, die auch prueft ob die Datei existiert allezeilen = datei.readlines() # Lesen aller Zeilen in eine Liste -datei.close() # Schliessen der Datei +datei.close() # Versuch die Kommentarzeilen loszuwerden -for zeile in allezeilen: # gehe durch die - if zeile.strip()[0:8] == '#@modul:': # Damit diese Zeile auch geprueft wird !! - codezeilen.append(zeile.strip()) # entferne Leerzeichen und fuege die Elemente der Liste hinzu - # Alle Zeilen die nicht betrachtet werden, kommen in diese Liste. Mir ist bis jetzt keine andere Loesung eingefallen. - if zeile.strip()[0:1] == '#' \ - or zeile.strip()[0:2] == '--' \ - or zeile.strip()[0:4] == 'j000' \ - or zeile.strip()[0:7] == 'SQLFILE' \ - or zeile.strip()[0:8] == 'fmeldung' \ - or zeile.strip()[0:5] == 'ALTER' \ - or zeile.strip()[0:10] == 'f_truncate' \ - or zeile.strip()[0:2] == 'fi' \ - or zeile.strip()[0:2] == 'if' \ - or zeile.strip()[0:10] == 'f_runstats': - kommentarzeilen.append(zeile.strip()) # entferne Leerzeichen und fuege die Elemente der Liste hinzu - else: # Die Codezeilen und ein paar mehr. - codezeilen.append(zeile.strip()) # entferne Leerzeichen und fuege die Elemente der Liste hinzu +for zeile in allezeilen: + s = zeile.strip() + if not s: + # leere Zeilen überspringen + continue + if s.startswith('#@modul:'): + codezeilen.append(s) + continue + # Gruppiere typische Kommentar-/Meta-Zeilen (case-insensitive) + up = s.upper() + if s.startswith('#') or s.startswith('--') or up.startswith('J000') or up.startswith('SQLFILE') \ + 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) + else: + codezeilen.append(s) trennzeile("+") @@ -116,13 +162,12 @@ print(ausgabe.format(such_ziel, zielliste)) # Ausgabe trennzeile("~") trennzeile("") print("Comparison of given script name with RepoVizInformation") -for item in modulliste: - trennzeile("") - trennzeile("~") - if item == p.basename(sourcefile): - print("Script", p.basename(sourcefile), "is in the RepoViz information!") - else: - print("Script", p.basename(sourcefile), "is missing in the RepoViz information!") +# Vergleiche nur einmal, normalisiere auf Großbuchstaben +script_basename = p.basename(sourcefile).upper() +if script_basename in [m.strip().upper() for m in modulliste]: + print("Script", p.basename(sourcefile), "is in the RepoViz information!") +else: + print("Script", p.basename(sourcefile), "is missing in the RepoViz information!") # Suche Objekte anhand der RepoVizInformationen trennzeile("~") @@ -131,17 +176,22 @@ print("") print("Create a list of tables from the existing code: list ") # Der Code steht schon in der Liste neue_liste = [] -# for line in codezeilen: +# verbessertes Regex-Parsing: capture-Gruppe für Tabellennamen, compile einmal +regex = re.compile(r"(?:\$SCHEMA\.|\${SCHEMA}\.)\s*([A-Z0-9_]+)", re.IGNORECASE) +# nutze Set für eindeutige Sammlung +neue_set = set() for line in allezeilen: - regex = re.compile(r"(?:\$SCHEMA\.|\${SCHEMA}\.).*?\s") - sql = re.findall(regex, line.upper()) # Suche nach $SCHEMA. oder ${SCHEMA}. - if len(sql) != 0: - tabname = sql[0].split(".")[1].strip() # der Name wird ... - if tabname.strip()[0:1] != '$': - tabname = re.sub("\\W", "", tabname) - if tabname not in neue_liste and tabname[0:4] != "TMP_": # Es werden nur Tabellen hinzugefuegt die noch nicht in der - # Ergebnisliste sind und nicht mit TMP_ beginnen. - neue_liste.append(tabname.strip()) + for match in regex.findall(line): + tabname = match.strip() + if not tabname: + continue + # ignoriere Variablen oder temporäre Tabellen + if tabname.startswith('$'): + continue + tabname_norm = re.sub(r"[^A-Z0-9_]", "", tabname.upper()) + if tabname_norm and not tabname_norm.startswith('TMP_'): + neue_set.add(tabname_norm) +neue_liste = sorted(neue_set) print("The list contains:", len(neue_liste), "entries") trennzeile("~") @@ -155,7 +205,8 @@ trennzeile("~") print("Are the tables of the list", such_quelle, "included in the SQL?") trennzeile("~") for item in quelleliste: - if item in neue_liste: + # quelleliste wurde in erstelle_liste bereits normalisiert (upper) + if item.upper() in neue_liste: ausgabe = "{:12}{:40}{:20}" print(ausgabe.format("The Table", item, "is available")) else: @@ -168,7 +219,7 @@ trennzeile("~") print("Are the tables of the list", such_ziel, "included in the SQL?") trennzeile("~") for item in zielliste: - if item in neue_liste: + if item.upper() in neue_liste: ausgabe = "{:12}{:40}{:20}" print(ausgabe.format("The Table", item, "is available")) else: @@ -177,12 +228,7 @@ for item in zielliste: trennzeile("~") print("Create unique sorted list from ", such_quelle, "and", such_ziel) -q_z_liste = [] -basis_liste = quelleliste + zielliste -for tabname in basis_liste: - if tabname not in q_z_liste: - q_z_liste.append(tabname) -q_z_liste.sort() +q_z_liste = sorted({t.upper() for t in (quelleliste + zielliste)}) trennzeile("~") @@ -202,9 +248,9 @@ for item in neue_liste: print(ausgabe.format("The Table", item, "is not available")) trennzeile("+") -print(" _____ _ _____ _ ") -print("|_ _| |__ ___ | ____|_ __ __| |") -print(" | | | '_ \ / _ \ | _| | '_ \ / _` |") -print(" | | | | | | __/ | |___| | | | (_| |") -print(" |_| |_| |_|\___| |_____|_| |_|\__,_|") +print(r" _____ _ _____ _ ") +print(r"|_ _| |__ ___ | ____|_ __ __| |") +print(r" | | | '_ \ / _ \ | _| | '_ \ / _` |") +print(r" | | | | | | __/ | |___| | | | (_| |") +print(r" |_| |_| |_|\___| |_____|_| |_|\__,_|") print("")