Creating a simple configuration file and parser in C++

Viewed 193033

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!

15 Answers

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.

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

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;
}

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.

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;
}
Related