blog · git · desktop · images · contact


INI-Dateien mit sed bearbeiten

2010-08-19

INI-Dateien sind irgendwie störrische Biester. Problematisch werden sie dadurch, dass sie in Sektionen gegliedert sind und dass das Ende einer Sektion nicht expliziert markiert ist, sondern sich nur implizit durch den Beginn einer neuen Sektion oder durch das Dateiende ergibt. Dadurch kann man nicht völlig trivial bestimmte Sektionen herausholen oder löschen, sondern muss sich immer irgendwie Gedanken machen. Schon alleine durch die Tatsache der Gliederung ergeben sich auch schon Schwierigkeiten: Möchte man ein bestimmtes Schlüssel-Wert-Paar ändern (oder auch nur suchen), dann muss man jedesmal berücksichtigen, in welchem Abschnitt das denn passieren soll.

Leider haben solche Dateien auch in der Unix-Welt recht große Verbreitung, obwohl man sie eigentlich mit Windows assoziiert. Desktop Entry Files zum Beispiel, aber auch die Adressverwaltung abook verwendet solch ein Format. Nun gibt es für "gescheite" Programmiersprachen natürlich Bibliotheken, um mit INI-Dateien umgehen zu können. Aber was macht man, wenn es eben doch etwas "Primitives" sein muss? Oder wenn man einfach Spaß an sed hat? ;)

Gewöhnlich benutzt man sed für einfache Ersetzungen. Hier ein Pfad in einer Makefile geändert, dort eine Include-Anweisung korrigiert. Nichts besonderes. Tatsächlich ist sed aber etwas mächtiger. Ob das jetzt zwecks der Bearbeitung von INI-Dateien mit Kanonen auf Spatzen geschossen ist, sei dahingestellt, aber Bedingungen und Sprünge können schon nicht schaden. Und das kann sed eben.

Grundsätzlich arbeitet sed in Zyklen: Lies eine Zeile in den "pattern space" ein und führe das übergebene Skript aus -- danach ist ein Zyklus beendet. Im Normalfall wird am Ende eines Zyklus der "pattern space" noch ausgegeben, aber das kann man abschalten. Bei einfachen Ersetzungen führen diese Zyklen dazu, dass einfach nur Zeile für Zeile bearbeitet wird. Das sed-Programm kann aber auch komplexer sein und Zyklen beliebig verlängern oder vorzeitig beenden.

Extrahieren eines bestimmten Abschnitts:

sed -n '/^\[section header\]$/ ba; d; :a p; n; /^\[/ d; ba' < inifile
     |  \-----------------------/  |   | |  |  \-----/   |
     |              |              |   | |  |     |      \- Ansonsten
     |              |              |   | |  |     |         springe wieder
     |              |              |   | |  |     |         zu "a".
     |              |              |   | |  |     |
     |              |              |   | |  |     \- Wenn hier eine neue
     |              |              |   | |  |        Sektion beginnt,
     |              |              |   | |  |        lösche die Zeile und
     |              |              |   | |  |        beende den Zyklus.
     |              |              |   | |  |
     |              |              |   | |  \- Lies nächste Zeile ein.
     |              |              |   | |
     |              |              |   | \- Gib die Zeile zuerst einmal aus.
     |              |              |   |
     |              |              |   \- Label "a".
     |              |              |
     |              |              \- Abschnitt begann nicht, lösche die
     |              |                 Zeile, nächster Zyklus.
     |              |
     |              \- Wenn der Abschnitt beginnt, dann springe
     |                 zum Label "a".
     |
     \- Nicht automatisch jede Zeile ausgeben.

Auslassen eines Abschnitts:

sed -n '/^\[section header\]$/ ba; p; b; :a n; /^\[/ {p; b}; ba' < inifile
     |  \-----------------------/  |  |   | |  \----------/  |
     |              |              |  |   | |       |        \- Ansonsten
     |              |              |  |   | |       |           wieder zum
     |              |              |  |   | |       |           Label "a".
     |              |              |  |   | |       |
     |              |              |  |   | |       \- Wenn nun ein neuer
     |              |              |  |   | |          Abschnitt beginnt,
     |              |              |  |   | |          gib die Zeile aus
     |              |              |  |   | |          und neuer Zyklus.
     |              |              |  |   | |
     |              |              |  |   | \- Lies die nächste Zeile ein.
     |              |              |  |   |
     |              |              |  |   \- Label "a".
     |              |              |  |
     |              |              |  \- ... beende den Zyklus.
     |              |              |
     |              |              \- Ansonsten gib die Zeile aus und ...
     |              |
     |              \- Wenn hier der Abschnitt beginnt, dann springe
     |                 zum Label "a".
     |
     \- Nicht automatisch jede Zeile ausgeben.

Die im Einzelnen benutzten Befehle kann man ganz gut in "man sed" nachlesen. Dort steht auch, wann welcher Befehl vorzeitig einen Zyklus verlässt oder dass man Adressangaben für Bedingungen nutzen kann.

Diese beiden Operationen kann man nun geeignet verknüpfen und zusammen mit einer Ersetzungsfunktion, die aber so einfach ist, dass ich sie hier nicht weiter erklären werde, in ein Shellskript bringen. Und schon kann man INI-Dateien bearbeiten:

Dabei nutze ich natürlich aus, dass es in der Regel nicht auf die Reihenfolge in INI-Dateien ankommt. Das Shellskript, das einen bestimmten Wert in einem bestimmten Abschnitt ändert, könnte dann so aussehen:

#!/bin/bash

extract_section()
{
    sed -n '/^\['"$1"'\]$/ ba; d; :a p; n; /^\[/ d; ba'
}

without_section()
{
    sed -n '/^\['"$1"'\]$/ ba; p; b; :a n; /^\[/ {p; b}; ba'
}

# Sektion auslesen, Wert eines Schlüssels ändern.
foosection=$(extract_section foo < inifile)
foosection=$(echo "$foosection" | sed -r 's/^(key=).*/\1whatever you wish/')

# Ursprüngliche Datei ohne besagte Sektion ausgeben, danach nur die
# bearbeitete Sektion.
without_section foo < inifile
echo
echo "$foosection"

Die Funktionen sind nicht zwingend nötig, erhöhen aber die Übersichtlichkeit, wie ich finde.

Der Einfachheit zuliebe bin ich jetzt auch davon ausgegangen, dass es keine "überschüssigen" Leerzeichen gibt, zum Beispiel vor einem Schlüssel oder vor und hinter dem Bezeichner für eine neue Sektion. Wenn soetwas im konkreten Anwendungsfall vorkommen kann, muss man das natürlich noch mit einbauen.

Comments?