Ini-Dateien parsen



  • Ich habe mir eine kleine Klasse geschrieben um Ini-Dateien zu parsen.

    ;Kommentar
    [Sektion]
    ;Kommentar
    Schlüssel=Wert
    

    Um meine Ini-Dateien aber noch übersichtlicher zu machen, erlaube ich auch solche Dateien.

    ;screen settings
    [screen]
        width=800
        height=600
    
        ;bits per pixel
        bpp=32
    
    ;player settings
    [player]
    
        ;player name
        name=John Doe
    
        ;highest score
        highscore=2342
    

    Meine Parserklasse sieht nun folgendermaßen aus:

    #include <iostream>
    #include <fstream>
    #include <string>
    #include <map>
    
    class IniFile
    {
        public:
            // constructor
            IniFile(std::string _filename)
                : filename(_filename)
            {
                // open inifile
                std::ifstream file(filename.c_str(), std::ios::in);
                if(!file) {
                    throw std::string("Could not open file.");
                }
    
                std::string line = "";
                std::string sectionName = "";
                unsigned int pos = 0;
    
                // read line
                while(getline(file, line)) {
    
                    // remove whitespaces and tabs at the beginning of the line
                    pos = line.find_first_not_of(" \t");
                    if(pos == std::string::npos) {
                        line.erase();
                    } else {
                        line.erase(0, pos);
                    }
    
                    if(line.empty() || line.at(0) == ';') {
                        // ignore empty line or comment
    
                    } else if(line.at(0) == '[') {
                        // new section found
                        pos = line.find(']', 1);
                        if(pos == std::string::npos) {
                            throw std::string("Could not find ']'");
                        }
                        sectionName = line.substr(1, pos - 1);
                        if(sectionName.length() == 0) {
                            throw std::string("Empty Section Name");
                        }
    
                        // add new section
                        data.insert(std::make_pair(sectionName, Section()));
    
                    } else {
                        // new key-value-pair found
                        pos = line.find('=', 1);
                        if(pos == std::string::npos) {
                            throw std::string("Could not find '='");
                        }
                        std::string key = line.substr(0, pos);
                        std::string value = line.substr(pos + 1, line.length() - pos - 1);
                        if(key.length() == 0 || value.length() == 0) {
                            throw std::string("Empty key or empty value");
                        }
    
                        // add new key-value-pair
                        if(sectionName == "") {
                            throw std::string("Key-Value-Pair without Section");
                        }
                        data[sectionName].insert(std::make_pair(key, value));
                    }
                }
            }
    
            // returns the value identified by key
            std::string getValue(std::string section, std::string key) {
    
                if(data.count(section)) {
    
                    if(data[section].count(key)) {
    
                        // return value
                        return data[section][key];
                    }
                }
    
                // section or key does not exist, remove an empty string
                return "";
            }
    
        private:
            std::string filename;
    
            // a section with key-value-pairs
            typedef std::map< std::string, std::string > Section;
    
            // the inifile contains several named sections
            std::map< std::string, Section > data;
    };
    
    int main()
    {
        try {
            // Create new IniFile-object
            IniFile test("test.ini");
    
            // Display some values
            std::cout << "Screen height: " << test.getValue("screen", "height") << std::endl;
            std::cout << "Player highscore: " << test.getValue("player", "highscore") << std::endl;
            std::cout << "Game title: " << test.getValue("game", "title") << std::endl;
    
        } catch(std::string& e) {
            std::cout << e << std::endl;
        }
    
    	return 0;
    }
    

    Ich habe versucht viel Gebrauch von den Funktionen der C++-Standardbibliotkek zu verwenden. Allerdings gefällt mir nicht, dass sich der komplette Parsercode im Konstruktor befindet. Ich weiß aber auch nicht wie ich die ganze Klasse schöner und effektiver gestalten könnte. Habt ihr irgendwelche Ideen wie ich diesen Parser noch verbessern kann?



  • Schau dir mal den magazinbeitrag zum Thema Parser/Boost::spirit an. Dort wird genau das Thema "ini dateien parsen" angegangen. (Und der Code ist seeeeehr vile kürzer und übersichtlicher und intuitiver als deiner hier 😉 )



  • Naja, ich würde auch den Parser und die "ini File Klasse" auftrennen.
    Für den Parser kannst du natürlich Spirit nehmen wie vorgeschlagen wurde, ich halte das allerdings für Overkill.

    Für so einen einfachen Parser reicht IMHO eine kleine State-Machine, hab' ich selbst schon geschrieben.



  • Boost::Spirit sieht interessant aus, aber ich glaube das wäre ein wenig überdimensioniert für meine Zwecke, aber ich werds mal damit ausprobieren.

    @hustbaer: Wie sollten denn die Klassen IniFileParser und IniFile dann aussehen? (Welche Methoden brauchen Sie) und wie funktioniert das mit der State-Machine?



  • Rein zufällig behandelt der Spirit-Artikel von Tobias Gerg im Forums-Magazin auch das Parsen von ini-Dateien. 😃



  • Hallo

    Artchi schrieb:

    Rein zufällig behandelt der Spirit-Artikel von Tobias Gerg im Forums-Magazin auch das Parsen von ini-Dateien. 😃

    Darum geht es ja unter anderem und der Link wurde auch schon gezeigt. 😕

    chrische



  • Ups!



  • OhneName schrieb:

    Boost::Spirit sieht interessant aus, aber ich glaube das wäre ein wenig überdimensioniert für meine Zwecke, aber ich werds mal damit ausprobieren.

    @hustbaer: Wie sollten denn die Klassen IniFileParser und IniFile dann aussehen? (Welche Methoden brauchen Sie) und wie funktioniert das mit der State-Machine?

    Naja, kommt drauf an ob man lieber Templates oder lieber Interfaces verwendet. Hier mal die Version mit nem Interface:

    class IniFileParser
    {
    public:
        class Client
        {
        public:
            virtual void ProcessSectionHeader(std::string const& sectionName) = 0;
            virtual void ProcessValue(std::string const& sectionName, std::string const& valueName, std::string const& value) = 0;
            // virtual void ProcessComment(std::string const& comment) = 0;
        };
    
        explicit IniFileParser(Client* client);
    
        void SetClient(Client* client);
    
        bool ProcessStream(std::istream& is);
        bool ProcessChar(char c);
        void Flush();
    
        // ...
    };
    
    class IniFile : private IniFileParser::Client
    {
    public:
        explicit IniFile(std::string const& path)
        {
            IniFileParser parser(this);
            parser.ProcessStream(std::ifstream(path));
            parser.Flush();
        }
    
    private:
        virtual void ProcessSectionHeader(std::string const& sectionName);
        virtual void ProcessValue(std::string const& sectionName, std::string const& valueName, std::string const& value);
    
        // ...
    };
    


  • hustbaer schrieb:

    Naja, kommt drauf an ob man lieber Templates oder lieber Interfaces verwendet.

    Kanonen auf Spatzen schießen?



  • Mr. N schrieb:

    hustbaer schrieb:

    Naja, kommt drauf an ob man lieber Templates oder lieber Interfaces verwendet.

    Kanonen auf Spatzen schießen?

    Huch?
    Der OP hat gemeint es stört ihn dass der gesamte Parser im ctor "enthalten" ist. Ich hab' ihm einen halbwegs sauberen Weg gezeigt das zu vermeiden.
    Wenn du das Interface meinst: was genau ist daran schlecht? Bzw. wo ist die "Kanone" - die 10 Zeilen Code die das mehr ausmacht? Naja...

    Strenggenommen ist schon die Verwendung von IniFileParser im ctor von IniFile unnötig und sollte vermieden werden, z.B. durch eine free function "LoadIniFile" die genau die beiden Klassen zusammenknotet. Ich wollte es dann aber auch nicht übertreiben...

    Ich würde es eher als Kanonen auf Spatzen schiessen bezeichnen jmd. der damit noch offensichtlich keine Erfahrung hat die Verwendung von Boost.Spirit zu empfehlen.



  • Hallo, ich lebe hinter dem Mond und würde gern wissen, wozu man Ini Dateien parsen tut.

    MfG
    DhMl



  • Vielen Dank für das Beispiel, hustbaer.

    Ich versuch das ganze jetzt erst mal mit Boost::Spirit umzusetzen, der Magazinartikel erklärt das eigentlich recht gut und ich hab die Boost-Biblioteken sowieso schon auf meinem Rechner.

    Anschließend schau ich mir auch deinen Vorschlag nochmal genauer an.

    @Der hinterm Mond lebt:
    Ich brauche ein Dateiformat um leicht Einstellungen speichern und lesen zu können. Die Windows-Registry will ich nicht verwenden und ein eigenes Format entwickeln auch nicht. Also hab ich erstmal INI gewählt, da XML doch noch ein ganzes Stück schwerer zu parsen ist. Wie eine Ini-Datei aufgebaut ist, habe ich in meinem ersten Post kurz dargestellt.



  • hustbaer schrieb:

    Mr. N schrieb:

    hustbaer schrieb:

    Naja, kommt drauf an ob man lieber Templates oder lieber Interfaces verwendet.

    Kanonen auf Spatzen schießen?

    Huch?
    Der OP hat gemeint es stört ihn dass der gesamte Parser im ctor "enthalten" ist. Ich hab' ihm einen halbwegs sauberen Weg gezeigt das zu vermeiden.
    Wenn du das Interface meinst: was genau ist daran schlecht? Bzw. wo ist die "Kanone" - die 10 Zeilen Code die das mehr ausmacht? Naja...

    Dein Weg ist natürlich nicht falsch, aber eben unnötig kompliziert. Ich würde auch kein Spirit verwenden. Aufteilen des Parsers in ein paar Methoden und Verwendung dessen, was der Standard und Boost.String_algo hergibt, und schon sieht das ganze viel schöner aus. 🙂

    Ich habe für einen Parser mit einem ähnlichen aber einfacheren Format (nur Key=Value und Kommentare mit 😵 etwa 40 Zeilen gebraucht und ich habe keine externe Bibliothek verwendet. (Hat mich sehr geärgert, dass die Boost-Abhängigkeit in meiner Abwesenheit rausgestrichen wurde. 🙄)

    OhneName schrieb:

    Also hab ich erstmal INI gewählt, da XML doch noch ein ganzes Stück schwerer zu parsen ist.

    Sicher, dass du Sektionen brauchst?



  • Mr. N schrieb:

    OhneName schrieb:

    Also hab ich erstmal INI gewählt, da XML doch noch ein ganzes Stück schwerer zu parsen ist.

    Sicher, dass du Sektionen brauchst?

    Das ist eine interessante Idee, weil eigentlich brauch ich sie nicht, und würde das ganze mit Sicherheit deutlich vereinfachen, doch wenn ich schonmal nen Parser schreib, dann auch gleich richtig. Ich hatte in meiner ursprünglichen Version ja auch bereits Unterstützung für Tabs und Leerzeichen eingebaut um Einrückung möglich zu machen.

    Also dank des großartigen Magazinartikels habe ich jetzt schonmal einen laufenden Parser mit Boost::Spirit. So schwierig find ich die Bibliothek garnicht, das Problem sind eher ein paar Unterschiede im Programmierstil zwischen mir und dem Autor (vorallem bei Variablennamen). Aber wenn ich das ganze erstmal an meine "Stil" angepasst habe, dann werd ich wohl bei dieser Lösung bleiben. Ich poste die fertige Klasse dann wieder hier.



  • Unter http://alphadev.awardspace.com/index.html könnt ihr euch nun das fertige Ergebnis ansehen. Ich bin eigentlich recht zufrieden damit und hab auch noch ein paar mehr Features drin als in der ursprünglichen Version.

    Jetzt würde mich interessieren:
    - Ist es sinnvoll boost::lexical_cast zu verwenden um Werte beliebigen Typs auszulesen?
    - Ist es okay, dass der Funktor GetData und das Struct Entry als innere Klassen des IniFileParsers implementiert sind oder sollte ich das besser trennen?
    - Gibt es eine bessere Lösung um die geparsten Werte in meiner Map zu speichern, als über das Struct Entry?

    Schonmal danke im Voraus für jegliche Kritik 🙂


Anmelden zum Antworten