The below is a C++ parser for a reasonably simple INI grammar (although my understanding is that there isn't an official spec as such).
My grammar is roughly:
comments: (optional whitespace){;#}
section: (optional whitespace)[{printable ASCII}]
keyval: (optional whitespace){nows printable ASCII}={printable ASCII}
ini_parser.h
#pragma once
#include <map>
#include <string>
class IniParser {
public:
IniParser(const std::string &path);
template<typename T>
T get(const std::string &key, const std::string §ion);
//get values in 'default' section
template<typename T>
T get(const std::string &key) { return get<T>(key, ""); }
private:
using SectionMap = std::map<std::string, std::string>;
using IniMap = std::map<std::string, SectionMap>;
IniMap inimap;
};
ini_parser.cpp
#include <cctype>
#include <cstdint>
#include <fstream>
#include <limits>
#include <vector>
#include <sstream>
#include "ini_parser.h"
namespace {
bool iskey(char c){
return isalnum(c) || ispunct(c);
}
bool isval(char c){
return isalnum(c) || ispunct(c) || isblank(c);
}
}
IniParser::IniParser(const std::string &path) {
enum class State {
init,
section,
key,
value,
skipline
} state = State::init;
std::ifstream f{path, std::ios::in};
if(!f.is_open()){
throw std::runtime_error("file doesn't exist");
}
std::vector<char> buf;
std::string section, key;
char c;
auto err_helper = [&](const std::string §ion){
std::ostringstream ss;
ss << "invalid character '" << c << "'(" << static_cast<int>(c) << ") in parsestate=" << section << ", at " << path << ":" << f.tellg();
throw std::runtime_error(ss.str());
};
while(f.get(c)){
switch (state){
case State::skipline:
if (c == '\n'){
state = State::init;
}
break;
case State::init:
if (c == ';' || c == '#'){
state = State::skipline;
} else if (c == '[') {
state = State::section;
} else if (std::isspace(c)){
//pass
} else if (iskey(c)){
state = State::key;
buf.push_back(c);
} else {
err_helper("init");
}
break;
case State::section:
if (c == ']'){
section = std::string(buf.begin(), buf.end());
buf.clear();
state = State::skipline;
} else if (isval(c)){
buf.push_back(c);
} else {
err_helper("section");
}
break;
case State::key:
if (c == '='){
key = std::string(buf.begin(), buf.end());
buf.clear();
state = State::value;
} else if (iskey(c)) { //disallow whitespace in key
buf.push_back(c);
} else {
err_helper("key");
}
break;
case State::value:
if (c == '\n'){
const std::string token = std::string(buf.begin(), buf.end());
buf.clear();
inimap[section][key] = token;
state = State::init;
} else if (isval(c)){
buf.push_back(c);
} else {
err_helper("value");
}
break;
}
}
if(state != State::init){
err_helper("eof");
}
}
template<>
std::string IniParser::get(const std::string &key, const std::string §ion){
return inimap.at(section).at(key);
}
template<>
int IniParser::get(const std::string &key, const std::string §ion){
return std::stoi(inimap.at(section).at(key));
}
template<>
std::uint16_t IniParser::get(const std::string &key, const std::string §ion){
auto val = std::stoul(inimap.at(section).at(key));
if(val > std::numeric_limits<std::uint16_t>::max()){
throw std::overflow_error("value too large for type");
}
return static_cast<std::uint16_t>(val);
}
template<>
double IniParser::get(const std::string &key, const std::string §ion){
return std::stod(inimap.at(section).at(key));
}
//add more specializations as neccessary
After completing this it occurred to me that a regex approach might have been superior (or at least, terser), but perhaps the error reporting is better here.