2

Looking for some C++ library (like boost::program_options) that is able to return line number of an INI file, where the given option or section was found.

Use cases:

  1. I ask that library to find value "vvv" in a section "[SSS]". Library returns line number where "vvv" in section "[SSS]" is found, or -1. It gives me an ability to say "line 55: vvv must be < 256".

  2. I iterate INI file for sections and validate their names. When some wild secsion is found, i tell: "line 55: section [Hahaha] is unknown".

update: i know about "INI is older than mammoth", but currently i have to port large windows project to cross-platform and cannot get rid of .ini files soon.

pavelkolodin
  • 2,859
  • 3
  • 31
  • 74
  • INI files are a deprecated windows-specific format - it's kind of odd to be looking at cross-platform ways to parse them. – Joris Timmermans Dec 02 '11 at 16:01
  • 3
    @MadKeithV: While that format was most popular in the windows world, there's nothing prevent it from being used on every platform that has text files (and that most certainly happened). Moreover, most variants are simple enough to be practical to parse yourself even if there is no cross-platform library already. –  Dec 02 '11 at 16:09
  • @delnan - I didn't say there was anything to prevent you from using it, just that it was odd to want to use it. If you're stuck in that unfortunate reality, you'll have to parse INI files, but if you have a choice, why not pick a better format? – Joris Timmermans Dec 02 '11 at 16:14
  • @MadKeithV: Yes, because e.g. PHP and MySQL are a windows specific tools. Oh wait. – Billy ONeal Dec 02 '11 at 16:15
  • 3
    @MadKeithV: I may be old-fashioned, but I don't see anything wrong with INI for simple key-value pairs that may be categorized in a flat namespace. No need to pull in a full XML (JSON, YAML, whatever) parser (and possibly another library for easier handling - ever processed a raw XML DOM?). Which are better formats, and how are they better? –  Dec 02 '11 at 16:17
  • I think you've misunderstood what Stack Overflow is for. – Lightness Races in Orbit Dec 02 '11 at 16:22
  • For fun and practice, I did an implementation based on Boost Spirit. – sehe Dec 03 '11 at 04:05

1 Answers1

7

Once again, took the opportunity to play with Boost Spirit. This time I got to play with line_pos_iterator.

Here is the fruit of my labour: https://gist.github.com/1425972

  • When POSITIONINFO == 0
    • input is streaming
    • output is raw strings (well, map<string, map<string, string> > for the sections)
  • When POSITIONINFO == 1

    • input is buffered
    • output is textnode_t:

      struct textnode_t {
          int sline, eline, scol, ecol;
          string_t text;
      };
      

      This means that the resulting map<textnode_t, map<textnode_t, textnode_t> > is able to report exactly what (line,col) start and end points mark the individual text nodes. See test output for a demo

    • Comments (#, /* ... */ style) have been implemented

    • Whitespace is 'tolerated'

      name = value # use a comment to force inclusion of trailing whitespace alternative = escape\ with slash\

    • De-escaping of the slashes is left as an exercise

    • Errors are also reported with full position info if enabled

NOTE C++11 support is NOT required, but I used it to dump the result of the parse. I'm too lazy to write it with C++03 verbose iterator style. :)

All code, makefile, example.ini can be found here: https://gist.github.com/1425972

Code

/* inireader.h */
#pragma once

#define POSITIONINFO 0

#include <map>
#include <string>
#include <iterator>
#include <boost/tuple/tuple_comparison.hpp>

template <typename S=std::string, typename Cmp=std::less<S> >
    class IniFile
{
    public:
    IniFile(Cmp cmp=Cmp()) : _cmp(cmp) 
        {}

    IniFile(const std::string& filename, Cmp cmp=Cmp()) : _cmp(cmp)
        { open(filename); }

    void open(const std::string& filename);

    typedef S string_t;
#if POSITIONINFO
    struct textnode_t
    {
        int sline, eline,
            scol, ecol;
        string_t text;

        operator const string_t&() const { return text; }
        friend std::ostream& operator<<(std::ostream& os, const textnode_t& t)
        { 
            os << "[L:" << t.sline << ",C" << t.scol << " .. L" << t.eline << ",C" << t.ecol << ":";
            for (typename string_t::const_iterator it=t.text.begin(); it!=t.text.end(); ++it)
            switch (*it)
            {
                case '\r' : os << "\\r"; break;
                case '\n' : os << "\\n"; break;
                case '\t' : os << "\\t"; break;
                case '\0' : os << "\\0"; break;
                default:    os << *it  ; break;
            }
            return os << "]"; 
        }

        bool operator<(const textnode_t& o) const 
            { return boost::tie(text/*, sline, eline, scol, ecol*/) <
                     boost::tie(o.text/*, o.sline, o.eline, o.scol, o.ecol*/); }

        textnode_t() : sline(0), eline(0), scol(0), ecol(0) { }
    };
#else
    typedef string_t textnode_t;
#endif

    typedef std::pair<textnode_t, textnode_t>   keyvalue_t;
    typedef std::map<textnode_t, textnode_t>    section_t;
    typedef std::map<textnode_t, section_t> sections_t;

  private:
    Cmp _cmp;
};

///////////////////////////////////////
// template implementation
//#define BOOST_SPIRIT_DEBUG

#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/support_istream_iterator.hpp>
#include <boost/spirit/include/support_line_pos_iterator.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/fusion/adapted/std_pair.hpp>
#include <fstream>

namespace qi = boost::spirit::qi;
namespace phx= boost::phoenix;

namespace inireader
{
    struct printer
    {
        printer(std::ostream& os) : _os(os) {}
        std::ostream& _os;

        typedef boost::spirit::utf8_string string;

        void element(string const& tag, string const& value, int depth) const
        {
            for (int i = 0; i < (depth*4); ++i) // indent to depth
                _os << ' ';

            _os << "tag: " << tag;
            if (value != "")
                _os << ", value: " << value;
            _os << std::endl;
        }
    };

    void print_info(std::ostream& os, boost::spirit::info const& what)
    {
        using boost::spirit::basic_info_walker;

        printer pr(os);
        basic_info_walker<printer> walker(pr, what.tag, 0);
        boost::apply_visitor(walker, what.value);
    }

    template <typename It, typename Skipper, typename Ini>
        struct Grammar : qi::grammar<It, typename Ini::sections_t(), Skipper>
    {
        typedef typename Ini::string_t string_t;
        typedef typename Ini::textnode_t textnode_t;

        struct textbuilder
        {
            template <typename> struct result { typedef textnode_t type; };

            textbuilder(It begin) : _begin(begin) { }

            textnode_t operator()(const boost::iterator_range<It>& iters) const
            {
#if !POSITIONINFO
                return textnode_t(std::begin(iters), std::end(iters));
#else
                using boost::spirit::get_line;
                using boost::spirit::get_line_start;
                using boost::spirit::get_column;

                textnode_t element;
                element.text  = string_t       (std::begin(iters)  , std::end(iters));
                element.sline = get_line       (std::begin(iters));
                element.eline = get_line       (std::end(iters));
                It sol        = get_line_start (_begin             , std::begin(iters));
                element.scol  = get_column     (sol                , std::begin(iters));
                element.ecol  = get_column     (sol                , std::end(iters));

                return element;
#endif
            }

          private: 
            const It _begin;
        } makenode;

        Grammar(It begin) : Grammar::base_type(inifile), makenode(begin)
        {
            using namespace qi;
            txt_ch = (lit('\\') > char_) | (char_ - (eol | '#' | "/*"));

            key     = raw [ lexeme [ +(txt_ch - char_("=")) ] ] [ _val = phx::bind(makenode, _1) ];
            value   = raw [ lexeme [ +txt_ch ] ]                [ _val = phx::bind(makenode, _1) ];
            pair   %= key > '=' > value;

            heading  = ('[' > raw [ +~char_(']') ] > ']') [ _val = phx::bind(makenode, _1) ];
            section %= heading >> +eol >> -((pair-heading) % +eol);
            inifile %= -(section % +eol) >> *eol > eoi;

            comment = 
                  ('#' >> *(char_ - eol))
                | ("/*" > *(char_ - "*/") > "*/");

            //BOOST_SPIRIT_DEBUG_NODE(comment);
            //BOOST_SPIRIT_DEBUG_NODE(txt_ch);
            BOOST_SPIRIT_DEBUG_NODE(heading);
            BOOST_SPIRIT_DEBUG_NODE(section);
            BOOST_SPIRIT_DEBUG_NODE(key);
            BOOST_SPIRIT_DEBUG_NODE(value);
            BOOST_SPIRIT_DEBUG_NODE(pair);
            BOOST_SPIRIT_DEBUG_NODE(inifile);
        }

        typedef typename Ini::keyvalue_t keyvalue_t;
        typedef typename Ini::section_t  section_t;
        typedef typename Ini::sections_t sections_t;
        typedef typename string_t::value_type Char;
        qi::rule<It>                        comment;
        qi::rule<It, Char()>                txt_ch;
        qi::rule<It, textnode_t(), Skipper> key, value, heading;
        qi::rule<It, keyvalue_t(), Skipper> pair;
        qi::rule<It, std::pair<textnode_t, section_t>(), Skipper> section;
        qi::rule<It, sections_t(), Skipper> inifile;
    };

    template <typename It, typename Builder>
        typename Builder::template result<void>::type
            fragment(const It& first, const It& last, const Builder& builder)
            {
                size_t len = std::distance(first, last);
                It frag_end = first;
                std::advance(frag_end, std::min(10ul, len));
                return builder(boost::iterator_range<It>(first, frag_end));
            }
}

template <typename S, typename Cmp>
void IniFile<S, Cmp>::open(const std::string& filename)
{
    using namespace qi;

    std::ifstream ifs(filename.c_str());
    ifs.unsetf(std::ios::skipws);

#if POSITIONINFO
    typedef std::string::const_iterator RawIt;
    typedef boost::spirit::line_pos_iterator<RawIt> It;

    typedef rule<It> Skipper;

    std::string buffer(std::istreambuf_iterator<char>(ifs), (std::istreambuf_iterator<char>()));
    It f(buffer.begin()), l(buffer.end());
#else
    typedef boost::spirit::istream_iterator It;
    typedef rule<It> Skipper;

    It f(ifs), l;
#endif

    inireader::Grammar<It, Skipper, IniFile<S, Cmp> > grammar(f);
    Skipper skip = char_(" \t") | grammar.comment;

    try
    {
        sections_t data;
        bool ok = phrase_parse(f, l, grammar, skip, data);
        if (ok)
        {
            std::cout << "Parse success!" << std::endl;

///////// C++11 specific features for quick display //////////
            for (auto& section : data)
            {
                std::cout << "[" << section.first << "]" << std::endl;
                for (auto& pair : section.second)
                    std::cout << pair.first << " = " << pair.second << std::endl;
///////// End C++11 specific /////////////////////////////////
            }
        } else
        {
            std::cerr << "Parse failed" << std::endl;
        }
    } catch (const qi::expectation_failure<It>& e)
    {
        std::cerr << "Exception: " << e.what() << 
              " " << inireader::fragment(e.first, e.last, grammar.makenode) << "... ";
        inireader::print_info(std::cerr, e.what_);
    }
    if (f!=l)
    {
        std::cerr << "Stopped at: '" << inireader::fragment(f, l, grammar.makenode) << "'" << std::endl;
    }
}

Demo Input

[Cat1]
name1=100 #skipped

name2=200 \#not \\skipped
name3=   dhfj dhjgfd/* skipped

*/

[Cat_2]
UsagePage=9
Usage=19
Offset=0x1204

/*
[Cat_2_bak]
UsagePage=9
Usage=19
Offset=0x1204
*/

[Cat_3]
UsagePage=12
Usage=39
#Usage4=39
Offset=0x12304

Demo Output (POSITIONINFO == 0)

Parse success!
[Cat1]
name1 = 100 
name2 = 200 \#not \\skipped
name3 = dhfj dhjgfd
[Cat_2]
Offset = 0x1204
Usage = 19
UsagePage = 9
[Cat_3]
Offset = 0x12304
Usage = 39
UsagePage = 12

Demo Output (POSITIONINFO == 1)

Parse success!
[[L:1,C2 .. L1,C6:Cat1]]
[L:2,C2 .. L2,C7:name1] = [L:2,C8 .. L2,C12:100 ]
[L:6,C2 .. L6,C7:name2] = [L:6,C8 .. L6,C27:200 \#not \\skipped]
[L:7,C2 .. L7,C7:name3] = [L:7,C11 .. L7,C22:dhfj dhjgfd]
[[L:13,C3 .. L13,C8:Cat_2]]
[L:16,C2 .. L16,C8:Offset] = [L:16,C9 .. L16,C15:0x1204]
[L:15,C2 .. L15,C7:Usage] = [L:15,C8 .. L15,C10:19]
[L:14,C2 .. L14,C11:UsagePage] = [L:14,C12 .. L14,C13:9]
[[L:25,C3 .. L25,C8:Cat_3]]
[L:29,C2 .. L29,C8:Offset] = [L:29,C9 .. L29,C16:0x12304]
[L:27,C2 .. L27,C7:Usage] = [L:27,C8 .. L27,C10:39]
[L:26,C2 .. L26,C11:UsagePage] = [L:26,C12 .. L26,C14:12]
sehe
  • 374,641
  • 47
  • 450
  • 633