Please review my ini file parser (and potentially some general config file formats).
This code is a continuation of this review:
I have tried to incorporate all the suggestions including write to file support. I did not take on the regex suggestion. I didn't really see the need for using it in this application.
config.hpp
#ifndef CONFIG_HPP_
#define CONFIG_HPP_
#include <string>
#include <istream>
#include <unordered_map>
// class to encapsulate ini file style configuration text file
class config
{
public:
// key=value pair within a section
typedef std::pair<std::string, std::string> keyvalue;
// [section1] - top level
typedef std::unordered_map< std::string, std::unordered_map<std::string, std::string> > sections;
// construct with a filename
config(const std::string& filename);
// construct with a stream
config(std::istream& strm);
// no copying allowed
config(const config&) = delete;
config& operator = (const config&) = delete;
// return a copy of whole structure
const sections get_sections() const;
// get a value
const std::string get_value(const std::string& sectionname, const std::string& keyname) const;
// set a value
void set_value(const std::string& sectionname, const std::string& keyname, const std::string& value);
// any changes will not be committed to underlying file until save_changes() is called
bool save_changes(const std::string& filename);
// standard way to iterate underlying container
typedef sections::iterator iterator;
iterator begin() { return sections_.begin(); }
iterator end() { return sections_.end(); }
private:
// parse stream into config data structure
void parse(std::istream& strm);
// top level of data structure - hash table
sections sections_;
};
#endif // CONFIG_HPP_
config.cpp
#include "config.hpp"
#include <iostream>
#include <fstream>
#include <string>
#include <algorithm>
#include <unordered_map>
#include <cstdio> // std::remove
#include <cassert>
// 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) {
std::ifstream fstrm;
fstrm.open(filename);
if (!fstrm)
throw std::invalid_argument(filename + " could not be opened");
parse(fstrm);
}
config::config(std::istream& strm) {
parse(strm);
}
const config::sections config::get_sections() const {
return sections_;
}
const std::string config::get_value(const std::string& sectionname, const std::string&keyname) const {
auto sect = sections_.find(sectionname);
if (sect != sections_.end()) {
auto key = sect->second.find(keyname);
if (key != sect->second.end()) {
return key->second;
}
}
return "";
}
void config::set_value(const std::string& sectionname, const std::string& keyname, const std::string& value) {
auto sect = sections_.find(sectionname);
if (sect == sections_.end()) {
auto ref = sections_[sectionname];
sect = sections_.find(sectionname);
}
assert(sect != sections_.end());
auto key = sect->second.find(keyname);
if (key != sect->second.end()) {
key->second = value;
}
else {
sect->second.insert(std::make_pair(keyname, value));
}
}
bool config::save_changes(const std::string& filename) {
bool success(false);
// delete existing file - if exists
std::remove(filename.c_str());
// iterate thru sections_ saving data to a file
std::string currentsectionname;
std::ofstream fstrm;
fstrm.open(filename);
if (fstrm.good()) {
for (auto heading : sections_) {
fstrm << '[' << heading.first << "]\n";
for (auto kvs : heading.second) {
fstrm << kvs.first << '=' << kvs.second << '\n';
}
fstrm << '\n';
}
success = true;
}
return success;
}
void config::parse(std::istream& strm) {
std::string currentsectionname;
for (std::string line; std::getline(strm, line);)
{
// skip comments
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 add it to hashtable
currentsectionname = line.substr(1, end - 1);
sections_[currentsectionname];
}
// else section has no closing ] char - ignore
}
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));
sections_[currentsectionname].insert(std::make_pair(name, value));
}
//else no key value delimitter - ignore
}
} // for
}
exercising code.
#include "config.hpp"
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <algorithm>
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
try {
config cfg("test1.ini");
std::cout << "email=" << cfg.get_value("user", "email") << '\n';
// iterate sections in ini file
for (auto& sec : cfg) {
std::cout << "section name: " << sec.first << std::endl;
// iterate key value pairs in section
for (auto& data : sec.second) {
std::cout << "key: " << data.first << ", value: " << data.second << std::endl;
}
}
const config::sections configs = cfg.get_sections();
for (auto heading : configs) {
std::cout << "heading: " << heading.first << std::endl;
for (config::keyvalue kvs : heading.second) {
std::cout << "first: " << kvs.first << " second: " << kvs.second << std::endl;
}
}
// now make a change and save changes
cfg.set_value("user", "colour", "red");
bool saved = cfg.save_changes("test1.ini");
std::cout << (saved ? "file successfully updated\n" : "error saving changes to file\n");
// construct with a stream test
std::stringstream ss{ "[protocol]\nversion = 6 \n\n[user]\nname = Bob Smith \n"
"email = [email protected] \nactive = true\n\npi = 3.14159" };
config cfg2(ss);
// now save as a persistent file
cfg2.save_changes("stuff.ini");
config cfg3("stuff.ini");
const std::string ver = cfg3.get_value("protocol", "version");
std::cout << "construct with stream test " << (ver == "6" ? "passed" : "failed") << std::endl;
}
catch (std::exception& e) {
std::cout << "Exception occurred: " << e.what() << std::endl;
}
}