Code View

// SPDX-License-Identifier: MIT
#include <cparse/Parser.hh>

#include <cctype>
#include <fstream>
#include <iostream>
#include <map>
#include <optional>
#include <string>

using Config = std::map<std::string, std::map<std::string, std::string>>;
using fedem::parser::ParseError;

class KeyValueParser : public fedem::parser::Parser
{
public:
    std::string getGrammarName() const noexcept override { return "kv-config"; }

    Config const& config() const noexcept { return config_; }

protected:
    bool start() override
    {
        while (true)
        {
            skipCommentsBlock();
            if (!getCursor().isValid()) return true;

            char const c = getCursor().safePeek();
            if (c == '\032') return true;         // clean EOF

            if (c == '[')
            {
                if (!parseSection()) return false;
                continue;
            }

            if (std::isalpha(static_cast<unsigned char>(c)) || c == '_')
            {
                if (!parseAssignment(currentSection_)) return false;
                continue;
            }

            recordError(ParseError::Kind::Syntax,
                        std::string{"unexpected character '"} + c + "'");
            return false;
        }
    }

    // Override the base skipComments to eat '#' line comments.
    bool skipComments() override
    {
        if (getCursor().safePeek() != '#') return false;
        while (getCursor().isValid())
        {
            char const c = getCursor().safeGet();
            if (c == '\n' || c == '\032') return true;
        }
        return true;
    }

private:
    bool parseSection()
    {
        unsigned long const line = getCursor().getNextLineNumber();
        unsigned long const col  = getCursor().getNextColumnNumber();

        // If we already had a section open, close it before opening the new one.
        if (!currentSection_.empty())
            leaveScope();

        getCursor().get();                        // consume '['
        enterScope();                             // section is now open
        sectionOpenLine_ = line;
        sectionOpenCol_  = col;

        std::string name;
        while (true)
        {
            char const c = getCursor().safePeek();
            if (c == ']' || c == '\n' || c == '\032') break;
            name += getCursor().safeGet();
        }
        while (!name.empty() && std::isspace(
                   static_cast<unsigned char>(name.back())))
            name.pop_back();

        if (name.empty())
        {
            recordError(ParseError::Kind::Syntax, "empty section name",
                        line, col);
            return false;
        }

        if (!extractCharacter(']'))
        {
            recordError(ParseError::Kind::Syntax,
                        "expected ']' to close section '" + name + "'",
                        line, col);
            return false;
        }

        currentSection_ = name;
        config_[currentSection_];                 // register empty section
        return true;
    }

    bool parseAssignment(std::string const& section)
    {
        unsigned long const line = getCursor().getNextLineNumber();
        unsigned long const col  = getCursor().getNextColumnNumber();

        if (section.empty())
        {
            recordError(ParseError::Kind::Syntax,
                        "assignment outside of any [section]",
                        line, col);
            return false;
        }

        std::string key;
        while (true)
        {
            char const c = getCursor().safePeek();
            if (c == '=' || std::isspace(static_cast<unsigned char>(c))
                || c == '\032') break;
            key += getCursor().safeGet();
        }
        if (key.empty()) {
            recordError(ParseError::Kind::Syntax, "expected identifier", line, col);
            return false;
        }

        skipSpacesOnly();
        if (!extractCharacter('=')) {
            recordError(ParseError::Kind::Syntax, "expected '=' after key '" + key + "'");
            return false;
        }
        skipSpacesOnly();

        std::string value;
        while (true) {
            char const c = getCursor().safePeek();
            if (c == '\n' || c == '\032') break;
            value += getCursor().safeGet();
        }
        while (!value.empty() && std::isspace(
                   static_cast<unsigned char>(value.back())))
            value.pop_back();

        config_[section][key] = value;
        return true;
    }

    // Whitespace-not-including-newline — line boundaries matter here.
    void skipSpacesOnly()
    {
        while (true) {
            char const c = getCursor().safePeek();
            if (c != ' ' && c != '\t') return;
            getCursor().get();
        }
    }

    Config        config_;
    std::string   currentSection_;
    unsigned long sectionOpenLine_{0};
    unsigned long sectionOpenCol_{0};
};

int main(int argc, char** argv)
{
    if (argc < 2)
    {
        std::cerr << "usage: kv <file.ini>\n";
        return 1;
    }

    KeyValueParser p;
    bool const ok = p.parse(argv[1]);

    for (auto const& e : p.errors())
        std::cerr << e.filename << ':' << e.line << ':' << e.col
                  << ": " << e.message << '\n';

    if (!ok) return 1;

    for (auto const& [section, entries] : p.config())
    {
        std::cout << '[' << section << "]\n";
        for (auto const& [k, v] : entries)
            std::cout << "  " << k << " = " << v << '\n';
    }
}