// 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';
}
}