[X] Boost::Spirit - Eine Einführung



  • Mach ich,

    werde die Hauptpunkte beim nächsten mal unterstreichen 😉

    Gruss
    Tobi



  • Aha! Jetzt wo die Grundlagen erklärt werden, sieht das ganze schon viel verständlicher aus, wenn man auf das ini-Beispiel stösst. 👍

    Ein Bitte noch, wo ich doch nicht so der Spirit-Kenner bin: es wäre vielleicht noch von Vorteil, wenn man schreibt, was beim Compilieren passiert? Wenn ich mich nicht täusche, ist das ja alles Metaprogrammierung, oder? Also es wird beim Compilieren ein Parser erzeugt. Ist ja schon wichtig zu wissen, wenn dem so ist. Aber wie gesagt, ich bin da jetzt nicht der Experte, deshalb hinterfrage ich es.



  • Hallo Artchi,

    freut mich wenn es jetzt klarer geworden ist. Auf die Vorgänge beim compilieren möchte ich nicht näher eingehen, da es sich um eine Einführung handelt und ich mich eigentlich auf die Verwendung konzentrieren möchte.

    Es könnte jedoch durchaus sein, dass auf den Einführungsartikel noch ein zweiter Artikel folgt, der in die tiefen von Spirit hinab steigt und eigene Scanner, Parser, Matchklassen etc. erklärt.

    Vorerst möchte ich aber das noch einmal zurückstellen...

    (Kommt eh noch einiges zusammen... 🙂 )

    Gruss
    Tobi



  • 1 Grundlegendes
    Dieser Artikel soll Sie in die Welt von Boost::Spirit einführen. Boost::Spirit ist ein Parser Gernerator Framework, dass in C++ realisiert ist. Um die folgenden Beispiele complieren zu können ist Boost::Spirit ab der Version 1.6.3 zu verwenden. Desweiteren wird ein ISO C++ Compilier (gcc, Visual C++(ab Version 7.1), ...) benötigt

    Anmerkung: Probleme bereiten unteranderem MS Visual C++ 6.0 und Open Watcom 1.3.

    1.1 Von Dateien und anderen zu durchsuchenden Strukturen
    Ein klassisches Beispiel für die Verwendung eines Parsers ist die Dateiverarbeitung. Wie oft kommt es vor, dass man ein bestimmtes Format oder eine bestimmte Struktur zu durchsuchen hat und keine geeigneten Klassen oder Strukturen zur verfügung stehen.
    XML Dateien werden etwa anders verarbeitet als INI Dateien. Mathematische Eingaben sollen anders interpretiert werden als bestimmte Stringfolgen. All dass kennt man mittlerweile als Programmierer zur Genüge.
    Auf diesem Gebiet hat Boost::Spirit seine unverkennbaren Stärken. Durch die einfache Definition von Regeln ist in null kommma nichts ein Parser entwicklet, der die Arbeit von alleine erledigt. Ob dabei Zeilenweise durch eine Datei geparst wird, oder Mathematische Ausdrücke geparst werden spielt dabei keine große Rolle. Boost::Spirit kann in diese Richtung fast alles handhaben.

    2 Installation von Boost::Spirit
    Um das Framework von Boost::Spirit nutzen zu können ist es erforderlich, die Header Dateien bekannt zu machen. Am einfachsten ist es den Ordner boost aus dem Verzeichnis spirit-1.6.3\boost sowie aus dem Verzeichnis spirit-1.6.3\boost\miniboost in das Include Verzeichnis des Compiliers/der Entwicklungsumgebung zu kopieren.
    In ein Projekt müssen unbedingt folgende Zeilen eingebunden werden.

    // ...
    #include <boost/spirit.hpp>
    /*****************************************************************************/
    /* Die obige Include Zeile ist nur zu verwenden, falls die Includedateien    */
    /* Systemweit bekannt gemacht wurden...                                      */
    /* Falls sie sich im Projektverzeichnis befinden bitte                       */
    /* #include "boost/spirit.hpp"                                               */
    /* einbinden.                                                                */
    /*****************************************************************************/
    // ...
    using namespace boost::spirit
    

    2.1 Theoretische Informatik
    Für die folgenden Beispiele ist es vielleicht ganz interessant sich etwas in die Theoretische Informatik einzulesen. Deswegen hab ich ein kleines PDF erstellt:
    Klick mich

    3 Die Grundlagen für Parseaktionen im Framework
    Der Datentyp, der in den nächsten Beispielen immer wieder vorkommen wird, ist der rule<> Datentyp. Mit diesem Datentyp, baut man Parser auf. Spirit stellt von sich aus schon diverse Parser zur Verfügung von denen ich einige in diesem Kapitel
    vorstellen möchte.

    3.1 Die Literale strlit<> und chlit<>

    // ...
    rule<> ch_A = chlit<>('A');
    rule<> str_Hallo = strlit<>("Hallo");
    // ...
    

    Wie schon zu vermuten ist, werden hier ein Character Literal und ein String Literal erstellt. Es ist durchaus möglich, auch Variablen in der Regel anzugeben wie das folgende Beispiel zeigt.

    // ...
    string myString = "Test";
    rule<> str_Variabel = strlit<>(myString.c_str());  //char* übergeben
    // Alternativ
    rule<> str_Variable2 = strlit<string::const_iterator>(myString.begin(), myString.end());
    // ...
    

    3.2 Ranges
    Um einen Bereich abzudecken gibt es den range<> Parser.

    //...
    rule<> rng = range<>('A', 'Z');
    //...
    

    Die Regel rng deckt die Buchstaben von 'A' bis 'Z' ab.

    3.3 Zahlen
    Es gibt in Spirit diverse Zahlenparser. Einer der wichtigeren ist wohl der Integerparser.

    //...
    rule<> integer = int_p;
    //...
    

    Erlaubt eine beliebige Ganzzahl.

    3.4 Operatoren
    Es gibt in Spirit eine Reihe von Operatoren zum Verknüpfen von Regeln. Dazu gehören unteranderem

    <a> | <b>	// <a> oder <b> oder beide werden gematched
    <a> - <b>	// <a> ohne <b>
    <a> >> <b>	// <a> gefolgt von <b>
    !<a>		// 0 oder 1 mal <a>
    *<a>		// 0 oder beliebig oft mal <a>
    +<a>		// 1 oder beliebig oft mal <a>
    ~<a>		// nicht <a>
    

    Weiterführendes Material findet sich unter Spirit Doku

    4 Boost::Spirit an Hand eines .ini Datei Parsers
    Zur Erläuterung der Funktionsweise von Boost::Spirit werden wir uns im folgenden einen .ini Fileparser zusammenbauen. Dazu definieren wir das Aussehen einer .ini Datei
    Definition:

    [Sektionsname{1}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    [Sektionsname{2}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    .
    .
    .
    [Sektionsname{n}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    

    Für diese Definition überlegen wir uns im folgenden Regeln, die wir in Spirit abbilden und die uns das Parsen erlauben.

    Es folgen die Regeln abgebildet in Spirit!

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          //[1]
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));               //[2]
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;//[3]
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION | _VAR_DECLARATION | eol_p;                 //[4]
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                   //[5] 
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:

    rule<> Variablename = Zuweisung
    

    Der rule<> Datentyp
    Der Variablentyp rule<> ist einer der wichtigsten Datentypen, wenn man mit Boost::Spirit in Kontakt kommt. Der rule<> Datentyp legt das "Aussehen" eines Parseausdruck fest.

    Zu [1]:
    Es wird die Regel _VALUE definiert. Wir legen fest, das _VALUE jedes Zeichen außer ein "End of Line" und ein "Eingabe Ende" beinahlten darf. Der * vor der Klammer ist der sogenannte "Kleene Star" und besagt, das 0 bis beliebig viele Zeichen vorkommen können.

    Zu [2]:
    Die Regel _VAR legt fest, dass for dem '=' mindestens ein Zeichen (+) stehen muss, dass kein '=' enthalten darf. Desweiteren dürfen keine Leerzeichen, keine Returns, keine Whitespaces und derartige dinge vorkommen(space_p Parser).

    Zu [3]:
    In der Regel _VAR_DECLARATION haben wir unser erstes "gefolgt von" Zeichen. Wir definieren das eine Variablen Declaration wie folgt aussieht. Die Regel _VAR gefolgt von (>>) einem Character Literal ('=') gefolgt von (>>) der Regel _VALUE gefolgt von (>>) einem möglichen "End of Line".

    Zu [4]:
    Die Regel _INI_ROW ist unsere sogenannte "Startregel". Sie ist es, die die parse Funktion bedient. In Ihr legen wir fest wie eine Zeile in der .ini File auszusehen hat. Entweder es kommt eine _SECTION, oder (|) eine _VAR_DECLARATION oder (|) ein "End of Line".

    Zu [5]:
    Die Funktion parse liefert ein Parseergebnis, auf dass wir später noch genauer eingehen werden. Vorerst ist es nur wichtig zu wissen, das die parse Funktion in unserem Beispiel zwei Argumente benötigt. Nämlich den zu parsenden Ausdruck und die "Startregel". Das .full zeigt uns, ob wir den Ausdruck komplett durchlaufen haben. Ist das geschehen geben wir "Valid" aus.

    Zusammenfassend zu diesem Beispiel:

    -> Der Datentyp rule<> als einer der wichtigsten Datentypen in Boost::Spirit.
    -> Es gibt verschiedene vorgefertigte Parser wie eol_p, space_p, anychar_p ...
    -> Wichtige Operatoren sind: *, +, |, >>
    -> Die parse Funktion übernimmt die Arbeit und liefert als Ergebnis einen "true"
    oder "false" Wert.

    5 Die Parse Funktion
    Wie im obigen Beispiel gesehen, ist es die parse Funktion die den eigentlichen Parsevorgang übernimmt. Diese Funktion liefert in Wirklichkeit mehr als nur einen boolsche Wert. Der Rückgabewert ist eine Variable vom Typ parse_info
    Die Struktur enthält folgende Membervariablen:

    -> stop
    -> hit
    -> full
    -> length

    Über diese Variablen lassen sich einige Informationen extrahieren, die Aufschluss über den Parsevorgang geben.

    Über Stop findet man die Position im geparsten Ausdruck wo aufgehört wurde zu parsen.

    Bsp:

    // ...
    
    // Eine Regel, die eine Liste von Zahlen getrennt durch Kommas liest
    rule<> r = int_p >> *(chlit<char>(',') >> int_p);
    
    // Das ParseInfo Objekt
    parse_info<> pI;
    
    // Parsen eines ausdrucks, der nicht nur Zahlen entält
    pI = parse("1,2,3,a,b,0", r);
    
    // Ausgabe der Werte von ParseInfo
    std::cout << pI.stop << std::endl << pI.full << std::endl << pI.hit << std::endl << pI.length << std::endl;
    
    // ...
    

    Die Ausgabe des Programms

    ,a,b,0      // Hier wurde aufgehört zu parsen
    0           // Ausdruck nicht vollständig geparst
    1           // Teilausdruck wurde erkannt
    5           // 5 Zeichen wurden geparst
    

    Mit diesen Angaben kann man unter anderem einschränken, wo der zu parsende Ausdruck "fehlerhaft" war.

    6 Anzeigen von Daten aus dem Parsevorgang mit Funktionen
    Nun ist es ja gut und schön wenn man weiß, dass eine zu parsender Ausdruck den Regeln entspricht. Aber wie an die Daten kommen, die in dem Ausdruck vorhanden sind.

    Dafür gibt es in Boost::Spirit unteranderem Funktionen die im Grundgerüst wie folgt definiert sind.

    template<IteratorT>
    void <funktionsname>(IteratorT begin, IteratorT end)
    {
      //Verarbeitung der Iteratoren
    }
    
    // oder falls nur ein Iterator zur Verfügung steht z.B bei Zahlenparsern von Spirit
    void <funktionsname>(IteratorT begin)
    {
      //Verarbeitung des Iterators
    }
    

    Als Beispiel nehmen wir erneut den .ini File Parser. Es wird im folgendem Beispiel eine Funktion definiert, die uns beispielhaft die Sektionen ausgibt.

    #include <iostream>
    #include <string>
    using namespace std;
    // ...
    template<typename IteratorT>
    void DebugOut(IteratorT begin, IteratorT end)
    {
        string str(begin, end);
        cout << str << endl;
        /* Alternative */
        /*
        while(begin != end)
            cout << *begin++;
        cout << endl;
        */
    }
    
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;
    
                                  //Hier wird die Ausgabe erzeugt!
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Mit diesem Konstrukt haben Sie die Möglichkeit sehr einfache Ausgaben zu erzeugen. Dies kann für Debug Informationen, die ausgegeben werden sollen, sehr nützlich sein.

    Zur Ergänzung möchte ich Ihnen nun noch ein Beispiel für Zahlenparser (ein Iterator) liefern.

    // ...
    
    template<typename IteratorT>
    void DebugOut(IteratorT only_one)
    {
        cout << only_one << endl;
        cout << only_one << " + 1 = " << only_one+1 << endl;
    }
    
    int main(int argc, char* argv)
    {
    
        rule<> count_val = int_p[&DebugOut<int>];
        if(parse("1", count_val).full)
            cout << "Supi";
    
    }
    

    Nun ist das Anzeigen von Daten zwar wie oben genannt in manchen Fällen nützlich, jedoch ist es fast immer der Fall dass man Informationen speichern muss. Spirit regelt diesen Vorgang über Funktionsobjekte.

    Wird fortgesetzt...



  • 1 Grundlegendes
    Dieser Artikel soll Sie in die Welt von Boost::Spirit einführen. Boost::Spirit ist ein Parser Gernerator Framework, dass in C++ realisiert ist. Um die folgenden Beispiele complieren zu können ist Boost::Spirit ab der Version 1.6.3 zu verwenden. Desweiteren wird ein ISO C++ Compilier (gcc, Visual C++(ab Version 7.1), ...) benötigt

    Anmerkung: Probleme bereiten unteranderem MS Visual C++ 6.0 und Open Watcom 1.3.

    1.1 Von Dateien und anderen zu durchsuchenden Strukturen
    Ein klassisches Beispiel für die Verwendung eines Parsers ist die Dateiverarbeitung. Wie oft kommt es vor, dass man ein bestimmtes Format oder eine bestimmte Struktur zu durchsuchen hat und keine geeigneten Klassen oder Strukturen zur verfügung stehen.
    XML Dateien werden etwa anders verarbeitet als INI Dateien. Mathematische Eingaben sollen anders interpretiert werden als bestimmte Stringfolgen. All dass kennt man mittlerweile als Programmierer zur Genüge.
    Auf diesem Gebiet hat Boost::Spirit seine unverkennbaren Stärken. Durch die einfache Definition von Regeln ist in null kommma nichts ein Parser entwicklet, der die Arbeit von alleine erledigt. Ob dabei Zeilenweise durch eine Datei geparst wird, oder Mathematische Ausdrücke geparst werden spielt dabei keine große Rolle. Boost::Spirit kann in diese Richtung fast alles handhaben.

    2 Installation von Boost::Spirit
    Um das Framework von Boost::Spirit nutzen zu können ist es erforderlich, die Header Dateien bekannt zu machen. Am einfachsten ist es den Ordner boost aus dem Verzeichnis spirit-1.6.3\boost sowie aus dem Verzeichnis spirit-1.6.3\boost\miniboost in das Include Verzeichnis des Compiliers/der Entwicklungsumgebung zu kopieren.
    In ein Projekt müssen unbedingt folgende Zeilen eingebunden werden.

    // ...
    #include <boost/spirit.hpp>
    /*****************************************************************************/
    /* Die obige Include Zeile ist nur zu verwenden, falls die Includedateien    */
    /* Systemweit bekannt gemacht wurden...                                      */
    /* Falls sie sich im Projektverzeichnis befinden bitte                       */
    /* #include "boost/spirit.hpp"                                               */
    /* einbinden.                                                                */
    /*****************************************************************************/
    // ...
    using namespace boost::spirit
    

    2.1 Theoretische Informatik
    Für die folgenden Beispiele ist es vielleicht ganz interessant sich etwas in die Theoretische Informatik einzulesen. Deswegen hab ich ein kleines PDF erstellt:
    Klick mich

    3 Die Grundlagen für Parseaktionen im Framework
    Der Datentyp, der in den nächsten Beispielen immer wieder vorkommen wird, ist der rule<> Datentyp. Mit diesem Datentyp, baut man Parser auf. Spirit stellt von sich aus schon diverse Parser zur Verfügung von denen ich einige in diesem Kapitel
    vorstellen möchte.

    3.1 Die Literale strlit<> und chlit<>

    // ...
    rule<> ch_A = chlit<>('A');
    rule<> str_Hallo = strlit<>("Hallo");
    // ...
    

    Wie schon zu vermuten ist, werden hier ein Character Literal und ein String Literal erstellt. Es ist durchaus möglich, auch Variablen in der Regel anzugeben wie das folgende Beispiel zeigt.

    // ...
    string myString = "Test";
    rule<> str_Variabel = strlit<>(myString.c_str());  //char* übergeben
    // Alternativ
    rule<> str_Variable2 = strlit<string::const_iterator>(myString.begin(), myString.end());
    // ...
    

    3.2 Ranges
    Um einen Bereich abzudecken gibt es den range<> Parser.

    //...
    rule<> rng = range<>('A', 'Z');
    //...
    

    Die Regel rng deckt die Buchstaben von 'A' bis 'Z' ab.

    3.3 Zahlen
    Es gibt in Spirit diverse Zahlenparser. Einer der wichtigeren ist wohl der Integerparser.

    //...
    rule<> integer = int_p;
    //...
    

    Erlaubt eine beliebige Ganzzahl.

    3.4 Operatoren
    Es gibt in Spirit eine Reihe von Operatoren zum Verknüpfen von Regeln. Dazu gehören unteranderem

    <a> | <b>	// <a> oder <b> oder beide werden gematched
    <a> - <b>	// <a> ohne <b>
    <a> >> <b>	// <a> gefolgt von <b>
    !<a>		// 0 oder 1 mal <a>
    *<a>		// 0 oder beliebig oft mal <a>
    +<a>		// 1 oder beliebig oft mal <a>
    ~<a>		// nicht <a>
    

    Weiterführendes Material findet sich unter der Spirit Doku.

    4 Boost::Spirit an Hand eines .ini Datei Parsers
    Zur Erläuterung der Funktionsweise von Boost::Spirit werden wir uns im folgenden einen .ini Fileparser zusammenbauen. Dazu definieren wir das Aussehen einer .ini Datei.
    Definition:

    [Sektionsname{1}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    [Sektionsname{2}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    .
    .
    .
    [Sektionsname{n}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    

    Für diese Definition überlegen wir uns im folgenden Regeln, die wir in Spirit abbilden und die uns das Parsen erlauben.

    Es folgen die Regeln abgebildet in Spirit!

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          //[1]
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));               //[2]
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;//[3]
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION | _VAR_DECLARATION | eol_p;                 //[4]
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                   //[5] 
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:

    rule<> Variablename = Zuweisung
    

    Der rule<> Datentyp
    Der Variablentyp rule<> ist einer der wichtigsten Datentypen, wenn man mit Boost::Spirit in Kontakt kommt. Der rule<> Datentyp legt das "Aussehen" eines Parseausdruck fest.

    Zu [1]:
    Es wird die Regel _VALUE definiert. Wir legen fest, das _VALUE jedes Zeichen außer ein "End of Line" und ein "Eingabe Ende" beinahlten darf. Der * vor der Klammer ist der sogenannte "Kleene Star" und besagt, das 0 bis beliebig viele Zeichen vorkommen können.

    Zu [2]:
    Die Regel _VAR legt fest, dass for dem '=' mindestens ein Zeichen (+) stehen muss, dass kein '=' enthalten darf. Desweiteren dürfen keine Leerzeichen, keine Returns, keine Whitespaces und derartige dinge vorkommen(space_p Parser).

    Zu [3]:
    In der Regel _VAR_DECLARATION haben wir unser erstes "gefolgt von" Zeichen. Wir definieren das eine Variablen Declaration wie folgt aussieht. Die Regel _VAR gefolgt von (>>) einem Character Literal ('=') gefolgt von (>>) der Regel _VALUE gefolgt von (>>) einem möglichen "End of Line".

    Zu [4]:
    Die Regel _INI_ROW ist unsere sogenannte "Startregel". Sie ist es, die die parse Funktion bedient. In Ihr legen wir fest wie eine Zeile in der .ini File auszusehen hat. Entweder es kommt eine _SECTION, oder (|) eine _VAR_DECLARATION oder (|) ein "End of Line".

    Zu [5]:
    Die Funktion parse liefert ein Parseergebnis, auf dass wir später noch genauer eingehen werden. Vorerst ist es nur wichtig zu wissen, das die parse Funktion in unserem Beispiel zwei Argumente benötigt. Nämlich den zu parsenden Ausdruck und die "Startregel". Das .full zeigt uns, ob wir den Ausdruck komplett durchlaufen haben. Ist das geschehen geben wir "Valid" aus.

    Zusammenfassend zu diesem Beispiel:

    -> Der Datentyp rule<> als einer der wichtigsten Datentypen in Boost::Spirit.
    -> Es gibt verschiedene vorgefertigte Parser wie eol_p, space_p, anychar_p ...
    -> Wichtige Operatoren sind: *, +, |, >>
    -> Die parse Funktion übernimmt die Arbeit und liefert als Ergebnis einen "true" oder "false" Wert.

    5 Die Parse Funktion
    Wie im obigen Beispiel gesehen, ist es die parse Funktion die den eigentlichen Parsevorgang übernimmt. Diese Funktion liefert in Wirklichkeit mehr als nur einen boolsche Wert. Der Rückgabewert ist eine Variable vom Typ parse_info
    Die Struktur enthält folgende Membervariablen:

    -> stop
    -> hit
    -> full
    -> length

    Über diese Variablen lassen sich einige Informationen extrahieren, die Aufschluss über den Parsevorgang geben.

    Über stop findet man die Position im geparsten Ausdruck wo aufgehört wurde zu parsen.

    hit ist true oder false. Sprich entweder wurde mindestens ein Teilausdruck geparst oder es wurde kein einziger Ausdruck geparst.

    full ist ebenfalls true oder false. Es sagt aus ob der ganze Parseausdruck geparst wurde oder nicht.

    length gibt die Anzahl der Zeichen an, die geparst wurden.

    Bsp:

    // ...
    
    // Eine Regel, die eine Liste von Zahlen getrennt durch Kommas liest
    rule<> r = int_p >> *(chlit<char>(',') >> int_p);
    
    // Das ParseInfo Objekt
    parse_info<> pI;
    
    // Parsen eines ausdrucks, der nicht nur Zahlen entält
    pI = parse("1,2,3,a,b,0", r);
    
    // Ausgabe der Werte von ParseInfo
    std::cout << pI.stop << std::endl << pI.full << std::endl << pI.hit << std::endl << pI.length << std::endl;
    
    // ...
    

    Die Ausgabe des Programms

    ,a,b,0      // Hier wurde aufgehört zu parsen
    0           // Ausdruck nicht vollständig geparst
    1           // Teilausdruck wurde erkannt
    5           // 5 Zeichen wurden geparst
    

    Mit diesen Angaben kann man unter anderem einschränken, wo der zu parsende Ausdruck "fehlerhaft" war.

    6 Anzeigen von Daten aus dem Parsevorgang mit Funktionen
    Nun ist es ja gut und schön wenn man weiß, dass eine zu parsender Ausdruck den Regeln entspricht. Aber wie an die Daten kommen, die in dem Ausdruck vorhanden sind.

    Dafür gibt es in Boost::Spirit unteranderem Funktionen die im Grundgerüst wie folgt definiert sind.

    template<IteratorT>
    void <funktionsname>(IteratorT begin, IteratorT end)
    {
      //Verarbeitung der Iteratoren
    }
    
    // oder falls nur ein Iterator zur Verfügung steht z.B bei Zahlenparsern von Spirit
    void <funktionsname>(IteratorT begin)
    {
      //Verarbeitung des Iterators
    }
    

    Als Beispiel nehmen wir erneut den .ini File Parser. Es wird im folgendem Beispiel eine Funktion definiert, die uns beispielhaft die Sektionen ausgibt.

    #include <iostream>
    #include <string>
    using namespace std;
    // ...
    template<typename IteratorT>
    void DebugOut(IteratorT begin, IteratorT end)
    {
        string str(begin, end);
        cout << str << endl;
        /* Alternative */
        /*
        while(begin != end)
            cout << *begin++;
        cout << endl;
        */
    }
    
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;
                          //Hier kommt die Debug Ausgabe                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Mit diesem Konstrukt haben Sie die Möglichkeit sehr einfache Ausgaben zu erzeugen. Dies kann für Debug Informationen, die ausgegeben werden sollen, sehr nützlich sein.

    Zur Ergänzung möchte ich Ihnen nun noch ein Beispiel für Zahlenparser (ein Iterator) liefern.

    // ...
    
    template<typename IteratorT>
    void DebugOut(IteratorT only_one)
    {
        cout << only_one << endl;
        cout << only_one << " + 1 = " << only_one+1 << endl;
    }
    
    int main(int argc, char* argv)
    {
    
        rule<> count_val = int_p[&DebugOut<int>];
        if(parse("1", count_val).full)
            cout << "Supi";
    
    }
    

    Nun ist das Anzeigen von Daten zwar wie oben genannt in manchen Fällen nützlich, jedoch ist es fast immer der Fall dass man Informationen speichern muss. Spirit regelt diesen Vorgang über Funktionsobjekte.

    7 Funktionsobjekte, oder wie speichert man Dateien aus einem Parsevorgang
    Spirit bietet zum Speichern der Daten Funktionsobjekte.

    Das Grundgerüst eines Funktionsobjektes sieht je nach Anzahl der Iteratoren wie folgt aus.

    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //...
        }
    } 
    
    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT only_one) const
        {
            //...
        }
    }
    

    Als Ausgangspunkt dient erneut der .ini File Parser

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Für den Parser definieren wir uns nun ein Funktionsobjekt sowie die geignete Speicherstruktur

    //...
    
    // Die Speicherstruktur
    typedef struct
    {
       string section;
       string variable;
       string value;
    }
    INISTRUCT;
    
    // Wo bin ich in der Regelabarbeitung
    enum whereIAm {Section, Variable, Value};
    
    // Das Funktionsobjekt
    struct getData
    {
        //Konstruktor: Call by Reference um Objekte verändern zu können
        getData(vector<INISTRUCT>& ini_File_, enum whereIAm& switcher_, INISTRUCT& actItem_) : ini_File(ini_File_), switcher(switcher_), actItem(actItem_)
        {
    
        }
    
        //Funktionsoperator überladen
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //String erzeugen
            string str(begin, end);
            // wo bin ich
            switch(switcher)
            {
                case Section:
                actItem.section = str; //Section
                break;
    
                case Variable:
                actItem.variable = str; //Variable
                break;
    
                case Value:
                actItem.value = str; //Value
                ini_File.push_back(actItem); // Element sichern
                break;
            }
        }
    
        //Benötigte Variablen
        vector<INISTRUCT>& ini_File;
        enum whereIAm& switcher;
        INISTRUCT& actItem;
    };
    
    // ...
    

    Erklärung:
    Wir überladen den Funktionsoperator um dass Funktionsobjekt in der Regel aufrufen zu können. (Der Sicherungsmechanisums ist simpel gehalten und dient nur zur Veranschaulichung.) Call by Reference ist hier Pflicht um mit der "Aussenwelt" komunizieren zu können.

    Nun muss man das ganze Konstrukt noch aufrufen.

    //...
    
    int main(int argc, char *argv[])
    {
        // ...
    
        vector<INISTRUCT> ini_File;
        INISTRUCT actItem;
        enum whereIAm section = Section, variable = Variable, value = Value;
    
        // Erzeugen der Funktionsobjekte
        getData func_sec(ini_File, section, actItem);  //Section
        getData func_var(ini_File, variable, actItem); //Variable
        getData func_val(ini_File, value, actItem);    //Value
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);
        rule<> _VAR = *(anychar_p - space_p - chlit<char>('='));
    
                                  //Aufruf von func_var                 Aufruf von func_val
        rule<> _VAR_DECLARATION = _VAR[func_var] >> chlit<char>('=') >> _VALUE[func_val] >> *eol_p;
    
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']')) >> chlit<char>(']') >> *eol_p;
    
        //                Aufruf von func_sec
        rule<> _INI_ROW = _SECTION[func_sec] | _VAR_DECLARATION | eol_p;
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:
    Was ist passiert? Es werden drei Funktionsobjekte erzeugt (func_sec, func_var, func_val) und aufgerufen. Dies passiert jedesmal, wenn der Parser in der Regel zur Aufrufstelle kommt.

    ... [b]_SECTION[func_sec][/b] ...
    

    Zum Verständnis für Funktionsobjekte möchte ich auf
    http://cplus.kompf.de/artikel/functors.html und http://www.sgi.com/tech/stl/functors.html verweisen, da alleine die Erklärung von Funktionsobjekten
    wohl einen Artikel rechtfertigen würde.

    Hiermit endet meine Einführung in Spirit. Ich hoffe ich konnte einen kleinen Einblick in die Funktionsweise und Struktur von Spirit geben.

    8 Ausblick
    Spirit 1.8.3 bietet einige interessante Neuerungen. Unteranderem Storable Rules die es erlauben dynamisch die Regel zu erweitern.

    Bsp.:

    //...
    stored_rule<> hallo = strlit<const char*>("Hallo");
    rule<> welt = strlit<const char*>(" Welt!");
    
    hallo = hallo.copy() >> welt; 
    //...
    

    Diese Methode bietet sich hervorragend an, um beim Parsen eines C Source Files zum Beispiel die nativen Datentypen um typedef Datentypen zu erweitern.

    8 Weiterführede Link's
    Spirit 1.6.3
    Spirit 1.8.3
    http://www.codeproject.com/vcpp/stl/spirit_semantic_actions.asp



  • Wie siehts hier eigentlich aus?

    Ist weiteres Feedback gewünscht? Oder kanns in die Korrektur gehen? 🙂



  • Werde es mir heute abend durchlesen. Habe aber jetzt schonen einen Kommentar:
    kann man bitte in die Code-Beispiele nach ca. 80 oder 100 Zeichen einen Umbruch rein machen? Lässt sich auf meinem TFT mit 1024 Pixelbreite seeeeehr schlecht lesen. Weil ich immer nach rechts und wieder zurück scrollen muß. Man kann sich vorstellen, das das ziemlich nervig ist und den Lesefluß stört.



  • Ja, das wäre wohl gut. 🙂

    In der Schule hab ich die 80-Zeichen-Grenze zwar immer gehasst, aber wenigstens konnte man die Programme ohne Probleme drucken. 😃



  • Ok mach ich... 🙂

    Ansonsten bin ich gespannt auf die Anmerkungen... Bitte auch zum PDF Theoretische Informatik...

    Danke
    Tobi



  • 1 Grundlegendes
    Dieser Artikel soll Sie in die Welt von Boost::Spirit einführen. Boost::Spirit ist ein Parser Gernerator Framework, dass in C++ realisiert ist. Um die folgenden Beispiele complieren zu können ist Boost::Spirit ab der Version 1.6.3 zu verwenden. Desweiteren wird ein ISO C++ Compilier (gcc, Visual C++(ab Version 7.1), ...) benötigt

    Anmerkung: Probleme bereiten unteranderem MS Visual C++ 6.0 und Open Watcom 1.3.

    1.1 Von Dateien und anderen zu durchsuchenden Strukturen
    Ein klassisches Beispiel für die Verwendung eines Parsers ist die Dateiverarbeitung. Wie oft kommt es vor, dass man ein bestimmtes Format oder eine bestimmte Struktur zu durchsuchen hat und keine geeigneten Klassen oder Strukturen zur verfügung stehen.
    XML Dateien werden etwa anders verarbeitet als INI Dateien. Mathematische Eingaben sollen anders interpretiert werden als bestimmte Stringfolgen. All dass kennt man mittlerweile als Programmierer zur Genüge.
    Auf diesem Gebiet hat Boost::Spirit seine unverkennbaren Stärken. Durch die einfache Definition von Regeln ist in null kommma nichts ein Parser entwicklet, der die Arbeit von alleine erledigt. Ob dabei Zeilenweise durch eine Datei geparst wird, oder Mathematische Ausdrücke geparst werden spielt dabei keine große Rolle. Boost::Spirit kann in diese Richtung fast alles handhaben.

    2 Installation von Boost::Spirit
    Um das Framework von Boost::Spirit nutzen zu können ist es erforderlich, die Header Dateien bekannt zu machen. Am einfachsten ist es den Ordner boost aus dem Verzeichnis spirit-1.6.3\boost sowie aus dem Verzeichnis spirit-1.6.3\boost\miniboost in das Include Verzeichnis des Compiliers/der Entwicklungsumgebung zu kopieren.
    In ein Projekt müssen unbedingt folgende Zeilen eingebunden werden.

    // ...
    #include <boost/spirit.hpp>
    /*****************************************************************************/
    /* Die obige Include Zeile ist nur zu verwenden, falls die Includedateien    */
    /* Systemweit bekannt gemacht wurden...                                      */
    /* Falls sie sich im Projektverzeichnis befinden bitte                       */
    /* #include "boost/spirit.hpp"                                               */
    /* einbinden.                                                                */
    /*****************************************************************************/
    // ...
    using namespace boost::spirit
    

    2.1 Theoretische Informatik
    Für die folgenden Beispiele ist es vielleicht ganz interessant sich etwas in die Theoretische Informatik einzulesen. Deswegen hab ich ein kleines PDF erstellt:
    Klick mich

    3 Die Grundlagen für Parseaktionen im Framework
    Der Datentyp, der in den nächsten Beispielen immer wieder vorkommen wird, ist der rule<> Datentyp. Mit diesem Datentyp, baut man Parser auf. Spirit stellt von sich aus schon diverse Parser zur Verfügung von denen ich einige in diesem Kapitel
    vorstellen möchte.

    3.1 Die Literale strlit<> und chlit<>

    // ...
    rule<> ch_A = chlit<>('A');
    rule<> str_Hallo = strlit<>("Hallo");
    // ...
    

    Wie schon zu vermuten ist, werden hier ein Character Literal und ein String Literal erstellt. Es ist durchaus möglich, auch Variablen in der Regel anzugeben wie das folgende Beispiel zeigt.

    // ...
    string myString = "Test";
    rule<> str_Variabel = strlit<>(myString.c_str());  //char* übergeben
    // Alternativ
    rule<> str_Variable2 = strlit<string::const_iterator>(myString.begin(), myString.end());
    // ...
    

    3.2 Ranges
    Um einen Bereich abzudecken gibt es den range<> Parser.

    //...
    rule<> rng = range<>('A', 'Z');
    //...
    

    Die Regel rng deckt die Buchstaben von 'A' bis 'Z' ab.

    3.3 Zahlen
    Es gibt in Spirit diverse Zahlenparser. Einer der wichtigeren ist wohl der Integerparser.

    //...
    rule<> integer = int_p;
    //...
    

    Erlaubt eine beliebige Ganzzahl.

    3.4 Operatoren
    Es gibt in Spirit eine Reihe von Operatoren zum Verknüpfen von Regeln. Dazu gehören unteranderem

    <a> | <b>	// <a> oder <b> oder beide werden gematched
    <a> - <b>	// <a> ohne <b>
    <a> >> <b>	// <a> gefolgt von <b>
    !<a>		// 0 oder 1 mal <a>
    *<a>		// 0 oder beliebig oft mal <a>
    +<a>		// 1 oder beliebig oft mal <a>
    ~<a>		// nicht <a>
    

    Weiterführendes Material findet sich unter der Spirit Doku.

    4 Boost::Spirit an Hand eines .ini Datei Parsers
    Zur Erläuterung der Funktionsweise von Boost::Spirit werden wir uns im folgenden einen .ini Fileparser zusammenbauen. Dazu definieren wir das Aussehen einer .ini Datei.
    Definition:

    [Sektionsname{1}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    [Sektionsname{2}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    .
    .
    .
    [Sektionsname{n}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    

    Für diese Definition überlegen wir uns im folgenden Regeln, die wir in Spirit abbilden und die uns das Parsen erlauben.

    Es folgen die Regeln abgebildet in Spirit!

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          //[1]
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));               //[2]
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;//[3]
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION | _VAR_DECLARATION | eol_p;                 //[4]
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                   //[5] 
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:

    rule<> Variablename = Zuweisung
    

    Der rule<> Datentyp
    Der Variablentyp rule<> ist einer der wichtigsten Datentypen, wenn man mit Boost::Spirit in Kontakt kommt. Der rule<> Datentyp legt das "Aussehen" eines Parseausdruck fest.

    Zu [1]:
    Es wird die Regel _VALUE definiert. Wir legen fest, das _VALUE jedes Zeichen außer ein "End of Line" und ein "Eingabe Ende" beinahlten darf. Der * vor der Klammer ist der sogenannte "Kleene Star" und besagt, das 0 bis beliebig viele Zeichen vorkommen können.

    Zu [2]:
    Die Regel _VAR legt fest, dass for dem '=' mindestens ein Zeichen (+) stehen muss, dass kein '=' enthalten darf. Desweiteren dürfen keine Leerzeichen, keine Returns, keine Whitespaces und derartige dinge vorkommen(space_p Parser).

    Zu [3]:
    In der Regel _VAR_DECLARATION haben wir unser erstes "gefolgt von" Zeichen. Wir definieren das eine Variablen Declaration wie folgt aussieht. Die Regel _VAR gefolgt von (>>) einem Character Literal ('=') gefolgt von (>>) der Regel _VALUE gefolgt von (>>) einem möglichen "End of Line".

    Zu [4]:
    Die Regel _INI_ROW ist unsere sogenannte "Startregel". Sie ist es, die die parse Funktion bedient. In Ihr legen wir fest wie eine Zeile in der .ini File auszusehen hat. Entweder es kommt eine _SECTION, oder (|) eine _VAR_DECLARATION oder (|) ein "End of Line".

    Zu [5]:
    Die Funktion parse liefert ein Parseergebnis, auf dass wir später noch genauer eingehen werden. Vorerst ist es nur wichtig zu wissen, das die parse Funktion in unserem Beispiel zwei Argumente benötigt. Nämlich den zu parsenden Ausdruck und die "Startregel". Das .full zeigt uns, ob wir den Ausdruck komplett durchlaufen haben. Ist das geschehen geben wir "Valid" aus.

    Zusammenfassend zu diesem Beispiel:

    -> Der Datentyp rule<> als einer der wichtigsten Datentypen in Boost::Spirit.
    -> Es gibt verschiedene vorgefertigte Parser wie eol_p, space_p, anychar_p ...
    -> Wichtige Operatoren sind: *, +, |, >>
    -> Die parse Funktion übernimmt die Arbeit und liefert als Ergebnis einen "true" oder "false" Wert.

    5 Die Parse Funktion
    Wie im obigen Beispiel gesehen, ist es die parse Funktion die den eigentlichen Parsevorgang übernimmt. Diese Funktion liefert in Wirklichkeit mehr als nur einen boolsche Wert. Der Rückgabewert ist eine Variable vom Typ parse_info
    Die Struktur enthält folgende Membervariablen:

    -> stop
    -> hit
    -> full
    -> length

    Über diese Variablen lassen sich einige Informationen extrahieren, die Aufschluss über den Parsevorgang geben.

    Über stop findet man die Position im geparsten Ausdruck wo aufgehört wurde zu parsen.

    hit ist true oder false. Sprich entweder wurde mindestens ein Teilausdruck geparst oder es wurde kein einziger Ausdruck geparst.

    full ist ebenfalls true oder false. Es sagt aus ob der ganze Parseausdruck geparst wurde oder nicht.

    length gibt die Anzahl der Zeichen an, die geparst wurden.

    Bsp:

    // ...
    
    // Eine Regel, die eine Liste von Zahlen getrennt durch Kommas liest
    rule<> r = int_p >> *(chlit<char>(',') >> int_p);
    
    // Das ParseInfo Objekt
    parse_info<> pI;
    
    // Parsen eines ausdrucks, der nicht nur Zahlen entält
    pI = parse("1,2,3,a,b,0", r);
    
    // Ausgabe der Werte von ParseInfo
    std::cout << pI.stop << std::endl 
              << pI.full << std::endl 
              << pI.hit << std::endl 
              << pI.length << std::endl;
    
    // ...
    

    Die Ausgabe des Programms

    ,a,b,0      // Hier wurde aufgehört zu parsen
    0           // Ausdruck nicht vollständig geparst
    1           // Teilausdruck wurde erkannt
    5           // 5 Zeichen wurden geparst
    

    Mit diesen Angaben kann man unter anderem einschränken, wo der zu parsende Ausdruck "fehlerhaft" war.

    6 Anzeigen von Daten aus dem Parsevorgang mit Funktionen
    Nun ist es ja gut und schön wenn man weiß, dass eine zu parsender Ausdruck den Regeln entspricht. Aber wie an die Daten kommen, die in dem Ausdruck vorhanden sind.

    Dafür gibt es in Boost::Spirit unteranderem Funktionen die im Grundgerüst wie folgt definiert sind.

    template<IteratorT>
    void <funktionsname>(IteratorT begin, IteratorT end)
    {
      //Verarbeitung der Iteratoren
    }
    
    // oder falls nur ein Iterator zur Verfügung steht z.B bei Zahlenparsern von Spirit
    void <funktionsname>(IteratorT begin)
    {
      //Verarbeitung des Iterators
    }
    

    Als Beispiel nehmen wir erneut den .ini File Parser. Es wird im folgendem Beispiel eine Funktion definiert, die uns beispielhaft die Sektionen ausgibt.

    #include <iostream>
    #include <string>
    using namespace std;
    // ...
    template<typename IteratorT>
    void DebugOut(IteratorT begin, IteratorT end)
    {
        string str(begin, end);
        cout << str << endl;
        /* Alternative */
        /*
        while(begin != end)
            cout << *begin++;
        cout << endl;
        */
    }
    
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;
                          //Hier kommt die Debug Ausgabe                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Mit diesem Konstrukt haben Sie die Möglichkeit sehr einfache Ausgaben zu erzeugen. Dies kann für Debug Informationen, die ausgegeben werden sollen, sehr nützlich sein.

    Zur Ergänzung möchte ich Ihnen nun noch ein Beispiel für Zahlenparser (ein Iterator) liefern.

    // ...
    
    template<typename IteratorT>
    void DebugOut(IteratorT only_one)
    {
        cout << only_one << endl;
        cout << only_one << " + 1 = " << only_one+1 << endl;
    }
    
    int main(int argc, char* argv)
    {
    
        rule<> count_val = int_p[&DebugOut<int>];
        if(parse("1", count_val).full)
            cout << "Supi";
    
    }
    

    Nun ist das Anzeigen von Daten zwar wie oben genannt in manchen Fällen nützlich, jedoch ist es fast immer der Fall dass man Informationen speichern muss. Spirit regelt diesen Vorgang über Funktionsobjekte.

    7 Funktionsobjekte, oder wie speichert man Dateien aus einem Parsevorgang
    Spirit bietet zum Speichern der Daten Funktionsobjekte.

    Das Grundgerüst eines Funktionsobjektes sieht je nach Anzahl der Iteratoren wie folgt aus.

    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //...
        }
    } 
    
    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT only_one) const
        {
            //...
        }
    }
    

    Als Ausgangspunkt dient erneut der .ini File Parser

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Für den Parser definieren wir uns nun ein Funktionsobjekt sowie die geignete Speicherstruktur

    //...
    
    // Die Speicherstruktur
    typedef struct
    {
       string section;
       string variable;
       string value;
    }
    INISTRUCT;
    
    // Wo bin ich in der Regelabarbeitung
    enum whereIAm {Section, Variable, Value};
    
    // Das Funktionsobjekt
    struct getData
    {
        //Konstruktor: Call by Reference um Objekte verändern zu können
        getData(vector<INISTRUCT>& ini_File_, enum whereIAm& switcher_, INISTRUCT& actItem_) : 
                                   ini_File(ini_File_), switcher(switcher_), actItem(actItem_)
        {
    
        }
    
        //Funktionsoperator überladen
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //String erzeugen
            string str(begin, end);
            // wo bin ich
            switch(switcher)
            {
                case Section:
                actItem.section = str; //Section
                break;
    
                case Variable:
                actItem.variable = str; //Variable
                break;
    
                case Value:
                actItem.value = str; //Value
                ini_File.push_back(actItem); // Element sichern
                break;
            }
        }
    
        //Benötigte Variablen
        vector<INISTRUCT>& ini_File;
        enum whereIAm& switcher;
        INISTRUCT& actItem;
    };
    
    // ...
    

    Erklärung:
    Wir überladen den Funktionsoperator um dass Funktionsobjekt in der Regel aufrufen zu können. (Der Sicherungsmechanisums ist simpel gehalten und dient nur zur Veranschaulichung.) Call by Reference ist hier Pflicht um mit der "Aussenwelt" komunizieren zu können.

    Nun muss man das ganze Konstrukt noch aufrufen.

    //...
    
    int main(int argc, char *argv[])
    {
        // ...
    
        vector<INISTRUCT> ini_File;
        INISTRUCT actItem;
        enum whereIAm section = Section, variable = Variable, value = Value;
    
        // Erzeugen der Funktionsobjekte
        getData func_sec(ini_File, section, actItem);  //Section
        getData func_var(ini_File, variable, actItem); //Variable
        getData func_val(ini_File, value, actItem);    //Value
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);
        rule<> _VAR = *(anychar_p - space_p - chlit<char>('='));
    
                                  //Aufruf von func_var                 Aufruf von func_val
        rule<> _VAR_DECLARATION = _VAR[func_var] >> chlit<char>('=') >> _VALUE[func_val] >> *eol_p;
    
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']')) >> chlit<char>(']') 
                                           >> *eol_p;
    
        //                Aufruf von func_sec
        rule<> _INI_ROW = _SECTION[func_sec] | _VAR_DECLARATION | eol_p;
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:
    Was ist passiert? Es werden drei Funktionsobjekte erzeugt (func_sec, func_var, func_val) und aufgerufen. Dies passiert jedesmal, wenn der Parser in der Regel zur Aufrufstelle kommt.

    ... [b]_SECTION[func_sec][/b] ...
    

    Zum Verständnis für Funktionsobjekte möchte ich auf
    http://cplus.kompf.de/artikel/functors.html und http://www.sgi.com/tech/stl/functors.html verweisen, da alleine die Erklärung von Funktionsobjekten
    wohl einen Artikel rechtfertigen würde.

    Hiermit endet meine Einführung in Spirit. Ich hoffe ich konnte einen kleinen Einblick in die Funktionsweise und Struktur von Spirit geben.

    8 Ausblick
    Spirit 1.8.3 bietet einige interessante Neuerungen. Unteranderem Storable Rules die es erlauben dynamisch die Regel zu erweitern.

    Bsp.:

    //...
    stored_rule<> hallo = strlit<const char*>("Hallo");
    rule<> welt = strlit<const char*>(" Welt!");
    
    hallo = hallo.copy() >> welt; 
    //...
    

    Diese Methode bietet sich hervorragend an, um beim Parsen eines C Source Files zum Beispiel die nativen Datentypen um typedef Datentypen zu erweitern.

    8 Weiterführede Link's
    Spirit 1.6.3
    Spirit 1.8.3
    http://www.codeproject.com/vcpp/stl/spirit_semantic_actions.asp



  • Hat noch irgendwer Anmerkungen oder kann ich den Artikel in die Rechtschreibkorrektur schicken?



  • Es gehört zwar schon zur Rechtschreibkorrektur, aber ich habe gesehen, dass du bei jeder Überschrift die folgende Leerzeile vergessen hast.



  • 1 Grundlegendes

    Dieser Artikel soll Sie in die Welt von Boost::Spirit einführen. Boost::Spirit ist ein Parser Gernerator Framework, dass in C++ realisiert ist. Um die folgenden Beispiele complieren zu können ist Boost::Spirit ab der Version 1.6.3 zu verwenden. Desweiteren wird ein ISO C++ Compilier (gcc, Visual C++(ab Version 7.1), ...) benötigt

    Anmerkung: Probleme bereiten unteranderem MS Visual C++ 6.0 und Open Watcom 1.3.

    1.1 Von Dateien und anderen zu durchsuchenden Strukturen

    Ein klassisches Beispiel für die Verwendung eines Parsers ist die Dateiverarbeitung. Wie oft kommt es vor, dass man ein bestimmtes Format oder eine bestimmte Struktur zu durchsuchen hat und keine geeigneten Klassen oder Strukturen zur verfügung stehen.
    XML Dateien werden etwa anders verarbeitet als INI Dateien. Mathematische Eingaben sollen anders interpretiert werden als bestimmte Stringfolgen. All dass kennt man mittlerweile als Programmierer zur Genüge.
    Auf diesem Gebiet hat Boost::Spirit seine unverkennbaren Stärken. Durch die einfache Definition von Regeln ist in null kommma nichts ein Parser entwicklet, der die Arbeit von alleine erledigt. Ob dabei Zeilenweise durch eine Datei geparst wird, oder Mathematische Ausdrücke geparst werden spielt dabei keine große Rolle. Boost::Spirit kann in diese Richtung fast alles handhaben.

    2 Installation von Boost::Spirit

    Um das Framework von Boost::Spirit nutzen zu können ist es erforderlich, die Header Dateien bekannt zu machen. Am einfachsten ist es den Ordner boost aus dem Verzeichnis spirit-1.6.3\boost sowie aus dem Verzeichnis spirit-1.6.3\boost\miniboost in das Include Verzeichnis des Compiliers/der Entwicklungsumgebung zu kopieren.
    In ein Projekt müssen unbedingt folgende Zeilen eingebunden werden.

    // ...
    #include <boost/spirit.hpp>
    /*****************************************************************************/
    /* Die obige Include Zeile ist nur zu verwenden, falls die Includedateien    */
    /* Systemweit bekannt gemacht wurden...                                      */
    /* Falls sie sich im Projektverzeichnis befinden bitte                       */
    /* #include "boost/spirit.hpp"                                               */
    /* einbinden.                                                                */
    /*****************************************************************************/
    // ...
    using namespace boost::spirit
    

    2.1 Theoretische Informatik

    Für die folgenden Beispiele ist es vielleicht ganz interessant sich etwas in die Theoretische Informatik einzulesen. Deswegen hab ich ein kleines PDF erstellt:
    Klick mich

    3 Die Grundlagen für Parseaktionen im Framework

    Der Datentyp, der in den nächsten Beispielen immer wieder vorkommen wird, ist der rule<> Datentyp. Mit diesem Datentyp, baut man Parser auf. Spirit stellt von sich aus schon diverse Parser zur Verfügung von denen ich einige in diesem Kapitel
    vorstellen möchte.

    3.1 Die Literale strlit<> und chlit<>

    // ...
    rule<> ch_A = chlit<>('A');
    rule<> str_Hallo = strlit<>("Hallo");
    // ...
    

    Wie schon zu vermuten ist, werden hier ein Character Literal und ein String Literal erstellt. Es ist durchaus möglich, auch Variablen in der Regel anzugeben wie das folgende Beispiel zeigt.

    // ...
    string myString = "Test";
    rule<> str_Variabel = strlit<>(myString.c_str());  //char* übergeben
    // Alternativ
    rule<> str_Variable2 = strlit<string::const_iterator>(myString.begin(), myString.end());
    // ...
    

    3.2 Ranges

    Um einen Bereich abzudecken gibt es den range<> Parser.

    //...
    rule<> rng = range<>('A', 'Z');
    //...
    

    Die Regel rng deckt die Buchstaben von 'A' bis 'Z' ab.

    3.3 Zahlen

    Es gibt in Spirit diverse Zahlenparser. Einer der wichtigeren ist wohl der Integerparser.

    //...
    rule<> integer = int_p;
    //...
    

    Erlaubt eine beliebige Ganzzahl.

    3.4 Operatoren

    Es gibt in Spirit eine Reihe von Operatoren zum Verknüpfen von Regeln. Dazu gehören unteranderem

    <a> | <b>	// <a> oder <b> oder beide werden gematched
    <a> - <b>	// <a> ohne <b>
    <a> >> <b>	// <a> gefolgt von <b>
    !<a>		// 0 oder 1 mal <a>
    *<a>		// 0 oder beliebig oft mal <a>
    +<a>		// 1 oder beliebig oft mal <a>
    ~<a>		// nicht <a>
    

    Weiterführendes Material findet sich unter der Spirit Doku.

    4 Boost::Spirit an Hand eines .ini Datei Parsers

    Zur Erläuterung der Funktionsweise von Boost::Spirit werden wir uns im folgenden einen .ini Fileparser zusammenbauen. Dazu definieren wir das Aussehen einer .ini Datei.
    Definition:

    [Sektionsname{1}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    [Sektionsname{2}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    .
    .
    .
    [Sektionsname{n}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    

    Für diese Definition überlegen wir uns im folgenden Regeln, die wir in Spirit abbilden und die uns das Parsen erlauben.

    Es folgen die Regeln abgebildet in Spirit!

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          //[1]
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));               //[2]
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;//[3]
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION | _VAR_DECLARATION | eol_p;                 //[4]
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                   //[5] 
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:

    rule<> Variablename = Zuweisung
    

    Der rule<> Datentyp
    Der Variablentyp rule<> ist einer der wichtigsten Datentypen, wenn man mit Boost::Spirit in Kontakt kommt. Der rule<> Datentyp legt das "Aussehen" eines Parseausdruck fest.

    Zu [1]:
    Es wird die Regel _VALUE definiert. Wir legen fest, das _VALUE jedes Zeichen außer ein "End of Line" und ein "Eingabe Ende" beinahlten darf. Der * vor der Klammer ist der sogenannte "Kleene Star" und besagt, das 0 bis beliebig viele Zeichen vorkommen können.

    Zu [2]:
    Die Regel _VAR legt fest, dass for dem '=' mindestens ein Zeichen (+) stehen muss, dass kein '=' enthalten darf. Desweiteren dürfen keine Leerzeichen, keine Returns, keine Whitespaces und derartige dinge vorkommen(space_p Parser).

    Zu [3]:
    In der Regel _VAR_DECLARATION haben wir unser erstes "gefolgt von" Zeichen. Wir definieren das eine Variablen Declaration wie folgt aussieht. Die Regel _VAR gefolgt von (>>) einem Character Literal ('=') gefolgt von (>>) der Regel _VALUE gefolgt von (>>) einem möglichen "End of Line".

    Zu [4]:
    Die Regel _INI_ROW ist unsere sogenannte "Startregel". Sie ist es, die die parse Funktion bedient. In Ihr legen wir fest wie eine Zeile in der .ini File auszusehen hat. Entweder es kommt eine _SECTION, oder (|) eine _VAR_DECLARATION oder (|) ein "End of Line".

    Zu [5]:
    Die Funktion parse liefert ein Parseergebnis, auf dass wir später noch genauer eingehen werden. Vorerst ist es nur wichtig zu wissen, das die parse Funktion in unserem Beispiel zwei Argumente benötigt. Nämlich den zu parsenden Ausdruck und die "Startregel". Das .full zeigt uns, ob wir den Ausdruck komplett durchlaufen haben. Ist das geschehen geben wir "Valid" aus.

    Zusammenfassend zu diesem Beispiel:

    -> Der Datentyp rule<> als einer der wichtigsten Datentypen in Boost::Spirit.
    -> Es gibt verschiedene vorgefertigte Parser wie eol_p, space_p, anychar_p ...
    -> Wichtige Operatoren sind: *, +, |, >>
    -> Die parse Funktion übernimmt die Arbeit und liefert als Ergebnis einen "true" oder "false" Wert.

    5 Die Parse Funktion

    Wie im obigen Beispiel gesehen, ist es die parse Funktion die den eigentlichen Parsevorgang übernimmt. Diese Funktion liefert in Wirklichkeit mehr als nur einen boolsche Wert. Der Rückgabewert ist eine Variable vom Typ parse_info
    Die Struktur enthält folgende Membervariablen:

    -> stop
    -> hit
    -> full
    -> length

    Über diese Variablen lassen sich einige Informationen extrahieren, die Aufschluss über den Parsevorgang geben.

    Über stop findet man die Position im geparsten Ausdruck wo aufgehört wurde zu parsen.

    hit ist true oder false. Sprich entweder wurde mindestens ein Teilausdruck geparst oder es wurde kein einziger Ausdruck geparst.

    full ist ebenfalls true oder false. Es sagt aus ob der ganze Parseausdruck geparst wurde oder nicht.

    length gibt die Anzahl der Zeichen an, die geparst wurden.

    Bsp:

    // ...
    
    // Eine Regel, die eine Liste von Zahlen getrennt durch Kommas liest
    rule<> r = int_p >> *(chlit<char>(',') >> int_p);
    
    // Das ParseInfo Objekt
    parse_info<> pI;
    
    // Parsen eines ausdrucks, der nicht nur Zahlen entält
    pI = parse("1,2,3,a,b,0", r);
    
    // Ausgabe der Werte von ParseInfo
    std::cout << pI.stop << std::endl 
              << pI.full << std::endl 
              << pI.hit << std::endl 
              << pI.length << std::endl;
    
    // ...
    

    Die Ausgabe des Programms

    ,a,b,0      // Hier wurde aufgehört zu parsen
    0           // Ausdruck nicht vollständig geparst
    1           // Teilausdruck wurde erkannt
    5           // 5 Zeichen wurden geparst
    

    Mit diesen Angaben kann man unter anderem einschränken, wo der zu parsende Ausdruck "fehlerhaft" war.

    6 Anzeigen von Daten aus dem Parsevorgang mit Funktionen

    Nun ist es ja gut und schön wenn man weiß, dass eine zu parsender Ausdruck den Regeln entspricht. Aber wie an die Daten kommen, die in dem Ausdruck vorhanden sind.

    Dafür gibt es in Boost::Spirit unteranderem Funktionen die im Grundgerüst wie folgt definiert sind.

    template<IteratorT>
    void <funktionsname>(IteratorT begin, IteratorT end)
    {
      //Verarbeitung der Iteratoren
    }
    
    // oder falls nur ein Iterator zur Verfügung steht z.B bei Zahlenparsern von Spirit
    void <funktionsname>(IteratorT begin)
    {
      //Verarbeitung des Iterators
    }
    

    Als Beispiel nehmen wir erneut den .ini File Parser. Es wird im folgendem Beispiel eine Funktion definiert, die uns beispielhaft die Sektionen ausgibt.

    #include <iostream>
    #include <string>
    using namespace std;
    // ...
    template<typename IteratorT>
    void DebugOut(IteratorT begin, IteratorT end)
    {
        string str(begin, end);
        cout << str << endl;
        /* Alternative */
        /*
        while(begin != end)
            cout << *begin++;
        cout << endl;
        */
    }
    
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;
                          //Hier kommt die Debug Ausgabe                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Mit diesem Konstrukt haben Sie die Möglichkeit sehr einfache Ausgaben zu erzeugen. Dies kann für Debug Informationen, die ausgegeben werden sollen, sehr nützlich sein.

    Zur Ergänzung möchte ich Ihnen nun noch ein Beispiel für Zahlenparser (ein Iterator) liefern.

    // ...
    
    template<typename IteratorT>
    void DebugOut(IteratorT only_one)
    {
        cout << only_one << endl;
        cout << only_one << " + 1 = " << only_one+1 << endl;
    }
    
    int main(int argc, char* argv)
    {
    
        rule<> count_val = int_p[&DebugOut<int>];
        if(parse("1", count_val).full)
            cout << "Supi";
    
    }
    

    Nun ist das Anzeigen von Daten zwar wie oben genannt in manchen Fällen nützlich, jedoch ist es fast immer der Fall dass man Informationen speichern muss. Spirit regelt diesen Vorgang über Funktionsobjekte.

    7 Funktionsobjekte, oder wie speichert man Dateien aus einem Parsevorgang

    Spirit bietet zum Speichern der Daten Funktionsobjekte.

    Das Grundgerüst eines Funktionsobjektes sieht je nach Anzahl der Iteratoren wie folgt aus.

    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //...
        }
    } 
    
    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT only_one) const
        {
            //...
        }
    }
    

    Als Ausgangspunkt dient erneut der .ini File Parser

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Für den Parser definieren wir uns nun ein Funktionsobjekt sowie die geignete Speicherstruktur

    //...
    
    // Die Speicherstruktur
    typedef struct
    {
       string section;
       string variable;
       string value;
    }
    INISTRUCT;
    
    // Wo bin ich in der Regelabarbeitung
    enum whereIAm {Section, Variable, Value};
    
    // Das Funktionsobjekt
    struct getData
    {
        //Konstruktor: Call by Reference um Objekte verändern zu können
        getData(vector<INISTRUCT>& ini_File_, enum whereIAm& switcher_, INISTRUCT& actItem_) : 
                                   ini_File(ini_File_), switcher(switcher_), actItem(actItem_)
        {
    
        }
    
        //Funktionsoperator überladen
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //String erzeugen
            string str(begin, end);
            // wo bin ich
            switch(switcher)
            {
                case Section:
                actItem.section = str; //Section
                break;
    
                case Variable:
                actItem.variable = str; //Variable
                break;
    
                case Value:
                actItem.value = str; //Value
                ini_File.push_back(actItem); // Element sichern
                break;
            }
        }
    
        //Benötigte Variablen
        vector<INISTRUCT>& ini_File;
        enum whereIAm& switcher;
        INISTRUCT& actItem;
    };
    
    // ...
    

    Erklärung:
    Wir überladen den Funktionsoperator um dass Funktionsobjekt in der Regel aufrufen zu können. (Der Sicherungsmechanisums ist simpel gehalten und dient nur zur Veranschaulichung.) Call by Reference ist hier Pflicht um mit der "Aussenwelt" komunizieren zu können.

    Nun muss man das ganze Konstrukt noch aufrufen.

    //...
    
    int main(int argc, char *argv[])
    {
        // ...
    
        vector<INISTRUCT> ini_File;
        INISTRUCT actItem;
        enum whereIAm section = Section, variable = Variable, value = Value;
    
        // Erzeugen der Funktionsobjekte
        getData func_sec(ini_File, section, actItem);  //Section
        getData func_var(ini_File, variable, actItem); //Variable
        getData func_val(ini_File, value, actItem);    //Value
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);
        rule<> _VAR = *(anychar_p - space_p - chlit<char>('='));
    
                                  //Aufruf von func_var                 Aufruf von func_val
        rule<> _VAR_DECLARATION = _VAR[func_var] >> chlit<char>('=') >> _VALUE[func_val] >> *eol_p;
    
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']')) >> chlit<char>(']') 
                                           >> *eol_p;
    
        //                Aufruf von func_sec
        rule<> _INI_ROW = _SECTION[func_sec] | _VAR_DECLARATION | eol_p;
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:
    Was ist passiert? Es werden drei Funktionsobjekte erzeugt (func_sec, func_var, func_val) und aufgerufen. Dies passiert jedesmal, wenn der Parser in der Regel zur Aufrufstelle kommt.

    ... [b]_SECTION[func_sec][/b] ...
    

    Zum Verständnis für Funktionsobjekte möchte ich auf
    http://cplus.kompf.de/artikel/functors.html und http://www.sgi.com/tech/stl/functors.html verweisen, da alleine die Erklärung von Funktionsobjekten
    wohl einen Artikel rechtfertigen würde.

    Hiermit endet meine Einführung in Spirit. Ich hoffe ich konnte einen kleinen Einblick in die Funktionsweise und Struktur von Spirit geben.

    8 Ausblick

    Spirit 1.8.3 bietet einige interessante Neuerungen. Unteranderem Storable Rules die es erlauben dynamisch die Regel zu erweitern.

    Bsp.:

    //...
    stored_rule<> hallo = strlit<const char*>("Hallo");
    rule<> welt = strlit<const char*>(" Welt!");
    
    hallo = hallo.copy() >> welt; 
    //...
    

    Diese Methode bietet sich hervorragend an, um beim Parsen eines C Source Files zum Beispiel die nativen Datentypen um typedef Datentypen zu erweitern.

    9 Weiterführede Link's

    Spirit 1.6.3
    Spirit 1.8.3
    http://www.codeproject.com/vcpp/stl/spirit_semantic_actions.asp



  • Hallo zusammen,

    ich werde heute Abend den Text von meinem PDF File als text Datei hochladen und hier verlinken. Wer Zeit und Lust hat, könnte mal drüber lesen und Rechtschreibfehler (die garantiert vorhanden sind) korregieren.

    Danke und Gruss
    Tobi



  • 1 Grundlegendes

    Dieser Artikel soll Sie in die Welt von Boost::Spirit einführen. Boost::Spirit ist ein Parser Generator Framework, das in C++ realisiert ist. Um die folgenden Beispiele complieren zu können, ist Boost::Spirit ab der Version 1.6.3 zu verwenden. Des Weiteren wird ein ISO C++-Compilier (gcc, Visual C++ (ab Version 7.1), ...) benötigt.

    Anmerkung: Probleme bereiten unter anderem MS Visual C++ 6.0 und Open Watcom 1.3.

    1.1 Von Dateien und anderen zu durchsuchenden Strukturen

    Ein klassisches Beispiel für die Verwendung eines Parsers ist die Dateiverarbeitung. Wie oft kommt es vor, dass man ein bestimmtes Format oder eine bestimmte Struktur zu durchsuchen hat und keine geeigneten Klassen oder Strukturen zur verfügung stehen.
    XML-Dateien werden etwa anders verarbeitet als INI-Dateien. Mathematische Eingaben sollen anders interpretiert werden als bestimmte Stringfolgen. All das kennt man mittlerweile als Programmierer zur Genüge.
    Auf diesem Gebiet hat Boost::Spirit seine unverkennbaren Stärken. Durch die einfache Definition von Regeln ist in null Komma nichts ein Parser entwicklet, der die Arbeit von alleine erledigt. Ob dabei zeilenweise durch eine Datei geparst wird oder mathematische Ausdrücke geparst werden, spielt dabei keine große Rolle. Boost::Spirit kann in diese Richtung fast alles handhaben.

    2 Installation von Boost::Spirit

    Um das Framework von Boost::Spirit nutzen zu können, ist es erforderlich, die Header-Dateien bekannt zu machen. Am einfachsten ist es, den Ordner boost aus dem Verzeichnis spirit-1.6.3\boost sowie aus dem Verzeichnis spirit-1.6.3\boost\miniboost in das Include-Verzeichnis des Kompilers / der Entwicklungsumgebung zu kopieren.
    In ein Projekt müssen unbedingt folgende Zeilen eingebunden werden.

    // ...
    #include <boost/spirit.hpp>
    /*****************************************************************************/
    /* Die obige Include Zeile ist nur zu verwenden, falls die Include-Dateien   */
    /* systemweit bekannt gemacht wurden...                                      */
    /* Falls Sie sich im Projektverzeichnis befinden, bitte                      */
    /* #include "boost/spirit.hpp"                                               */
    /* einbinden.                                                                */
    /*****************************************************************************/
    // ...
    using namespace boost::spirit
    

    2.1 Theoretische Informatik

    Für die folgenden Beispiele ist es vielleicht ganz interessant, sich etwas in die theoretische Informatik einzulesen. Deswegen hab ich ein kleines PDF erstellt:
    Klick mich

    3 Die Grundlagen für Parseaktionen im Framework

    Der Datentyp, der in den nächsten Beispielen immer wieder vorkommen wird, ist der rule<>-Datentyp. Mit diesem Datentyp baut man Parser auf. Spirit stellt von sich aus schon diverse Parser zur Verfügung, von denen ich einige in diesem Kapitel vorstellen möchte.

    3.1 Die Literale strlit<> und chlit<>

    // ...
    rule<> ch_A = chlit<>('A');
    rule<> str_Hallo = strlit<>("Hallo");
    // ...
    

    Wie schon zu vermuten ist, werden hier ein Character Literal und ein String Literal erstellt. Es ist durchaus möglich, auch Variablen in der Regel anzugeben wie das folgende Beispiel zeigt.

    // ...
    string myString = "Test";
    rule<> str_Variabel = strlit<>(myString.c_str());  //char* übergeben
    // Alternativ
    rule<> str_Variable2 = strlit<string::const_iterator>(myString.begin(), myString.end());
    // ...
    

    3.2 Ranges

    Um einen Bereich abzudecken, gibt es den range<>-Parser.

    //...
    rule<> rng = range<>('A', 'Z');
    //...
    

    Die Regel rng deckt die Buchstaben von 'A' bis 'Z' ab.

    3.3 Zahlen

    Es gibt in Spirit diverse Zahlenparser. Einer der wichtigeren ist wohl der Integerparser.

    //...
    rule<> integer = int_p;
    //...
    

    Erlaubt eine beliebige Ganzzahl.

    3.4 Operatoren

    Es gibt in Spirit eine Reihe von Operatoren zum Verknüpfen von Regeln. Dazu gehören unter anderem

    <a> | <b>	// <a> oder <b> oder beide werden gematched
    <a> - <b>	// <a> ohne <b>
    <a> >> <b>	// <a> gefolgt von <b>
    !<a>		// 0 oder 1 mal <a>
    *<a>		// 0 oder beliebig oft mal <a>
    +<a>		// 1 oder beliebig oft mal <a>
    ~<a>		// nicht <a>
    

    Weiterführendes Material findet sich unter der Spirit Doku.

    4 Boost::Spirit anhand eines INI-Datei-Parsers

    Zur Erläuterung der Funktionsweise von Boost::Spirit werden wir uns im Folgenden einen INI-Fileparser zusammenbauen. Dazu definieren wir das Aussehen einer INI-Datei.
    Definition:

    [Sektionsname{1}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    [Sektionsname{2}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    .
    .
    .
    [Sektionsname{n}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    

    Für diese Definition überlegen wir uns im Folgenden Regeln, die wir in Spirit abbilden und die uns das Parsen erlauben.

    Es folgen die Regeln abgebildet in Spirit!

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          //[1]
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));               //[2]
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;//[3]
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION | _VAR_DECLARATION | eol_p;                 //[4]
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                   //[5] 
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:

    rule<> Variablename = Zuweisung
    

    Der rule<> Datentyp
    Der Variablentyp rule<> ist einer der wichtigsten Datentypen, wenn man mit Boost::Spirit in Kontakt kommt. Der rule<>-Datentyp legt das "Aussehen" eines Parseausdruck fest.

    Zu [1]:
    Es wird die Regel _VALUE definiert. Wir legen fest, das _VALUE jedes Zeichen außer ein "End of Line" und ein "Eingabe Ende" beinhalten darf. Der * vor der Klammer ist der so genannte "Kleene Star" und besagt, das 0 bis beliebig viele Zeichen vorkommen können.

    Zu [2]:
    Die Regel _VAR legt fest, dass vor dem '=' mindestens ein Zeichen (+) stehen muss, das kein '=' enthalten darf. Des Weiteren dürfen keine Leerzeichen, keine Returns, keine Whitespaces und derartige Dinge vorkommen (space_p-Parser).

    Zu [3]:
    In der Regel _VAR_DECLARATION haben wir unser erstes "gefolgt von"-Zeichen. Wir definieren, dass eine Variablen-Declaration wie folgt aussieht. Die Regel _VAR gefolgt von (>>) einem Character Literal ('=') gefolgt von (>>) der Regel _VALUE gefolgt von (>>) einem möglichen "End of Line".

    Zu [4]:
    Die Regel _INI_ROW ist unsere so genannte "Startregel". Sie ist es, die die Parse-Funktion bedient. In ihr legen wir fest, wie eine Zeile in der INI-File auszusehen hat. Entweder es kommt eine _SECTION oder (|) eine _VAR_DECLARATION oder (|) ein "End of Line".

    Zu [5]:
    Die Funktion parse liefert ein Parse-Ergebnis, auf das wir später noch genauer eingehen werden. Vorerst ist es nur wichtig zu wissen, dass die parse-Funktion in unserem Beispiel zwei Argumente benötigt. Nämlich den zu parsenden Ausdruck und die "Startregel". Das .full zeigt uns, ob wir den Ausdruck komplett durchlaufen haben. Ist das geschehen, geben wir "Valid" aus.

    Zusammenfassend zu diesem Beispiel:

    -> Der Datentyp rule<> als einer der wichtigsten Datentypen in Boost::Spirit.
    -> Es gibt verschiedene vorgefertigte Parser wie eol_p, space_p, anychar_p ...
    -> Wichtige Operatoren sind: *, +, |, >>
    -> Die parse-Funktion übernimmt die Arbeit und liefert als Ergebnis einen "true" oder "false" Wert.

    5 Die Parse-Funktion

    Wie im obigen Beispiel gesehen, ist es die parse-Funktion, die den eigentlichen Parsevorgang übernimmt. Diese Funktion liefert in Wirklichkeit mehr als nur einen boolsche Wert. Der Rückgabewert ist eine Variable vom Typ parse_info
    Die Struktur enthält folgende Membervariablen:

    -> stop
    -> hit
    -> full
    -> length

    Über diese Variablen lassen sich einige Informationen extrahieren, die Aufschluss über den Parsevorgang geben.

    Über stop findet man die Position im geparsten Ausdruck, wo aufgehört wurde zu parsen.

    hit ist true oder false. Sprich entweder wurde mindestens ein Teilausdruck geparst oder es wurde kein einziger Ausdruck geparst.

    full ist ebenfalls true oder false. Es sagt aus, ob der ganze Parseausdruck geparst wurde oder nicht.

    length gibt die Anzahl der Zeichen an, die geparst wurden.

    Bsp:

    // ...
    
    // Eine Regel, die eine Liste von Zahlen getrennt durch Kommas liest
    rule<> r = int_p >> *(chlit<char>(',') >> int_p);
    
    // Das ParseInfo-Objekt
    parse_info<> pI;
    
    // Parsen eines Ausdrucks, der nicht nur Zahlen enthält
    pI = parse("1,2,3,a,b,0", r);
    
    // Ausgabe der Werte von ParseInfo
    std::cout << pI.stop << std::endl 
              << pI.full << std::endl 
              << pI.hit << std::endl 
              << pI.length << std::endl;
    
    // ...
    

    Die Ausgabe des Programms

    ,a,b,0      // Hier wurde aufgehört zu parsen
    0           // Ausdruck nicht vollständig geparst
    1           // Teilausdruck wurde erkannt
    5           // 5 Zeichen wurden geparst
    

    Mit diesen Angaben kann man unter anderem einschränken, wo der zu parsende Ausdruck "fehlerhaft" war.

    6 Anzeigen von Daten aus dem Parsevorgang mit Funktionen

    Nun ist es ja gut und schön, wenn man weiß, dass ein zu parsender Ausdruck den Regeln entspricht. Aber wie an die Daten kommen, die in dem Ausdruck vorhanden sind?

    Dafür gibt es in Boost::Spirit unter anderem Funktionen, die im Grundgerüst wie folgt definiert sind.

    template<IteratorT>
    void <funktionsname>(IteratorT begin, IteratorT end)
    {
      // Verarbeitung der Iteratoren
    }
    
    // oder falls nur ein Iterator zur Verfügung steht, z.B. bei Zahlenparsern von Spirit
    void <funktionsname>(IteratorT begin)
    {
      // Verarbeitung des Iterators
    }
    

    Als Beispiel nehmen wir erneut den INI-File-Parser. Es wird im folgenden Beispiel eine Funktion definiert, die uns beispielhaft die Sektionen ausgibt.

    #include <iostream>
    #include <string>
    using namespace std;
    // ...
    template<typename IteratorT>
    void DebugOut(IteratorT begin, IteratorT end)
    {
        string str(begin, end);
        cout << str << endl;
        /* Alternative */
        /*
        while(begin != end)
            cout << *begin++;
        cout << endl;
        */
    }
    
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;
                          //Hier kommt die Debug Ausgabe                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Mit diesem Konstrukt haben Sie die Möglichkeit sehr einfache Ausgaben zu erzeugen. Dies kann für Debug-Informationen, die ausgegeben werden sollen, sehr nützlich sein.

    Zur Ergänzung möchte ich Ihnen nun noch ein Beispiel für Zahlenparser (ein Iterator) liefern.

    // ...
    
    template<typename IteratorT>
    void DebugOut(IteratorT only_one)
    {
        cout << only_one << endl;
        cout << only_one << " + 1 = " << only_one+1 << endl;
    }
    
    int main(int argc, char* argv)
    {
    
        rule<> count_val = int_p[&DebugOut<int>];
        if(parse("1", count_val).full)
            cout << "Supi";
    
    }
    

    Nun ist das Anzeigen von Daten zwar wie oben genannt in manchen Fällen nützlich, jedoch ist es fast immer der Fall, dass man Informationen speichern muss. Spirit regelt diesen Vorgang über Funktionsobjekte.

    7 Funktionsobjekte oder "Wie speichert man Dateien aus einem Parsevorgang?"

    Spirit bietet zum Speichern der Daten Funktionsobjekte.

    Das Grundgerüst eines Funktionsobjektes sieht je nach Anzahl der Iteratoren wie folgt aus:

    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //...
        }
    } 
    
    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT only_one) const
        {
            //...
        }
    }
    

    Als Ausgangspunkt dient erneut der INI-File-Parser

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Für den Parser definieren wir uns nun ein Funktionsobjekt sowie die geeignete Speicherstruktur

    //...
    
    // Die Speicherstruktur
    typedef struct
    {
       string section;
       string variable;
       string value;
    }
    INISTRUCT;
    
    // Wo bin ich in der Regelabarbeitung
    enum whereIAm {Section, Variable, Value};
    
    // Das Funktionsobjekt
    struct getData
    {
        //Konstruktor: Call by Reference um Objekte verändern zu können
        getData(vector<INISTRUCT>& ini_File_, enum whereIAm& switcher_, INISTRUCT& actItem_) : 
                                   ini_File(ini_File_), switcher(switcher_), actItem(actItem_)
        {
    
        }
    
        // Funktionsoperator überladen
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            // String erzeugen
            string str(begin, end);
            // Wo bin ich
            switch(switcher)
            {
                case Section:
                actItem.section = str; //Section
                break;
    
                case Variable:
                actItem.variable = str; //Variable
                break;
    
                case Value:
                actItem.value = str; //Value
                ini_File.push_back(actItem); // Element sichern
                break;
            }
        }
    
        // Benötigte Variablen
        vector<INISTRUCT>& ini_File;
        enum whereIAm& switcher;
        INISTRUCT& actItem;
    };
    
    // ...
    

    Erklärung:
    Wir überladen den Funktionsoperator, um das Funktionsobjekt in der Regel aufrufen zu können. (Der Sicherungsmechanisums ist simpel gehalten und dient nur zur Veranschaulichung.) Call by Reference ist hier Pflicht, um mit der "Außenwelt" kommunizieren zu können.

    Nun muss man das ganze Konstrukt noch aufrufen.

    //...
    
    int main(int argc, char *argv[])
    {
        // ...
    
        vector<INISTRUCT> ini_File;
        INISTRUCT actItem;
        enum whereIAm section = Section, variable = Variable, value = Value;
    
        // Erzeugen der Funktionsobjekte
        getData func_sec(ini_File, section, actItem);  //Section
        getData func_var(ini_File, variable, actItem); //Variable
        getData func_val(ini_File, value, actItem);    //Value
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);
        rule<> _VAR = *(anychar_p - space_p - chlit<char>('='));
    
                                  //Aufruf von func_var                 Aufruf von func_val
        rule<> _VAR_DECLARATION = _VAR[func_var] >> chlit<char>('=') >> _VALUE[func_val] >> *eol_p;
    
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']')) >> chlit<char>(']') 
                                           >> *eol_p;
    
        //                Aufruf von func_sec
        rule<> _INI_ROW = _SECTION[func_sec] | _VAR_DECLARATION | eol_p;
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:
    Was ist passiert? Es werden drei Funktionsobjekte erzeugt (func_sec, func_var, func_val) und aufgerufen. Dies passiert jedes Mal, wenn der Parser in der Regel zur Aufrufstelle kommt.

    ... [b]_SECTION[func_sec][/b] ...
    

    Zum Verständnis für Funktionsobjekte möchte ich auf
    http://cplus.kompf.de/artikel/functors.html und http://www.sgi.com/tech/stl/functors.html verweisen, da alleine die Erklärung von Funktionsobjekten wohl einen Artikel rechtfertigen würde.

    Hiermit endet meine Einführung in Spirit. Ich hoffe, ich konnte einen kleinen Einblick in die Funktionsweise und die Struktur von Spirit geben.

    8 Ausblick

    Spirit 1.8.3 bietet einige interessante Neuerungen. Unter anderem Storable Rules, die es erlauben, die Regel dynamisch zu erweitern.

    Bsp.:

    //...
    stored_rule<> hallo = strlit<const char*>("Hallo");
    rule<> welt = strlit<const char*>(" Welt!");
    
    hallo = hallo.copy() >> welt; 
    //...
    

    Diese Methode bietet sich hervorragend an, um beim Parsen eines C-Source-Files zum Beispiel die nativen Datentypen um typedef-Datentypen zu erweitern.

    9 Weiterführende Links

    Spirit 1.6.3
    Spirit 1.8.3
    http://www.codeproject.com/vcpp/stl/spirit_semantic_actions.asp



  • Hmm, Tobias, hast du bei den Aufzählungen ([list]) absichtlich keine Bullets gemacht? Ich finde es sieht mit schöner aus. 🙂

    • foo
    • bar
    • baz


  • 1 Grundlegendes

    Dieser Artikel soll Sie in die Welt von Boost::Spirit einführen. Boost::Spirit ist ein Parser Generator Framework, das in C++ realisiert ist. Um die folgenden Beispiele complieren zu können, ist Boost::Spirit ab der Version 1.6.3 zu verwenden. Des Weiteren wird ein ISO C++-Compilier (gcc, Visual C++ (ab Version 7.1), ...) benötigt.

    Anmerkung: Probleme bereiten unter anderem MS Visual C++ 6.0 und Open Watcom 1.3.

    1.1 Von Dateien und anderen zu durchsuchenden Strukturen

    Ein klassisches Beispiel für die Verwendung eines Parsers ist die Dateiverarbeitung. Wie oft kommt es vor, dass man ein bestimmtes Format oder eine bestimmte Struktur zu durchsuchen hat und keine geeigneten Klassen oder Strukturen zur verfügung stehen.
    XML-Dateien werden etwa anders verarbeitet als INI-Dateien. Mathematische Eingaben sollen anders interpretiert werden als bestimmte Stringfolgen. All das kennt man mittlerweile als Programmierer zur Genüge.
    Auf diesem Gebiet hat Boost::Spirit seine unverkennbaren Stärken. Durch die einfache Definition von Regeln ist in null Komma nichts ein Parser entwicklet, der die Arbeit von alleine erledigt. Ob dabei zeilenweise durch eine Datei geparst wird oder mathematische Ausdrücke geparst werden, spielt dabei keine große Rolle. Boost::Spirit kann in diese Richtung fast alles handhaben.

    2 Installation von Boost::Spirit

    Um das Framework von Boost::Spirit nutzen zu können, ist es erforderlich, die Header-Dateien bekannt zu machen. Am einfachsten ist es, den Ordner boost aus dem Verzeichnis spirit-1.6.3\boost sowie aus dem Verzeichnis spirit-1.6.3\boost\miniboost in das Include-Verzeichnis des Kompilers / der Entwicklungsumgebung zu kopieren.
    In ein Projekt müssen unbedingt folgende Zeilen eingebunden werden.

    // ...
    #include <boost/spirit.hpp>
    /*****************************************************************************/
    /* Die obige Include Zeile ist nur zu verwenden, falls die Include-Dateien   */
    /* systemweit bekannt gemacht wurden...                                      */
    /* Falls Sie sich im Projektverzeichnis befinden, bitte                      */
    /* #include "boost/spirit.hpp"                                               */
    /* einbinden.                                                                */
    /*****************************************************************************/
    // ...
    using namespace boost::spirit
    

    2.1 Theoretische Informatik

    Für die folgenden Beispiele ist es vielleicht ganz interessant, sich etwas in die theoretische Informatik einzulesen. Deswegen hab ich ein kleines PDF erstellt:
    Klick mich

    3 Die Grundlagen für Parseaktionen im Framework

    Der Datentyp, der in den nächsten Beispielen immer wieder vorkommen wird, ist der rule<>-Datentyp. Mit diesem Datentyp baut man Parser auf. Spirit stellt von sich aus schon diverse Parser zur Verfügung, von denen ich einige in diesem Kapitel vorstellen möchte.

    3.1 Die Literale strlit<> und chlit<>

    // ...
    rule<> ch_A = chlit<>('A');
    rule<> str_Hallo = strlit<>("Hallo");
    // ...
    

    Wie schon zu vermuten ist, werden hier ein Character Literal und ein String Literal erstellt. Es ist durchaus möglich, auch Variablen in der Regel anzugeben wie das folgende Beispiel zeigt.

    // ...
    string myString = "Test";
    rule<> str_Variabel = strlit<>(myString.c_str());  //char* übergeben
    // Alternativ
    rule<> str_Variable2 = strlit<string::const_iterator>(myString.begin(), myString.end());
    // ...
    

    3.2 Ranges

    Um einen Bereich abzudecken, gibt es den range<>-Parser.

    //...
    rule<> rng = range<>('A', 'Z');
    //...
    

    Die Regel rng deckt die Buchstaben von 'A' bis 'Z' ab.

    3.3 Zahlen

    Es gibt in Spirit diverse Zahlenparser. Einer der wichtigeren ist wohl der Integerparser.

    //...
    rule<> integer = int_p;
    //...
    

    Erlaubt eine beliebige Ganzzahl.

    3.4 Operatoren

    Es gibt in Spirit eine Reihe von Operatoren zum Verknüpfen von Regeln. Dazu gehören unter anderem

    <a> | <b>	// <a> oder <b> oder beide werden gematched
    <a> - <b>	// <a> ohne <b>
    <a> >> <b>	// <a> gefolgt von <b>
    !<a>		// 0 oder 1 mal <a>
    *<a>		// 0 oder beliebig oft mal <a>
    +<a>		// 1 oder beliebig oft mal <a>
    ~<a>		// nicht <a>
    

    Weiterführendes Material findet sich unter der Spirit Doku.

    4 Boost::Spirit anhand eines INI-Datei-Parsers

    Zur Erläuterung der Funktionsweise von Boost::Spirit werden wir uns im Folgenden einen INI-Fileparser zusammenbauen. Dazu definieren wir das Aussehen einer INI-Datei.
    Definition:

    [Sektionsname{1}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    [Sektionsname{2}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    .
    .
    .
    [Sektionsname{n}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    

    Für diese Definition überlegen wir uns im Folgenden Regeln, die wir in Spirit abbilden und die uns das Parsen erlauben.

    Es folgen die Regeln abgebildet in Spirit!

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          //[1]
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));               //[2]
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;//[3]
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION | _VAR_DECLARATION | eol_p;                 //[4]
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                   //[5] 
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:

    rule<> Variablename = Zuweisung
    

    Der rule<> Datentyp
    Der Variablentyp rule<> ist einer der wichtigsten Datentypen, wenn man mit Boost::Spirit in Kontakt kommt. Der rule<>-Datentyp legt das "Aussehen" eines Parseausdruck fest.

    Zu [1]:
    Es wird die Regel _VALUE definiert. Wir legen fest, das _VALUE jedes Zeichen außer ein "End of Line" und ein "Eingabe Ende" beinhalten darf. Der * vor der Klammer ist der so genannte "Kleene Star" und besagt, das 0 bis beliebig viele Zeichen vorkommen können.

    Zu [2]:
    Die Regel _VAR legt fest, dass vor dem '=' mindestens ein Zeichen (+) stehen muss, das kein '=' enthalten darf. Des Weiteren dürfen keine Leerzeichen, keine Returns, keine Whitespaces und derartige Dinge vorkommen (space_p-Parser).

    Zu [3]:
    In der Regel _VAR_DECLARATION haben wir unser erstes "gefolgt von"-Zeichen. Wir definieren, dass eine Variablen-Declaration wie folgt aussieht. Die Regel _VAR gefolgt von (>>) einem Character Literal ('=') gefolgt von (>>) der Regel _VALUE gefolgt von (>>) einem möglichen "End of Line".

    Zu [4]:
    Die Regel _INI_ROW ist unsere so genannte "Startregel". Sie ist es, die die Parse-Funktion bedient. In ihr legen wir fest, wie eine Zeile in der INI-File auszusehen hat. Entweder es kommt eine _SECTION oder (|) eine _VAR_DECLARATION oder (|) ein "End of Line".

    Zu [5]:
    Die Funktion parse liefert ein Parse-Ergebnis, auf das wir später noch genauer eingehen werden. Vorerst ist es nur wichtig zu wissen, dass die parse-Funktion in unserem Beispiel zwei Argumente benötigt. Nämlich den zu parsenden Ausdruck und die "Startregel". Das .full zeigt uns, ob wir den Ausdruck komplett durchlaufen haben. Ist das geschehen, geben wir "Valid" aus.

    Zusammenfassend zu diesem Beispiel:

    • Der Datentyp rule<> als einer der wichtigsten Datentypen in Boost::Spirit.
    • Es gibt verschiedene vorgefertigte Parser wie eol_p, space_p, anychar_p ...
    • Wichtige Operatoren sind: *, +, |, >>
    • Die parse-Funktion übernimmt die Arbeit und liefert als Ergebnis einen "true" oder "false" Wert.

    5 Die Parse-Funktion

    Wie im obigen Beispiel gesehen, ist es die parse-Funktion, die den eigentlichen Parsevorgang übernimmt. Diese Funktion liefert in Wirklichkeit mehr als nur einen boolsche Wert. Der Rückgabewert ist eine Variable vom Typ parse_info
    Die Struktur enthält folgende Membervariablen:

    • stop
    • hit
    • full
    • length

    Über diese Variablen lassen sich einige Informationen extrahieren, die Aufschluss über den Parsevorgang geben.

    • Über stop findet man die Position im geparsten Ausdruck, wo aufgehört wurde zu parsen.
    • hit ist true oder false. Sprich entweder wurde mindestens ein Teilausdruck geparst oder es wurde kein einziger Ausdruck geparst.
    • full ist ebenfalls true oder false. Es sagt aus, ob der ganze Parseausdruck geparst wurde oder nicht.
    • length gibt die Anzahl der Zeichen an, die geparst wurden.

    Bsp:

    // ...
    
    // Eine Regel, die eine Liste von Zahlen getrennt durch Kommas liest
    rule<> r = int_p >> *(chlit<char>(',') >> int_p);
    
    // Das ParseInfo-Objekt
    parse_info<> pI;
    
    // Parsen eines Ausdrucks, der nicht nur Zahlen enthält
    pI = parse("1,2,3,a,b,0", r);
    
    // Ausgabe der Werte von ParseInfo
    std::cout << pI.stop << std::endl 
              << pI.full << std::endl 
              << pI.hit << std::endl 
              << pI.length << std::endl;
    
    // ...
    

    Die Ausgabe des Programms

    ,a,b,0      // Hier wurde aufgehört zu parsen
    0           // Ausdruck nicht vollständig geparst
    1           // Teilausdruck wurde erkannt
    5           // 5 Zeichen wurden geparst
    

    Mit diesen Angaben kann man unter anderem einschränken, wo der zu parsende Ausdruck "fehlerhaft" war.

    6 Anzeigen von Daten aus dem Parsevorgang mit Funktionen

    Nun ist es ja gut und schön, wenn man weiß, dass ein zu parsender Ausdruck den Regeln entspricht. Aber wie an die Daten kommen, die in dem Ausdruck vorhanden sind?

    Dafür gibt es in Boost::Spirit unter anderem Funktionen, die im Grundgerüst wie folgt definiert sind.

    template<IteratorT>
    void <funktionsname>(IteratorT begin, IteratorT end)
    {
      // Verarbeitung der Iteratoren
    }
    
    // oder falls nur ein Iterator zur Verfügung steht, z.B. bei Zahlenparsern von Spirit
    void <funktionsname>(IteratorT begin)
    {
      // Verarbeitung des Iterators
    }
    

    Als Beispiel nehmen wir erneut den INI-File-Parser. Es wird im folgenden Beispiel eine Funktion definiert, die uns beispielhaft die Sektionen ausgibt.

    #include <iostream>
    #include <string>
    using namespace std;
    // ...
    template<typename IteratorT>
    void DebugOut(IteratorT begin, IteratorT end)
    {
        string str(begin, end);
        cout << str << endl;
        /* Alternative */
        /*
        while(begin != end)
            cout << *begin++;
        cout << endl;
        */
    }
    
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;
                          //Hier kommt die Debug Ausgabe                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Mit diesem Konstrukt haben Sie die Möglichkeit sehr einfache Ausgaben zu erzeugen. Dies kann für Debug-Informationen, die ausgegeben werden sollen, sehr nützlich sein.

    Zur Ergänzung möchte ich Ihnen nun noch ein Beispiel für Zahlenparser (ein Iterator) liefern.

    // ...
    
    template<typename IteratorT>
    void DebugOut(IteratorT only_one)
    {
        cout << only_one << endl;
        cout << only_one << " + 1 = " << only_one+1 << endl;
    }
    
    int main(int argc, char* argv)
    {
    
        rule<> count_val = int_p[&DebugOut<int>];
        if(parse("1", count_val).full)
            cout << "Supi";
    
    }
    

    Nun ist das Anzeigen von Daten zwar wie oben genannt in manchen Fällen nützlich, jedoch ist es fast immer der Fall, dass man Informationen speichern muss. Spirit regelt diesen Vorgang über Funktionsobjekte.

    7 Funktionsobjekte oder "Wie speichert man Dateien aus einem Parsevorgang?"

    Spirit bietet zum Speichern der Daten Funktionsobjekte.

    Das Grundgerüst eines Funktionsobjektes sieht je nach Anzahl der Iteratoren wie folgt aus:

    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //...
        }
    } 
    
    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT only_one) const
        {
            //...
        }
    }
    

    Als Ausgangspunkt dient erneut der INI-File-Parser

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Für den Parser definieren wir uns nun ein Funktionsobjekt sowie die geeignete Speicherstruktur

    //...
    
    // Die Speicherstruktur
    typedef struct
    {
       string section;
       string variable;
       string value;
    }
    INISTRUCT;
    
    // Wo bin ich in der Regelabarbeitung
    enum whereIAm {Section, Variable, Value};
    
    // Das Funktionsobjekt
    struct getData
    {
        //Konstruktor: Call by Reference um Objekte verändern zu können
        getData(vector<INISTRUCT>& ini_File_, enum whereIAm& switcher_, INISTRUCT& actItem_) : 
                                   ini_File(ini_File_), switcher(switcher_), actItem(actItem_)
        {
    
        }
    
        // Funktionsoperator überladen
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            // String erzeugen
            string str(begin, end);
            // Wo bin ich
            switch(switcher)
            {
                case Section:
                actItem.section = str; //Section
                break;
    
                case Variable:
                actItem.variable = str; //Variable
                break;
    
                case Value:
                actItem.value = str; //Value
                ini_File.push_back(actItem); // Element sichern
                break;
            }
        }
    
        // Benötigte Variablen
        vector<INISTRUCT>& ini_File;
        enum whereIAm& switcher;
        INISTRUCT& actItem;
    };
    
    // ...
    

    Erklärung:
    Wir überladen den Funktionsoperator, um das Funktionsobjekt in der Regel aufrufen zu können. (Der Sicherungsmechanisums ist simpel gehalten und dient nur zur Veranschaulichung.) Call by Reference ist hier Pflicht, um mit der "Außenwelt" kommunizieren zu können.

    Nun muss man das ganze Konstrukt noch aufrufen.

    //...
    
    int main(int argc, char *argv[])
    {
        // ...
    
        vector<INISTRUCT> ini_File;
        INISTRUCT actItem;
        enum whereIAm section = Section, variable = Variable, value = Value;
    
        // Erzeugen der Funktionsobjekte
        getData func_sec(ini_File, section, actItem);  //Section
        getData func_var(ini_File, variable, actItem); //Variable
        getData func_val(ini_File, value, actItem);    //Value
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);
        rule<> _VAR = *(anychar_p - space_p - chlit<char>('='));
    
                                  //Aufruf von func_var                 Aufruf von func_val
        rule<> _VAR_DECLARATION = _VAR[func_var] >> chlit<char>('=') >> _VALUE[func_val] >> *eol_p;
    
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']')) >> chlit<char>(']') 
                                           >> *eol_p;
    
        //                Aufruf von func_sec
        rule<> _INI_ROW = _SECTION[func_sec] | _VAR_DECLARATION | eol_p;
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:
    Was ist passiert? Es werden drei Funktionsobjekte erzeugt (func_sec, func_var, func_val) und aufgerufen. Dies passiert jedes Mal, wenn der Parser in der Regel zur Aufrufstelle kommt.

    ... [b]_SECTION[func_sec][/b] ...
    

    Zum Verständnis für Funktionsobjekte möchte ich auf
    http://cplus.kompf.de/artikel/functors.html und http://www.sgi.com/tech/stl/functors.html verweisen, da alleine die Erklärung von Funktionsobjekten wohl einen Artikel rechtfertigen würde.

    Hiermit endet meine Einführung in Spirit. Ich hoffe, ich konnte einen kleinen Einblick in die Funktionsweise und die Struktur von Spirit geben.

    8 Ausblick

    Spirit 1.8.3 bietet einige interessante Neuerungen. Unter anderem Storable Rules, die es erlauben, die Regel dynamisch zu erweitern.

    Bsp.:

    //...
    stored_rule<> hallo = strlit<const char*>("Hallo");
    rule<> welt = strlit<const char*>(" Welt!");
    
    hallo = hallo.copy() >> welt; 
    //...
    

    Diese Methode bietet sich hervorragend an, um beim Parsen eines C-Source-Files zum Beispiel die nativen Datentypen um typedef-Datentypen zu erweitern.

    9 Weiterführende Links

    Spirit 1.6.3
    Spirit 1.8.3
    http://www.codeproject.com/vcpp/stl/spirit_semantic_actions.asp



  • 1 Grundlegendes

    Dieser Artikel soll Sie in die Welt von Boost::Spirit einführen. Boost::Spirit ist ein Parser-Generator-Framework, das in C++ realisiert ist. Um die folgenden Beispiele kompilieren zu können, ist Boost::Spirit ab der Version 1.6.3 zu verwenden. Des Weiteren wird ein ISO C++-Compiler (gcc, Visual C++ (ab Version 7.1), ...) benötigt.

    Anmerkung: Probleme bereiten unter anderem MS Visual C++ 6.0 und Open Watcom 1.3.

    1.1 Von Dateien und anderen zu durchsuchenden Strukturen

    Ein klassisches Beispiel für die Verwendung eines Parsers ist die Dateiverarbeitung. Wie oft kommt es vor, dass man ein bestimmtes Format oder eine bestimmte Struktur zu durchsuchen hat und keine geeigneten Klassen oder Strukturen zur Verfügung stehen.
    XML-Dateien werden etwa anders verarbeitet als INI-Dateien. Mathematische Eingaben sollen anders interpretiert werden als bestimmte Stringfolgen. All das kennt man mittlerweile als Programmierer zur Genüge.
    Auf diesem Gebiet hat Boost::Spirit seine unverkennbaren Stärken. Durch die einfache Definition von Regeln ist in null Komma nichts ein Parser entwickelt, der die Arbeit von alleine erledigt. Ob dabei zeilenweise durch eine Datei geparst wird oder mathematische Ausdrücke geparst werden, spielt keine große Rolle. Boost::Spirit kann in diese Richtung fast alles handhaben.

    2 Installation von Boost::Spirit

    Um das Framework von Boost::Spirit nutzen zu können, ist es erforderlich, die Header-Dateien bekannt zu machen. Am einfachsten ist es, den Ordner boost aus dem Verzeichnis spirit-1.6.3\boost sowie aus dem Verzeichnis spirit-1.6.3\boost\miniboost in das Include-Verzeichnis des Compilers / der Entwicklungsumgebung zu kopieren.
    In ein Projekt müssen unbedingt folgende Zeilen eingebunden werden.

    // ...
    #include <boost/spirit.hpp>
    /*****************************************************************************/
    /* Die obige Include Zeile ist nur zu verwenden, falls die Include-Dateien   */
    /* systemweit bekannt gemacht wurden...                                      */
    /* Falls Sie sich im Projektverzeichnis befinden, bitte                      */
    /* #include "boost/spirit.hpp"                                               */
    /* einbinden.                                                                */
    /*****************************************************************************/
    // ...
    using namespace boost::spirit
    

    2.1 Theoretische Informatik

    Für die folgenden Beispiele ist es vielleicht ganz interessant, sich etwas in die theoretische Informatik einzulesen. Deswegen hab ich ein kleines PDF erstellt:
    Klick mich

    3 Die Grundlagen für Parseaktionen im Framework

    Der Datentyp, der in den nächsten Beispielen immer wieder vorkommen wird, ist der rule<>-Datentyp. Mit diesem Datentyp baut man Parser auf. Spirit stellt von sich aus schon diverse Parser zur Verfügung, von denen ich einige in diesem Kapitel vorstellen möchte.

    3.1 Die Literale strlit<> und chlit<>

    // ...
    rule<> ch_A = chlit<>('A');
    rule<> str_Hallo = strlit<>("Hallo");
    // ...
    

    Wie schon zu vermuten ist, werden hier ein Character Literal und ein String Literal erstellt. Es ist durchaus möglich, auch Variablen in der Regel anzugeben wie das folgende Beispiel zeigt.

    // ...
    string myString = "Test";
    rule<> str_Variabel = strlit<>(myString.c_str());  //char* übergeben
    // Alternativ
    rule<> str_Variable2 = strlit<string::const_iterator>(myString.begin(), myString.end());
    // ...
    

    3.2 Ranges

    Um einen Bereich abzudecken, gibt es den range<>-Parser.

    //...
    rule<> rng = range<>('A', 'Z');
    //...
    

    Die Regel rng deckt die Buchstaben von 'A' bis 'Z' ab.

    3.3 Zahlen

    Es gibt in Spirit diverse Zahlenparser. Einer der wichtigeren ist wohl der Integerparser.

    //...
    rule<> integer = int_p;
    //...
    

    Erlaubt eine beliebige Ganzzahl.

    3.4 Operatoren

    Es gibt in Spirit eine Reihe von Operatoren zum Verknüpfen von Regeln. Dazu gehören unter anderem

    <a> | <b>	// <a> oder <b> oder beide werden gematched
    <a> - <b>	// <a> ohne <b>
    <a> >> <b>	// <a> gefolgt von <b>
    !<a>		// 0 oder 1 mal <a>
    *<a>		// 0 oder beliebig oft mal <a>
    +<a>		// 1 oder beliebig oft mal <a>
    ~<a>		// nicht <a>
    

    Weiterführendes Material findet sich unter der Spirit Doku.

    4 Boost::Spirit anhand eines INI-Datei-Parsers

    Zur Erläuterung der Funktionsweise von Boost::Spirit werden wir uns im Folgenden einen INI-Fileparser zusammenbauen. Dazu definieren wir das Aussehen einer INI-Datei.
    Definition:

    [Sektionsname{1}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    [Sektionsname{2}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    .
    .
    .
    [Sektionsname{n}]
    variable{1}=wert{1}
    variable{2}=wert{2}
    .
    .
    .
    variable{n}=wert{n}
    

    Für diese Definition überlegen wir uns im Folgenden Regeln, die wir in Spirit abbilden und die uns das Parsen erlauben.

    Es folgen die Regeln abgebildet in Spirit.

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          //[1]
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));               //[2]
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;//[3]
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION | _VAR_DECLARATION | eol_p;                 //[4]
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                   //[5] 
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:

    rule<> Variablename = Zuweisung
    

    Der rule<> Datentyp
    Der Variablentyp rule<> ist einer der wichtigsten Datentypen, wenn man mit Boost::Spirit in Kontakt kommt. Der rule<>-Datentyp legt das "Aussehen" eines Parseausdruck fest.

    Zu [1]:
    Es wird die Regel _VALUE definiert. Wir legen fest, das _VALUE jedes Zeichen außer ein "End of Line" und ein "Eingabe Ende" beinhalten darf. Der * vor der Klammer ist der so genannte "Kleene Star" und besagt, das 0 bis beliebig viele Zeichen vorkommen können.

    Zu [2]:
    Die Regel _VAR legt fest, dass vor dem '=' mindestens ein Zeichen (+) stehen muss, das kein '=' enthalten darf. Des Weiteren dürfen keine Leerzeichen, keine Returns, keine Whitespaces und derartige Dinge vorkommen (space_p-Parser).

    Zu [3]:
    In der Regel _VAR_DECLARATION haben wir unser erstes "gefolgt von"-Zeichen. Wir definieren, dass eine Variablen-Deklaration wie folgt aussieht. Die Regel _VAR gefolgt von (>>) einem Character Literal ('=') gefolgt von (>>) der Regel _VALUE gefolgt von (>>) einem möglichen "End of Line".

    Zu [4]:
    Die Regel _INI_ROW ist unsere so genannte "Startregel". Sie ist es, die die Parse-Funktion bedient. In ihr legen wir fest, wie eine Zeile in dem INI-File auszusehen hat. Entweder es kommt eine _SECTION oder (|) eine _VAR_DECLARATION oder (|) ein "End of Line".

    Zu [5]:
    Die Funktion parse liefert ein Parse-Ergebnis, auf das wir später noch genauer eingehen werden. Vorerst ist es nur wichtig zu wissen, dass die parse-Funktion in unserem Beispiel zwei Argumente benötigt. Nämlich den zu parsenden Ausdruck und die "Startregel". Das .full zeigt uns, ob wir den Ausdruck komplett durchlaufen haben. Ist das geschehen, geben wir "Valid" aus.

    Zusammenfassend zu diesem Beispiel:

    • Der Datentyp rule<> als einer der wichtigsten Datentypen in Boost::Spirit.
    • Es gibt verschiedene vorgefertigte Parser wie eol_p, space_p, anychar_p ...
    • Wichtige Operatoren sind: *, +, |, >>
    • Die parse-Funktion übernimmt die Arbeit und liefert als Ergebnis einen "true" oder "false" Wert.

    5 Die Parse-Funktion

    Wie im obigen Beispiel gesehen, ist es die parse-Funktion, die den eigentlichen Parsevorgang übernimmt. Diese Funktion liefert in Wirklichkeit mehr als nur einen boolschen Wert. Der Rückgabewert ist eine Variable vom Typ parse_info.
    Die Struktur enthält folgende Membervariablen:

    • stop
    • hit
    • full
    • length

    Über diese Variablen lassen sich einige Informationen extrahieren, die Aufschluss über den Parsevorgang geben.

    • Über stop findet man die Position im geparsten Ausdruck, wo aufgehört wurde zu parsen.
    • hit ist true oder false. Sprich entweder wurde mindestens ein Teilausdruck geparst oder es wurde kein einziger Ausdruck geparst.
    • full ist ebenfalls true oder false. Es sagt aus, ob der ganze Parseausdruck geparst wurde oder nicht.
    • length gibt die Anzahl der Zeichen an, die geparst wurden.

    Bsp:

    // ...
    
    // Eine Regel, die eine Liste von Zahlen getrennt durch Kommas liest
    rule<> r = int_p >> *(chlit<char>(',') >> int_p);
    
    // Das ParseInfo-Objekt
    parse_info<> pI;
    
    // Parsen eines Ausdrucks, der nicht nur Zahlen enthält
    pI = parse("1,2,3,a,b,0", r);
    
    // Ausgabe der Werte von ParseInfo
    std::cout << pI.stop << std::endl 
              << pI.full << std::endl 
              << pI.hit << std::endl 
              << pI.length << std::endl;
    
    // ...
    

    Die Ausgabe des Programms

    ,a,b,0      // Hier wurde aufgehört zu parsen
    0           // Ausdruck nicht vollständig geparst
    1           // Teilausdruck wurde erkannt
    5           // 5 Zeichen wurden geparst
    

    Mit diesen Angaben kann man unter anderem einschränken, wo der zu parsende Ausdruck "fehlerhaft" war.

    6 Anzeigen von Daten aus dem Parsevorgang mit Funktionen

    Nun ist es ja gut und schön, wenn man weiß, dass ein zu parsender Ausdruck den Regeln entspricht. Aber wie an die Daten kommen, die in dem Ausdruck vorhanden sind?

    Dafür gibt es in Boost::Spirit unter anderem Funktionen, die im Grundgerüst wie folgt definiert sind.

    template<IteratorT>
    void <funktionsname>(IteratorT begin, IteratorT end)
    {
      // Verarbeitung der Iteratoren
    }
    
    // oder falls nur ein Iterator zur Verfügung steht, z.B. bei Zahlenparsern von Spirit
    void <funktionsname>(IteratorT begin)
    {
      // Verarbeitung des Iterators
    }
    

    Als Beispiel nehmen wir erneut den INI-File-Parser. Es wird im folgenden Beispiel eine Funktion definiert, die uns beispielhaft die Sektionen ausgibt.

    #include <iostream>
    #include <string>
    using namespace std;
    // ...
    template<typename IteratorT>
    void DebugOut(IteratorT begin, IteratorT end)
    {
        string str(begin, end);
        cout << str << endl;
        /* Alternative */
        /*
        while(begin != end)
            cout << *begin++;
        cout << endl;
        */
    }
    
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;
                          //Hier kommt die Debug Ausgabe                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Mit diesem Konstrukt haben Sie die Möglichkeit sehr einfache Ausgaben zu erzeugen. Dies kann für Debug-Informationen, die ausgegeben werden sollen, sehr nützlich sein.

    Zur Ergänzung möchte ich Ihnen nun noch ein Beispiel für Zahlenparser (ein Iterator) liefern.

    // ...
    
    template<typename IteratorT>
    void DebugOut(IteratorT only_one)
    {
        cout << only_one << endl;
        cout << only_one << " + 1 = " << only_one+1 << endl;
    }
    
    int main(int argc, char* argv)
    {
    
        rule<> count_val = int_p[&DebugOut<int>];
        if(parse("1", count_val).full)
            cout << "Supi";
    
    }
    

    Nun ist das Anzeigen von Daten zwar wie oben genannt in manchen Fällen nützlich, jedoch ist es fast immer der Fall, dass man Informationen speichern muss. Spirit regelt diesen Vorgang über Funktionsobjekte.

    7 Funktionsobjekte oder "Wie speichert man Dateien aus einem Parsevorgang?"

    Spirit bietet zum Speichern der Daten Funktionsobjekte.

    Das Grundgerüst eines Funktionsobjektes sieht je nach Anzahl der Iteratoren wie folgt aus:

    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            //...
        }
    } 
    
    struct <Funktionsobjektname>
    {
    
        template<typename IteratorT>
        void operator()(IteratorT only_one) const
        {
            //...
        }
    }
    

    Als Ausgangspunkt dient erneut der INI-File-Parser

    // ...
    int main(int argc, char** argv)
    {
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);                          
        rule<> _VAR = +(anychar_p - space_p - chlit<char>('='));              
        rule<> _VAR_DECLARATION = _VAR >> chlit<char>('=') >> _VALUE >> *eol_p;
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']'))  
                          >> chlit<char>(']') >> *eol_p;                       
        rule<> _INI_ROW = _SECTION[&DebugOut<const char*>] | _VAR_DECLARATION | eol_p;                 
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Für den Parser definieren wir uns nun ein Funktionsobjekt sowie die geeignete Speicherstruktur

    //...
    
    // Die Speicherstruktur
    typedef struct
    {
       string section;
       string variable;
       string value;
    }
    INISTRUCT;
    
    // Wo bin ich in der Regelabarbeitung
    enum whereIAm {Section, Variable, Value};
    
    // Das Funktionsobjekt
    struct getData
    {
        //Konstruktor: Call by Reference um Objekte verändern zu können
        getData(vector<INISTRUCT>& ini_File_, enum whereIAm& switcher_, INISTRUCT& actItem_) : 
                                   ini_File(ini_File_), switcher(switcher_), actItem(actItem_)
        {
    
        }
    
        // Funktionsoperator überladen
        template<typename IteratorT>
        void operator()(IteratorT begin, IteratorT end) const
        {
            // String erzeugen
            string str(begin, end);
            // Wo bin ich
            switch(switcher)
            {
                case Section:
                actItem.section = str; //Section
                break;
    
                case Variable:
                actItem.variable = str; //Variable
                break;
    
                case Value:
                actItem.value = str; //Value
                ini_File.push_back(actItem); // Element sichern
                break;
            }
        }
    
        // Benötigte Variablen
        vector<INISTRUCT>& ini_File;
        enum whereIAm& switcher;
        INISTRUCT& actItem;
    };
    
    // ...
    

    Erklärung:
    Wir überladen den Funktionsoperator, um das Funktionsobjekt in der Regel aufrufen zu können. (Der Sicherungsmechanisums ist simpel gehalten und dient nur zur Veranschaulichung.) Call by Reference ist hier Pflicht, um mit der "Außenwelt" kommunizieren zu können.

    Nun muss man das ganze Konstrukt noch aufrufen.

    //...
    
    int main(int argc, char *argv[])
    {
        // ...
    
        vector<INISTRUCT> ini_File;
        INISTRUCT actItem;
        enum whereIAm section = Section, variable = Variable, value = Value;
    
        // Erzeugen der Funktionsobjekte
        getData func_sec(ini_File, section, actItem);  //Section
        getData func_var(ini_File, variable, actItem); //Variable
        getData func_val(ini_File, value, actItem);    //Value
    
        rule<> _VALUE = *(anychar_p - eol_p - end_p);
        rule<> _VAR = *(anychar_p - space_p - chlit<char>('='));
    
                                  //Aufruf von func_var                 Aufruf von func_val
        rule<> _VAR_DECLARATION = _VAR[func_var] >> chlit<char>('=') >> _VALUE[func_val] >> *eol_p;
    
        rule<> _SECTION = chlit<char>('[') >> *(anychar_p - chlit<char>(']')) >> chlit<char>(']') 
                                           >> *eol_p;
    
        //                Aufruf von func_sec
        rule<> _INI_ROW = _SECTION[func_sec] | _VAR_DECLARATION | eol_p;
    
        // ...
    
        if(parse(<zeile aus der .ini Datei>, _INI_ROW).full)                    
        {
            cout << "Valid";
        }
    
        // ...
    }
    

    Erklärung:
    Was ist passiert? Es werden drei Funktionsobjekte erzeugt (func_sec, func_var, func_val) und aufgerufen. Dies passiert jedes Mal, wenn der Parser in der Regel zur Aufrufstelle kommt.

    ... [b]_SECTION[func_sec][/b] ...
    

    Zum Verständnis für Funktionsobjekte möchte ich auf
    http://cplus.kompf.de/artikel/functors.html und http://www.sgi.com/tech/stl/functors.html verweisen, da alleine die Erklärung von Funktionsobjekten wohl einen Artikel rechtfertigen würde.

    Hiermit endet meine Einführung in Spirit. Ich hoffe, ich konnte einen kleinen Einblick in die Funktionsweise und die Struktur von Spirit geben.

    8 Ausblick

    Spirit 1.8.3 bietet einige interessante Neuerungen. Unter anderem Storable Rules, die es erlauben, die Regel dynamisch zu erweitern.

    Bsp.:

    //...
    stored_rule<> hallo = strlit<const char*>("Hallo");
    rule<> welt = strlit<const char*>(" Welt!");
    
    hallo = hallo.copy() >> welt; 
    //...
    

    Diese Methode bietet sich hervorragend an, um beim Parsen eines C-Source-Files zum Beispiel die nativen Datentypen um typedef-Datentypen zu erweitern.

    9 Weiterführende Links

    Spirit 1.6.3
    Spirit 1.8.3
    http://www.codeproject.com/vcpp/stl/spirit_semantic_actions.asp

    -------
    ➡ Noch mal ein paar kleine Fehler verbessert.

    BTW, wie siehts jetzt mit dem pdf aus? Auf dem Server seh ich noch keine Text-Datei.



  • -predator- schrieb:

    BTW, wie siehts jetzt mit dem pdf aus? Auf dem Server seh ich noch keine Text-Datei.

    Guxtu 😉



  • Aaaachso! Hab die zweite Seite gar nicht gesehen. 🤡


Anmelden zum Antworten