Problem with parsing a INI file into structs with Boost Spirit

49 Views Asked by At

I'm trying to store INI file information in a struct with this code:

#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/spirit/include/qi.hpp>
#include <iostream>
#include <map>
#include <vector>

using Key       = std::string;
using Value     = std::string;
using Section   = std::map<Key, Value>;

namespace qi = boost::spirit::qi;

namespace client 
{
    struct IniSection 
    {
        std::string name;
        Section     pair;
    };

    struct IniFile 
    {
        std::vector<IniSection> sections;
    };
};

namespace client 
{
    template <typename Iterator> 
    struct ini_grammar : qi::grammar<Iterator, IniFile()> 
    {
        ini_grammar() : ini_grammar::base_type(start) 
        {
            skipper = qi::blank | '#' >> *(qi::char_ - qi::eol);

            key     = +qi::char_("a-zA-Z_0-9");
            value   = *(qi::char_ - qi::eol);
            pair    = key >> '=' >> value;
            section = '[' >> key >> ']' >> +qi::eol >> *(pair >> +qi::eol);
            file    = *section;

            start   = qi::skip(copy(skipper))[file];

        }
        using Skipper = qi::rule<Iterator>;
        using KVP = std::pair<Key, Value>;
        Skipper skipper;

        
        qi::rule<Iterator, IniFile()>           start;
        qi::rule<Iterator, IniFile(), Skipper>  file;
        qi::rule<Iterator, IniSection()>        section;
        qi::rule<Iterator, KVP()>               pair;

        qi::rule<Iterator> value;
        qi::rule<Iterator> key;
    };
} 

int main() 
{
    std::string const ini_section = 
        R"([Section]
        key1 = value1
        key2 = value2
)";

    using It = std::string::const_iterator;
    client::ini_grammar<It> grammar;
    client::IniFile iniFile;

    It iter = ini_section.begin(), end = ini_section.end();
    bool r = parse(iter, end, grammar, iniFile);

    if (iter == end) 
    {
        std::cout << "-------------------------\n";
        std::cout << "Parsing succeeded\n";
        std::cout << "-------------------------\n";

        for (const auto& section : iniFile.sections)
        {
            std::cout << "[" << section.name << "]\n";
            for (const auto& kvp : section.pair)
            {
                std::cout << kvp.first << " = " << kvp.second << "\n";
            }
        }
    } 
    else 
    {
        std::cout << "-------------------------\n";
        std::cout << "Parsing failed\n";
        std::cout << "Stopped at position: " << std::distance(ini_section.begin(), iter) << std::endl;
        std::cout << "\nRemaining input unparsed:\n" << std::string(iter, end);
        std::cout << "-------------------------\n";
    }
}

The problem is that the code outputs a gigantic error, and I don't understand any of it. I think that most part of the error is irrelevant for solving the problem, so I'll give the parts that I think are important.

The error

boost_1_84_0/boost/spirit/home/support/container.hpp:130:12: error: no type named ‘value_type’ in ‘struct client::IniFile’
  130 |     struct container_value
      |            ^~~~~~~~~~~~~~~

boost_1_84_0/boost/spirit/home/qi/detail/pass_container.hpp:320:66: error: no type named ‘type’ in ‘struct boost::spirit::traits::container_value<client::IniFile, void>’
  320 |             typedef typename traits::container_value<Attr>::type value_type;
      |                                                                  ^~~~~~~~~~
boost_1_84_0/boost/spirit/home/qi/detail/pass_container.hpp:333:15: error: no type named ‘type’ in ‘struct boost::spirit::traits::container_value<client::IniFile, void>’
  333 |             > predicate;
      |               ^~~~~~~~~

boost_1_84_0/boost/spirit/home/support/container.hpp:130:12: error: no type named ‘value_type’ in ‘struct client::IniSection’
  130 |     struct container_value
      |            ^~~~~~~~~~~~~~~

boost/boost_1_84_0/boost/spirit/home/qi/detail/pass_container.hpp:320:66: error: no type named ‘type’ in ‘struct boost::spirit::traits::container_value<client::IniSection, void>’
  320 |             typedef typename traits::container_value<Attr>::type value_type;
      |
boost/boost_1_84_0/boost/spirit/home/qi/detail/pass_container.hpp:333:15: error: no type named ‘type’ in ‘struct boost::spirit::traits::container_value<client::IniSection, void>’
  333 |             > predicate;
      |               ^~~~~~~~~

I'm using the code provided on the answers of this question as a reference. I tried to use BOOST_FUSION_ADAPT_STRUCT to address the issue, but I think it does not relate to the problem, it just created more errrors.

1

There are 1 best solutions below

1
sehe On

It looks like you've been shifting things around since last time. Some of the shifts have caused confusion.

E.g.:

using Section = std::map<Key, Value>;

struct IniSection {
    std::string name;
    Section     pair;
};

There's a strong disconnect between the member name pair and its type Section.

Now instead of std::vector<IniSection> I would 100% use an associative container like I showed before:

using IniFile = std::multimap<Key, Section>;

But let's for the moment assume that you really really want to retain the input order just for sections (but not for keys, apparently). The logical change would be

using IniFile = std::vector<std::pair<Key, Section> >;

Introducing your own type IniSection and IniFile means you have to teach Qi how to propagate attributes to them. I can show you that, but first let's keep it simple:

namespace client {
    using Key     = std::string;
    using Value   = std::string;
    using Entries = std::map<Key, Value>;
    using Section = std::pair<Key, Entries>;
    using IniFile = std::vector<Section>;
}; // namespace client

However, somehow your value and key rules have dropped their attributes. This quite simply means that they cannot expose their attribute at all:

    qi::rule<Iterator> value;
    qi::rule<Iterator> key;

Therefore no automatic propagation will occur anymore, because there's nothing to propagate. By definition all your data structures are incompatible with "no attributes". Restoring those first from my previous answer:

    // lexemes
    qi::rule<Iterator, Key()>   key;
    qi::rule<Iterator, Value()> value;

Now you include

#include <boost/fusion/include/adapt_struct.hpp>

That also means you stopped including

#include <boost/fusion/include/std_pair.hpp>

Let's also restore that from my previous answer:

#include <boost/fusion/adapted.hpp> // includes both

At this point things compile again, but it's not parsing the entire section anymore. Looking more closely, that's because you dropped the skipper from the IniSection and KVP rules. (WHY?)

    qi::rule<Iterator, IniSection()>        section;
    qi::rule<Iterator, KVP()>               pair;

Restoring that from my previous answer, we get a full parse:

Live On Coliru

#include <boost/fusion/adapted.hpp>
#include <boost/spirit/include/qi.hpp>
#include <iostream>
#include <map>

namespace client {
    using Key     = std::string;
    using Value   = std::string;
    using Entries = std::map<Key, Value>;
    using Section = std::pair<Key, Entries>;
    using IniFile = std::vector<Section>;
}; // namespace client

namespace client {
    namespace qi = boost::spirit::qi;

    template <typename Iterator> struct ini_grammar : qi::grammar<Iterator, IniFile()> {
        ini_grammar() : ini_grammar::base_type(start) {
            skipper = qi::blank | '#' >> *(qi::char_ - qi::eol);

            key     = +qi::char_("a-zA-Z_0-9");
            value   = *(qi::char_ - qi::eol);
            entry   = key >> '=' >> value;
            section = '[' >> key >> ']' >> +qi::eol >> *(entry >> +qi::eol);
            file    = *section;

            start   = qi::skip(copy(skipper))[file];
        }
        using Skipper = qi::rule<Iterator>;
        using Entry   = std::pair<Key, Value>;
        Skipper skipper;

        qi::rule<Iterator, IniFile()>          start;
        qi::rule<Iterator, IniFile(), Skipper> file;
        qi::rule<Iterator, Section(), Skipper> section;
        qi::rule<Iterator, Entry(), Skipper>   entry;

        qi::rule<Iterator, Key()>   value;
        qi::rule<Iterator, Value()> key;
    };
} // namespace client

int main() {
    for (std::string const input : {
             R"([Section]
                key1 = value1
                key2 = value2

                [Section10]
                key3 = value3

                [Section3] # let's see that order is preserved
                key4 = value4
        )",
         }) {

        std::cout << "-------------------------\n";
        using It = std::string::const_iterator;
        client::ini_grammar<It> grammar;
        client::IniFile         iniFile;

        It   iter = input.begin(), end = input.end();
        bool r = parse(iter, end, grammar, iniFile);

        if (r) {
            std::cout << "Parsing succeeded\n";

            for (auto const& [name, entries] : iniFile) {
                std::cout << "[" << name << "]\n";
                for (auto const& [k,v] : entries) {
                    std::cout << k << " = " << v << "\n";
                }
            }
        } else {
            std::cout << "Parsing failed\n";
        }

        if (iter!=end) {
            std::cout << "Stopped at position: " << std::distance(input.begin(), iter) << std::endl;
            std::cout << "\nRemaining input unparsed:\n" << std::string(iter, end);
        }
    }
}

Printing

-------------------------
Parsing succeeded
[Section]
key1 = value1
key2 = value2
[Section10]
key3 = value3
[Section3]
key4 = value4

Side Notes

You no longer even checked the parse result (r) in main. Nor do you incorporate the check for eoi into the parser. Adding those finishing touches:

Live On Coliru

#include <boost/fusion/adapted.hpp>
#include <boost/spirit/include/qi.hpp>
#include <iostream>
#include <map>

namespace client {
    using Key     = std::string;
    using Value   = std::string;
    using Entries = std::map<Key, Value>;
    using Section = std::pair<Key, Entries>;
    using IniFile = std::vector<Section>;
}; // namespace client

namespace client {
    namespace qi = boost::spirit::qi;

    template <typename Iterator> struct ini_grammar : qi::grammar<Iterator, IniFile()> {
        ini_grammar() : ini_grammar::base_type(start) {
            skipper = qi::blank | '#' >> *(qi::char_ - qi::eol);

            key     = +qi::char_("a-zA-Z_0-9");
            value   = *(qi::char_ - qi::eol);
            entry   = key >> '=' >> value >> (+qi::eol | qi::eoi);
            section = '[' >> key >> ']' >> (+qi::eol | qi::eoi) >> *entry;
            file    = *section;

            start = qi::skip(copy(skipper))[ //
                file >> qi::eoi              //
            ];
        }

      private:
        using Skipper = qi::rule<Iterator>;
        using Entry   = std::pair<Key, Value>;
        Skipper skipper;

        qi::rule<Iterator, IniFile()>          start;
        qi::rule<Iterator, IniFile(), Skipper> file;
        qi::rule<Iterator, Section(), Skipper> section;
        qi::rule<Iterator, Entry(), Skipper>   entry;

        qi::rule<Iterator, Key()>   value;
        qi::rule<Iterator, Value()> key;
    };
} // namespace client

int main() {
    using It = std::string::const_iterator;
    static client::ini_grammar<It> const grammar;

    for (std::string const input :
         {
             R"([Section]
                key1 = value1
                key2 = value2

                [Section10]
                key3 = value3

                [Section3] # let's see that order is preserved
                key4 = value4
        )",
             R"()",
             R"(oops=bad)",
         }) //
    {
        std::cout << "-------------------------\n";
        client::IniFile         iniFile;

        if (parse(input.begin(), input.end(), grammar, iniFile)) {
            std::cout << "Parsing succeeded\n";

            for (auto const& [name, entries] : iniFile) {
                std::cout << "[" << name << "]\n";
                for (auto const& [k,v] : entries) {
                    std::cout << k << " = " << v << "\n";
                }
            }
        } else {
            std::cout << "Parsing failed\n";
        }
    }
}

Printing the expected output

-------------------------
Parsing succeeded
[Section]
key1 = value1
key2 = value2
[Section10]
key3 = value3
[Section3]
key4 = value4
-------------------------
Parsing succeeded
-------------------------
Parsing failed

SUMMARY

You are doing good toying with the code to fit your needs as well as to "make it your own", i.e. understand it fully. However, along the way you have accrued a combination of many interfering changes that together made it impossible for yourself to see what went wrong. I have a suspicion, one thing led to another¹ and here you are.

The lesson there is: by all means go and tweak the code. A programmer cannot function if they fear to touch the code. However, help yourself with two basic principles:

  1. have test cases
  2. change only one thing at a time
  3. check the test cases at each case.

This implies you can never have a change that fails to compile (you just undo that change) and will immediately spot when you break something. That doesn't necessarily mean you have to undo the change. Perhaps you needed to compensate for a change somewhere else in the code. Regardless, do not change anything unrelated until all tests pass again.

This is the Way Of The Enlightened Programmer.


¹ and perhaps you started accepting some suggestions from Copilot?