Please review my ini file parser (and potentially some general config file formats).
The data structure used is a section, see below. Does that seem a good approach?
I was wondering about the name. It will parse ini files ok, but unix config files are so variable that calling the class config might be over selling it a bit.
I don't support mid-line comments. Only comments on a line on their own are well just ignored.
All values are stored as strings. I thought that only the user of the config file will know if a value should be used in his or her application as a string or integer or whatever and onus is on user to convert.
Any comments would be much appreciated. Its intended initial use is for Windows ini files. Parsing the configuration into sections.
config.hpp
#ifndef CONFIG_HPP_
#define CONFIG_HPP_
#include <string>
#include <unordered_map>
#include <list>
struct section
{
std::string name;
std::unordered_map<std::string, std::string> keyvalues;
};
class config
{
public:
config(const std::string& filename);
section* get_section(const std::string& sectionname);
std::list<section>& get_sections();
std::string get_value(const std::string& sectionname, const std::string& keyname);
private:
void parse(const std::string& filename);
std::list<section> sections;
};
#endif // CONFIG_HPP_
config implementation - config.cpp
#include "config.hpp"
#include <iostream>
#include <fstream>
#include <string>
#include <algorithm>
#include <unordered_map>
#include <list>
// trim leading white-spaces
static std::string& ltrim(std::string& s) {
size_t startpos = s.find_first_not_of(" \t\r\n\v\f");
if (std::string::npos != startpos)
{
s = s.substr(startpos);
}
return s;
}
// trim trailing white-spaces
static std::string& rtrim(std::string& s) {
size_t endpos = s.find_last_not_of(" \t\r\n\v\f");
if (std::string::npos != endpos)
{
s = s.substr(0, endpos + 1);
}
return s;
}
config::config(const std::string& filename) {
parse(filename);
}
section* config::get_section(const std::string& sectionname) {
std::list<section>::iterator found = std::find_if(sections.begin(), sections.end(), [sectionname](const section& sect) {
return sect.name.compare(sectionname) == 0; });
return found != sections.end() ? &*found : NULL;
}
std::list<section>& config::get_sections() {
return sections;
}
std::string config::get_value(const std::string& sectionname, const std::string&keyname) {
section* sect = get_section(sectionname);
if (sect != NULL) {
std::unordered_map<std::string, std::string>::const_iterator it = sect->keyvalues.find(keyname);
if (it != sect->keyvalues.end())
return it->second;
}
return "";
}
void config::parse(const std::string& filename) {
section currentsection;
std::ifstream fstrm;
fstrm.open(filename);
if (!fstrm)
throw std::invalid_argument(filename + " could not be opened");
for (std::string line; std::getline(fstrm, line);)
{
// if a comment
if (!line.empty() && (line[0] == ';' || line[0] == '#')) {
// allow both ; and # comments at the start of a line
}
else if (line[0] == '[') {
/* A "[section]" line */
size_t end = line.find_first_of(']');
if (end != std::string::npos) {
// this is a new section so if we have a current section populated, add it to list
if (!currentsection.name.empty()) {
sections.push_back(currentsection); // copy
currentsection.name.clear(); // clear section for re-use
currentsection.keyvalues.clear();
}
currentsection.name = line.substr(1, end - 1);
}
else {
// section has no closing ] char
}
}
else if (!line.empty()) {
/* Not a comment, must be a name[=:]value pair */
size_t end = line.find_first_of("=:");
if (end != std::string::npos) {
std::string name = line.substr(0, end);
std::string value = line.substr(end + 1);
ltrim(rtrim(name));
ltrim(rtrim(value));
currentsection.keyvalues[name] = value;
}
else {
// no key value delimitter
}
}
} // for
// if we are out of loop we add last section
// this is a new section so if we have a current section populated, add it to list
if (!currentsection.name.empty()) {
sections.push_back(currentsection); // copy
currentsection.name = "";
currentsection.keyvalues.clear();
}
}
some code to do a simple test
#include "config.hpp"
#include <iostream>
#include <fstream>
#include <string>
void generate_config(const std::string& filename) {
std::ofstream ostrm;
ostrm.open(filename);
if (ostrm) {
ostrm << "[protocol]\nversion = 6 \n\n[user]\nname = Bob Smith \nemail = [email protected] \nactive = true\n\npi = 3.14159";
}
}
int main() {
// generate test file
generate_config("test1.ini");
// retrieve some information from config file
config cfg("test1.ini");
section* usersection = cfg.get_section("user");
if (usersection != NULL) {
std::cout << "section name: " << usersection->name << std::endl;
std::cout << "email=" << cfg.get_value("user", "email") << '\n';
}
}