Чтение конфигурационных файлов
В этой статье создадим свой инструмент чтения конфигурационных файлов на 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() && 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& str, std::vector<string>& 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& key, std::vector<int>& 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& key, string& 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)
Для удобства можно отредактировать наш заголовочный файл и выбросить оттуда инструкции вывода промежуточных результатов обработки.
Заключение
В итоге мы получили удобный инструмент чтения конфигурационных файлов. Знаю, что замечаний по коду и формату может быть много, но для первого приближения вполне подойдет и такой вариант.
Добавить комментарий