Python und die Verarbeitung der Bash-Befehlshistorie
Unter Linux arbeite ich relativ viel mit der Kommandozeile. Dabei nutze ich intensiv die Befehlshistorie, die bei mir über 100 000 unterschiedliche Einträge der letzten Jahre enthält und damit eine wertvolle und zuverlässige Wissensdatenbank darstellt.
Erst vor kurzem ist mir aufgefallen, dass sich bei der bash die Einträge auch um zusätzliche Annotationen und Kommentare erweitern lassen. Beispielsweise fügt die Shell den Zeitstempel der Ausführung als zusätzliche Information ein, wenn die Umgebungsvariable HISTTIMEFORMAT gesetzt und nicht leer ist.
Wenig überraschend ändert sich durch die Aktivierung dieses Features das Format der Datei, in der die Einträge gespeichert sind. Bisher wurde jede Kommandozeile in genau einer Zeile gespeichert, was die Verarbeitung mit anderen Unix-Tools relativ einfach machte. Im erweiterten Format werden die Zusatzinformationen vor der eigentlichen Befehlszeile in zusätzlichen Zeilen abgelegt, die alle mit dem Zeichen # beginnen.
Aufgrund dieser Änderung musste ich eines meiner Skripte anpassen, das die Historie unterschiedlicher Rechner aggregiert und ungewünschte redundante Einträge entfernt. Für solche Aufgaben eignet sich die Skriptsprache Python bestens.
Ich betrachte im folgenden nur die Aufgabenstellung, alle doppelten Kommandozeilen zu entfernen. Dabei soll jeweils das letzte Auftreten verwendet werden, so dass die Reihenfolge der jüngsten Einträge erhalten bleibt. Der gesamte Code verteilt sich auf drei Funktionen. Die erste Funktion zerteilt den Eingabestrom und liefert die einzelnen Einträge der Historie, die zweite Funktion entfernt die doppelten Einträge und die dritte Funktion gibt das Ergebnis über einen Ausgabestrom aus.
def parse(iterable):
'''parse bash history entries with optional comment lines'''
iterator = iterable.__iter__()
for line in iterator:
comments = []
while line.startswith(b'#'):
comments.append(line)
line = next(iterator)
yield line, b''.join(comments)
Bemerkenswert an dieser Funktion finde ich, was alles nicht da ist. Damit will ich sagen: Die Funktion ist verständlich, enthält eine vollständige Fehlerbehandlung und würde in anderen, objekt-orientierten Programmiersprachen vermutlich sehr ähnlich aussehen. Aber im Gegensatz zu anderen Sprachen ist Python – ganz ohne Magie – sehr kompakt und fokussiert auf das Wesentliche. Anschaulich wird das, wenn man in Gedanken die entsprechende Java-Implementierung dagegen hält.
def compact(iterable):
# dummy implementation, will be fixed later.
return iterable
Auf die zweite Funktion gehe ich erst weiter unten ein. Zunächst beschränke ich mich auf eine Implementierung, die alle Einträge ungefiltert zurückgibt.
Die dritte und letzte Funktion ist die Einfachste. Sie gibt in einer Schleife die Einträge über den angegebenen Ausgabestrom aus. Dabei ist noch anzumerken, dass die Zeilenumbrüche bereits in den Einträgen enthalten sind.
def dump(out, iterable):
'''dump history commands with comments to out'''
for command, comments in iterable:
out.write(comments)
out.write(command)
Das Hauptprogramm besteht lediglich aus der Verkettung dieser drei Funktionen und ist so bereits lauffähig. Allerdings werden noch keine doppelten Einträge entfernt. Die komplette Eingabe wird also unverändert ausgegeben – und zwar auf eine etwas umständliche Art und Weise.
if __name__ == '__main__':
dump(sys.stdout.buffer, compact(parse(sys.stdin.buffer)))
Zurück zur Filter-Funktion: Die Aufgabe hört sich schwieriger an als sie tatsächlich ist. Denn praktischerweise enthält die Standardbibliothek von Python eine Datenstruktur, die sich für diesen Anwendungsfall sehr gut eignet. Bei der Klasse collections.OrderedDict
handelt es sich um ein Dictionary, bei dem die einzelnen Elemente zusätzlich in einer beeinflussbaren Reihenfolge miteinander verkettet sind. Am ehesten ist sie mir der LinkedHashMap
aus Java vergleichbar. Ich finde die zahlreichen Einsatzmöglichkeiten dieser Klasse nicht immer intuitiv, aber wie man in der folgenden Funktion sieht, löst sich damit das Problem praktisch von selbst.
def compact(iterable):
'''remove duplicate commands and keep the last occurrences'''
entries = collections.OrderedDict()
for command, comments in iterable:
# overwrite entry if it already exists and move it to the end
entries[command] = comments
entries.move_to_end(command)
return entries.items()
Ich hoffe dieses Beispiel konnte ein wenig motivieren, dass viele kleine Aufgabenstellungen sehr einfach, sicher und effizient mit Python gelöst werden können. Besonders geeignet ist sie für Entwickler, die viel mit und unter Linux arbeiten.
Meiner Meinung nach sollte jeder fortgeschrittene Softwareentwickler mehrere höhere und hinreichend unterschiedliche Programmiersprachen gut beherrschen, um nicht in die Gefahr zu laufen, in jedem Problem den sprichwörtlichen Nagel zu sehen. Aus meiner Sicht bietet sich Python insbesondere für Java- und C++‑Entwickler an, da die Sprache sowohl sehr modern als auch hinreichend anders ist.
Leider wird diese Vielfältigkeit in der Praxis nach meiner Erfahrung nicht gelebt. Und wenn doch, dann muss man sich noch vor dem umgekehrten Problem in Acht nehmen: Denn neben den Schrauben nagelnden Entwicklern gibt es leider auch sehr viele, die umgekehrt glauben, mit dem scheinbar coolen Schraubenzieher besser Nägel in das Brett hauen zu können.