Чтение конфигурационных файлов

В этой статье создадим свой инструмент чтения конфигурационных файлов на C++.

Описание

Допустим, что у нашего приложения есть обширная серия параметров, значения которых требуется регулировать. Тогда всю нужную информацию целесообразно сохранить в конфигурационном файле. Например таком:

# Window properties
[Window]
caption = Hello_v1.0
pos = 0,0   # X and Y
size = 100, 100   # Width and Height

# Button properties
[button]
caption=OK
x=10
y=10
type=Accept
width=52
height=25

В целом, формат данных практически тот же, что и в ini файлах в Windows. В квадратных скобках указывается имя группы параметров, после чего идет их набор в виде <имя параметра>=<значение>. Каждый параметр расположен в отдельной строке. Если требуется дать комментарий, то перед ним ставится символ #, как и во многих конфигурационных файлах в linux.

Основная цель — получить правильно интерпретированную информацию по указанному имени ключа. Под ключом будем иметь в виду строку, по содержанию которой мы сможем получить нужные данные из конфигурационного файла.

Теперь перейдем непосредственно к коду.

Код

Общий смысл

Для удобства сосредоточим весь код в одном заголовочном файле ConfReader.h. Тем более, что исходник небольшой.

Итак, заголовочные файлы, которые нам потребуются это:

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

Рассматривать исходник совсем уж детально не будем, так как для понимания написанного важен небольшой ряд ключевых моментов. Вот о них и поговорим.

Во первых, все данные и методы будут сосредоточены внутри структуры с именем ConfReader.

Во вторых, нам нужно свести все данные, что хранятся в конфигурационном файле к набору пар <ключ>-<значение>. Это просто реализовать, используя следующий объект:

	std::map<string, string> params;

Процедура чтения файла

Далее следует рассмотреть процедуру чтения конфигурационного файла:

	void read_config(const char* fname) {
		
		string str;

		ifstream In(fname);
		vector<string> v;
	
		string prefix = "";
		while ( ! In.eof() ) {
			getline (In, str);
			str.erase(std::remove_if(str.begin(), str.end(), isSpace), str.end());
			int cage;
			if((cage = str.find('#')) != string::npos)
				str.erase(cage);
			if(str[0] == '[')
				prefix = str.substr(1, str.find(']')-1);
			else if(!str.empty() &amp;&amp; str[0] != '#')
				v.push_back(prefix + "_" + str);
		};

Отмечу, что код требует доработки, поскольку не рассчитан на разного рода исключения (ошибки при чтении файла, неверный формат данных и т.д.).

Итак, обо всем по порядку. В строке 29 открываем файл на чтение. В строке 32 объявляем переменную-префикс. Она будет хранить имя раздела, в котором расположена текущая пара ключ-значение. Позиции 34-35 содержат команды чтения строки из файла и очищения этой строки от пробелов соответственно. Далее 36-38 — это удаление комментария из строки, если он присутствовал. Напомню, что комментарий начинается со знака #.

В строках 39-42 содержится алгоритм, помещающий имя раздела в переменную-префикс, если имя записано в квадратных скобках. В противном случае, если строка не пуста и не является целиком комментарием, то приписываем к ней слева имя текущего раздела через «_».

Вышеописанная процедура существует для формирования всех ключей в виде <имя раздела>_<имя параметра>. Если какой-либо параметр находиться вне разделов, то его ключ будет иметь вид _<имя параметра>.

Таким образом, переменная v хранит все строки входного файла в виде <имя раздела>_<параметр>=<значение>. Причем ни одна из них не содержит комментариев и пробельных символов.

Осталось разбить такой набор строк на пары ключ-значение. Для этого служит следующий код:

for(int i=0; i<v.size(); i++)
	params.insert(std::make_pair(v[i].substr(0, v[i].find('=')), v[i].substr(v[i].find('=')+1) ));

Как не трудно видеть, всё, что слева от знака равенства — это ключ, а справа — значение.

Вспомогательная процедура

Так как пробелы в строках отсутствуют, то разделять серию однотипных данных будем запятой. Следовательно, нам понадобится процедура преобразования такой серии в массив:

	void splitStr(string&amp; str, std::vector<string>&amp; parts) {
		int d=0, old_d = 0;
		while((d = str.find(',', old_d)) != string::npos) {
			parts.push_back(str.substr(old_d, d-old_d));
			old_d = d+1;
		};
		parts.push_back(str.substr(old_d));
	};

Здесь формируем массив из подстрок, руководствуясь расположением запятых в основной строке. Результат помещаем в переменную parts.

Получение необходимых параметров

Осталось только интерпретировать массив нужным образом.

Например, для получения массива из целых чисел, записанных через запятую будем использовать метод:

	void getInts(string&amp; key, std::vector<int>&amp; res) {
		string s = params[key];
		std::vector<string> args;
		splitStr(s, args);
		for(int i=0; i<args.size(); i++)
			res.push_back(std::stoi(args[i]));
	};

Отметим, что метод работает корректно и в случае одного числа. Тогда массив будет состоять из одного элемента.

Далее идет метод для получения строки:

	void getString(string&amp; key, string&amp; res) {
		res = params[key];
	};

Для работы с массивом элементов типа double определена аналогичная процедура. Различие заключается только в типе возвращаемого результата и в функции преобразования строкового значения в числовое.

Исходник проекта находится здесь.

Тест

Для теста возьмем следующий конфигурационный файл:

x = 100
y = 200

# --- Some text!!! ---
[load]
file = ./images/test.png  # Image file name
pos = 23, 32      # X and Y
width = 640
height = 480
d = 34.25, 0.5, 3.14, 254.789

Его имя укажем в качестве параметра командной строки для следующей программы.

#include <iostream>
#include <vector>
#include "ConfReader.h"

using namespace std;

int main(int argc, char* argv[])
{

	if(argc < 2) {
		printf("Config_reader <cfg_file>\n");
		return 0;
	};

	ConfReader cr;
	cr.read_config(argv[1]);

	std::vector<int> ints;
	string key = "_x";
	cr.getInts(key, ints);
	cout << "Global x=" << ints[0] << endl;
	ints.clear();

	key = "_y";
	cr.getInts(key, ints);
	cout << "Global y=" << ints[0] << endl;
	ints.clear();

	key = "load_file";
	string fname;
	cr.getString(key, fname);
	cout << "Image file to load=" << fname << endl;
	
	key = "load_pos";
	cr.getInts(key, ints);
	cout << "Image x=" << ints[0] << " y=" << ints[1] << endl;
	ints.clear();

	key = "load_width";
	cr.getInts(key, ints);
	cout << "Image width=" << ints[0] << endl;
	ints.clear();

	key = "load_height";
	cr.getInts(key, ints);
	cout << "Image height=" << ints[0] << endl;
	ints.clear();

	key = "load_d";
	std::vector<double> d;
	cr.getDoubles(key, d);
	cout << "And some doubles from \'load\' section ->";

	for(int i=0; i<d.size(); i++)
		cout << "(" << d[i] << ")";
	cout << endl;

	return 0;
};

В результате на выходе будем иметь следующий текст:

1 string is "_x=100"
2 string is "_y=200"
3 string is "load_file=./images/test.png"
4 string is "load_pos=23,32"
5 string is "load_width=640"
6 string is "load_height=480"
7 string is "load_d=34.25,0.5,3.14,254.789"
'_x' -> '100'
'_y' -> '200'
'load_d' -> '34.25,0.5,3.14,254.789'
'load_file' -> './images/test.png'
'load_height' -> '480'
'load_pos' -> '23,32'
'load_width' -> '640'
Global x=100
Global y=200
Image file to load=./images/test.png
Image x=23 y=32
Image width=640
Image height=480
And some doubles from 'load' section ->(34.25)(0.5)(3.14)(254.789)

Для удобства можно отредактировать наш заголовочный файл и выбросить оттуда инструкции вывода промежуточных результатов обработки.

Заключение

В итоге мы получили удобный инструмент чтения конфигурационных файлов. Знаю, что замечаний по коду и формату может быть много, но для первого приближения вполне подойдет и такой вариант.

Добавить комментарий

Ваш e-mail не будет опубликован.