I got frustrated with C++ not being Python, so I put together a .ini file reader for myself. The last C++ code I posted, the feedback was that it's better to use free functions that private static methods when possible, so I tried to apply that here. I also tried to follow the single layer of abstraction principle as dogmatically as possible to see what would happen.
A questionable decision I made was to have, for example, isSectionHeader
assume that isComment
has already been called. I'm starting to think that was a mistake, and that I should have just done
bool isSectionHeader(const std::string& line)
{
if (isComment(line))
{
return false;
}
etc.
}
so feedback in those areas would be nice. Also, because I do not get much chance to explore past C++98, if you see any places where modern language features would help, I would love to hear about it.
Handling whitespace is deliberately ommitted.
ConfigParser.h
#pragma once
#include <map>
#include <string>
#include <vector>
typedef std::map<std::string, std::string> Section;
typedef std::map<std::string, Section> Config;
class ConfigParser
{
public:
ConfigParser(const std::string& pathname);
Section getSection(const std::string& sectionName) const
{
return mSections.at(sectionName);
}
std::string get(const std::string& sectionName, const std::string& key) const
{
return mSections.at(sectionName).at(key);
}
std::vector<std::string> getSections() const;
private:
Config parseFile(const std::string& pathname);
void parseLine(const std::string& line, Config& sections);
void addSection(const std::string& line, Config& sections);
void addKeyValuePair(const std::string& line, Config& sections) const;
std::string mCurrentSection;
const Config mSections;
};
ConfigParser.cpp
#include "ConfigParser.h"
#include <algorithm>
#include <cctype>
#include <stdexcept>
#include <fstream>
#include <string>
#include <sstream>
#include <vector>
// The function in this namespace will go into a custom string util library
namespace stringUtil
{
std::vector<std::string> split(const std::string& string, char delimiter = ' ')
{
std::vector<std::string> result;
std::stringstream stream(string);
std::string word;
while (std::getline(stream, word, delimiter))
{
result.push_back(word);
}
return result;
}
}
namespace
{
bool isComment(const std::string& line)
{
return line[0] == ';';
}
std::string extractSectionName(const std::string& line)
{
return std::string(line.begin() + 1, line.end() - 1);
}
bool isSectionHeading(const std::string& line)
{
if (line[0] != '[' || line[line.size() - 1] != ']')
{
return false;
}
const std::string sectionName = extractSectionName(line);
return std::all_of(sectionName.begin(), sectionName.end(), std::isalpha);
}
bool isKeyValuePair(const std::string& line)
{
// Assume we have already checked if it's a comment or section header.
return std::count(line.begin(), line.end(), '=') == 1;
}
void ensureSectionIsUnique(const std::string& sectionName, const Config& sections)
{
if (sections.count(sectionName) != 0)
{
throw std::runtime_error(sectionName + " appears twice in config file");
}
}
void ensureKeyIsUnique(const std::string& key, const Section& section)
{
if (section.count(key) != 0)
{
throw std::runtime_error(key + " appears twice in section");
}
}
void ensureCurrentSection(const std::string& line, const std::string& currentSection)
{
if (currentSection.empty())
{
throw std::runtime_error(line + " does not occur within a section");
}
}
std::pair<std::string, std::string> parseKeyValuePair(const std::string& line)
{
std::vector<std::string> pair = stringUtil::split(line, '=');
return std::pair <std::string, std::string>(pair[0], pair[1]);
}
}
ConfigParser::ConfigParser(const std::string& pathname) :
mCurrentSection(""),
mSections(parseFile(pathname))
{
}
std::vector<std::string> ConfigParser::getSections() const
{
std::vector<std::string> sectionNames;
for (auto it = mSections.begin(); it != mSections.end(); ++it)
{
sectionNames.push_back(it->first);
}
return sectionNames;
}
Config ConfigParser::parseFile(const std::string& pathname)
{
Config sections;
std::ifstream input(pathname);
std::string line;
while (std::getline(input, line))
{
parseLine(line, sections);
}
return sections;
}
void ConfigParser::parseLine(const std::string& line, Config& sections)
{
if (isComment(line))
{
return;
}
else if (isSectionHeading(line))
{
addSection(line, sections);
}
else if (isKeyValuePair(line))
{
addKeyValuePair(line, sections);
}
}
void ConfigParser::addSection(const std::string& line, Config& sections)
{
const std::string sectionName = extractSectionName(line);
ensureSectionIsUnique(sectionName, sections);
sections.insert(std::pair<std::string, Section>(sectionName, Section()));
mCurrentSection = sectionName;
}
void ConfigParser::addKeyValuePair(const std::string& line, Config& sections) const
{
ensureCurrentSection(line, mCurrentSection);
const auto keyValuePair = parseKeyValuePair(line);
ensureKeyIsUnique(keyValuePair.first, sections.at(mCurrentSection));
sections.at(mCurrentSection).insert(keyValuePair);
}