Creating a simple configuration file and parser in C++

I am trying to create a simple configuration file that looks like this

url = http://mysite.com
file = main.exe
true = 0

when the program runs, I would like it to load the configuration settings into the programs variables listed below.

string url, file;
bool true_false;

I have done some research and this link seemed to help (nucleon's post) but I can't seem to get it to work and it is too complicated to understand on my part. Is there a simple way of doing this? I can load the file using ifstream but that is as far as I can get on my own. Thanks!

197574 次浏览

Why not trying something simple and human-readable, like JSON (or XML) ?

There are many pre-made open-source implementations of JSON (or XML) for C++ - I would use one of them.

And if you want something more "binary" - try BJSON or BSON :)

A naive approach could look like this:

#include <map>
#include <sstream>
#include <stdexcept>
#include <string>


std::map<std::string, std::string> options; // global?


void parse(std::istream & cfgfile)
{
for (std::string line; std::getline(cfgfile, line); )
{
std::istringstream iss(line);
std::string id, eq, val;


bool error = false;


if (!(iss >> id))
{
error = true;
}
else if (id[0] == '#')
{
continue;
}
else if (!(iss >> eq >> val >> std::ws) || eq != "=" || iss.get() != EOF)
{
error = true;
}


if (error)
{
// do something appropriate: throw, skip, warn, etc.
}
else
{
options[id] = val;
}
}
}

Now you can access each option value from the global options map anywhere in your program. If you want castability, you could make the mapped type a boost::variant.

In general, it's easiest to parse such typical config files in two stages: first read the lines, and then parse those one by one.
In C++, lines can be read from a stream using std::getline(). While by default it will read up to the next '\n' (which it will consume, but not return), you can pass it some other delimiter, too, which makes it a good candidate for reading up-to-some-char, like = in your example.

For simplicity, the following presumes that the = are not surrounded by whitespace. If you want to allow whitespaces at these positions, you will have to strategically place is >> std::ws before reading the value and remove trailing whitespaces from the keys. However, IMO the little added flexibility in the syntax is not worth the hassle for a config file reader.

const char config[] = "url=http://example.com\n"
"file=main.exe\n"
"true=0";


std::istringstream is_file(config);


std::string line;
while( std::getline(is_file, line) )
{
std::istringstream is_line(line);
std::string key;
if( std::getline(is_line, key, '=') )
{
std::string value;
if( std::getline(is_line, value) )
store_line(key, value);
}
}

(Adding error handling is left as an exercise to the reader.)

As others have pointed out, it will probably be less work to make use of an existing configuration-file parser library rather than re-invent the wheel.

For example, if you decide to use the Config4Cpp library (which I maintain), then your configuration file syntax will be slightly different (put double quotes around values and terminate assignment statements with a semicolon) as shown in the example below:

# File: someFile.cfg
url = "http://mysite.com";
file = "main.exe";
true_false = "true";

The following program parses the above configuration file, copies the desired values into variables and prints them:

#include <config4cpp/Configuration.h>
#include <iostream>
using namespace config4cpp;
using namespace std;


int main(int argc, char ** argv)
{
Configuration *  cfg = Configuration::create();
const char *     scope = "";
const char *     configFile = "someFile.cfg";
const char *     url;
const char *     file;
bool             true_false;


try {
cfg->parse(configFile);
url        = cfg->lookupString(scope, "url");
file       = cfg->lookupString(scope, "file");
true_false = cfg->lookupBoolean(scope, "true_false");
} catch(const ConfigurationException & ex) {
cerr << ex.c_str() << endl;
cfg->destroy();
return 1;
}
cout << "url=" << url << "; file=" << file
<< "; true_false=" << true_false
<< endl;
cfg->destroy();
return 0;
}

The Config4Cpp website provides comprehensive documentation, but reading just Chapters 2 and 3 of the "Getting Started Guide" should be more than sufficient for your needs.

I've searched config parsing libraries for my project recently and found these libraries:

libconfig is very easy, and what's better, it uses a pseudo json notation for better readability.

Easy to install on Ubuntu: sudo apt-get install libconfig++8-dev

and link: -lconfig++

Here is a simple work around for white space between the '=' sign and the data, in the config file. Assign to the istringstream from the location after the '=' sign and when reading from it, any leading white space is ignored.

Note: while using an istringstream in a loop, make sure you call clear() before assigning a new string to it.

//config.txt
//Input name = image1.png
//Num. of rows = 100
//Num. of cols = 150


std::string ipName;
int nR, nC;


std::ifstream fin("config.txt");
std::string line;
std::istringstream sin;


while (std::getline(fin, line)) {
sin.str(line.substr(line.find("=")+1));
if (line.find("Input name") != std::string::npos) {
std::cout<<"Input name "<<sin.str()<<std::endl;
sin >> ipName;
}
else if (line.find("Num. of rows") != std::string::npos) {
sin >> nR;
}
else if (line.find("Num. of cols") != std::string::npos) {
sin >> nC;
}
sin.clear();
}

How about formatting your configuration as JSON, and using a library like jsoncpp?

e.g.

{"url": "http://mysite dot com",
"file": "main.exe",
"true": 0}

You can then read it into named variables, or even store it all in a std::map, etc. The latter means you can add options without having to change and recompile your configuration parser.

I was looking for something that worked like the python module ConfigParser and found this: https://github.com/jtilly/inih

This is a header only C++ version of inih.

inih (INI Not Invented Here) is a simple .INI file parser written in C. It's only a couple of pages of code, and it was designed to be small and simple, so it's good for embedded systems. It's also more or less compatible with Python's ConfigParser style of .INI files, including RFC 822-style multi-line syntax and name: value entries.

SimpleConfigFile is a library that does exactly what you require and it is very simple to use.

# File file.cfg
url = http://example.com
file = main.exe
true = 0

The following program reads the previous configuration file:

#include<iostream>
#include<string>
#include<vector>
#include "config_file.h"


int main(void)
{
// Variables that we want to read from the config file
std::string url, file;
bool true_false;


// Names for the variables in the config file. They can be different from the actual variable names.
std::vector<std::string> ln = {"url","file","true"};


// Open the config file for reading
std::ifstream f_in("file.cfg");


CFG::ReadFile(f_in, ln, url, file, true_false);
f_in.close();


std::cout << "url: " << url << std::endl;
std::cout << "file: " << file << std::endl;
std::cout << "true: " << true_false << std::endl;


return 0;
}

The function CFG::ReadFile uses variadic templates. This way, you can pass the variables you want to read and the corresponding type is used for reading the data in the appropriate way.

So I merged some of the above solutions into my own, which for me made more sense, became more intuitive and a bit less error prone. I use a public stp::map to keep track of the possible config ids, and a struct to keep track of the possible values. Her it goes:

struct{
std::string PlaybackAssisted = "assisted";
std::string Playback = "playback";
std::string Recording = "record";
std::string Normal = "normal";
} mode_def;


std::map<std::string, std::string> settings = {
{"mode", mode_def.Normal},
{"output_log_path", "/home/root/output_data.log"},
{"input_log_path", "/home/root/input_data.log"},
};


void read_config(const std::string & settings_path){
std::ifstream settings_file(settings_path);
std::string line;


if (settings_file.fail()){
LOG_WARN("Config file does not exist. Default options set to:");
for (auto it = settings.begin(); it != settings.end(); it++){
LOG_INFO("%s=%s", it->first.c_str(), it->second.c_str());
}
}


while (std::getline(settings_file, line)){
std::istringstream iss(line);
std::string id, eq, val;


if (std::getline(iss, id, '=')){
if (std::getline(iss, val)){
if (settings.find(id) != settings.end()){
if (val.empty()){
LOG_INFO("Config \"%s\" is empty. Keeping default \"%s\"", id.c_str(), settings[id].c_str());
}
else{
settings[id] = val;
LOG_INFO("Config \"%s\" read as \"%s\"", id.c_str(), settings[id].c_str());
}
}
else{ //Not present in map
LOG_ERROR("Setting \"%s\" not defined, ignoring it", id.c_str());
continue;
}
}
else{
// Comment line, skiping it
continue;
}
}
else{
//Empty line, skipping it
continue;
}
}

}

I was searching for a similar simple C++ config file parser and this tutorial website provided me with a basic yet working solution. Its quick and dirty soultion to get the job done.

myConfig.txt


gamma=2.8
mode  =  1
path = D:\Photoshop\Projects\Workspace\Images\

The following program reads the previous configuration file:

#include <iostream>
#include <fstream>
#include <algorithm>
#include <string>


int main()
{
double gamma = 0;
int mode = 0;
std::string path;


// std::ifstream is RAII, i.e. no need to call close
std::ifstream cFile("myConfig.txt");
if (cFile.is_open())
{
std::string line;
while (getline(cFile, line))
{
line.erase(std::remove_if(line.begin(), line.end(), isspace),line.end());
if (line[0] == '#' || line.empty()) continue;


auto delimiterPos = line.find("=");
auto name = line.substr(0, delimiterPos);
auto value = line.substr(delimiterPos + 1);


//Custom coding
if (name == "gamma") gamma = std::stod(value);
else if (name == "mode") mode = std::stoi(value);
else if (name == "path") path = value;
}
}
else
{
std::cerr << "Couldn't open config file for reading.\n";
}


std::cout << "\nGamma=" << gamma;
std::cout << "\nMode=" << mode;
std::cout << "\nPath=" << path;
std::getchar();
}

I would like to recommend a single header C++ 11 YAML parser mini-yaml.

A quick-start example taken from the above repository.

file.txt

key: foo bar
list:
- hello world
- integer: 123
boolean: true

.cpp

Yaml::Node root;
Yaml::Parse(root, "file.txt");


// Print all scalars.
std::cout << root["key"].As<std::string>() << std::endl;
std::cout << root["list"][0].As<std::string>() << std::endl;
std::cout << root["list"][1]["integer"].As<int>() << std::endl;
std::cout << root["list"][1]["boolean"].As<bool>() << std::endl;


// Iterate second sequence item.
Node & item = root[1];
for(auto it = item.Begin(); it != item.End(); it++)
{
std::cout << (*it).first << ": " << (*it).second.As<string>() << std::endl;
}

Output

foo bar
hello world
123
1
integer: 123
boolean: true

Using the above answer by Shan, I did some simple modification for easy data reading from files like- multiple inputs in a single line, option for comment with '#' after inputs.

The following is the input file config.txt

# comments with #
# inputs can be separeted by comma
name=S.Das, age=28 #details
weight=65

Here is the code,

#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
using std::istringstream;


using std::string;


void readinput(std::unordered_map<string, string>& data) {
// std::ifstream is RAII, i.e. no need to call close
std::ifstream cFile("config.txt");
if (cFile.is_open()) {
std::string line;
while (getline(cFile, line)) {
line.erase(std::remove_if(line.begin(), line.end(), isspace), line.end());
if (line[0] == '#' || line.empty()) {
continue;
} else if (line.find('#')) {
line = line.substr(0, line.find('#'));
}
std::istringstream iss(line);
string strr;
while (getline(iss, strr, ',')) {
auto delimiterPos = strr.find("=");
auto name         = strr.substr(0, delimiterPos);
string value      = strr.substr(delimiterPos + 1);
// std::cout << name << " " << value << '\n';
data[name] = value;
}
}
} else {
std::cerr << "Couldn't open config file for reading.\n";
}
}


int main() {
std::unordered_map<string, string> data;
readinput(data);
std::cout << data.at("age") << std::endl;
return 0;
}

No one mentioned <regex>. I prefer them since they are really easy to read and less error prone. MWE:

#include <fstream>
#include <iostream>
#include <regex>
#include <string>


struct config_t
{
// (define variables here)


void read_from(const std::string& fname)
{
std::ifstream cfg_file(fname);
if(!cfg_file.good())
throw std::runtime_error("Cannot open file: " + fname);


std::string line;
while(std::getline(cfg_file, line))
{
std::regex re(R"XXX(^(\s*(\S+)\s*=\s*(\S+))?\s*(#.*)?$)XXX",
std::regex::optimize);
std::smatch match;
if(std::regex_search(line, match, re))
{
if(match.length(2))
{
std::string key = match.str(2),
value = match.str(3);
std::cout << "key-value-pair: " << key << " -> " << value << std::endl;
// (fill variables here)
}
}
else
throw std::runtime_error("Invalid line: " + line);
}
}
};


int main(int argc, char** argv)
{
int rval = EXIT_SUCCESS;
try
{
config_t cfg;
if (argc != 2)
throw std::runtime_error("Expecting exactly one argument");
cfg.read_from(argv[1]);
}
catch (const std::exception& e)
{
std::cerr << "Error: " << e.what() << std::endl;
rval = EXIT_FAILURE;
}
return rval;
}