INI 文件是一种常见的配置文件,相比 json 等文件格式简单得多,对 INI 文件的解析适合练手。

实现代码如下,其中使用了 std::ranges

ini_parser.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#pragma once

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

class IniParser {
public:
void set(const std::string &section, const std::string &key,
const std::string &value) {
m_data[section][key] = value;
}

void set(const std::string &key, const std::string &value) {
m_data[""][key] = value; // default section
}

auto get(const std::string &section,
const std::string &key) const -> std::optional<std::string> {
auto iter = m_data.find(section);
if (iter != m_data.end()) {
auto itKey = iter->second.find(key);
if (itKey != iter->second.end()) { return itKey->second; }
}
return std::nullopt;
}

auto get(const std::string &key) const {
return get("", key); // default section
}

void read(const std::string &file_name) {
std::fstream f(file_name, std::ios::in);
if (f.fail()) {
throw std::runtime_error{"file open error: " + file_name};
}

std::string line;
std::string cur_section;
while (std::getline(f, line)) {
clean_str(line);

// support multiline
while (!line.empty() && line.back() == '\\') {
line.pop_back();
std::string next_line;
if (std::getline(f, next_line)) {
clean_str(next_line);
line += next_line;
}
}

// ':' -> '='
std::size_t n_colon_start = line.find_first_of(':');
if (n_colon_start != std::string::npos) {
line[n_colon_start] = '=';
}

// erase comments
std::size_t n_comment_start = line.find_first_of('#');
if (n_comment_start != std::string::npos) {
line.erase(n_comment_start);
}

trim_str(line, " ");

if (line.empty()) continue;

// switch section
if (line.front() == '[' && line.back() == ']') {
cur_section = line.substr(1, line.size() - 2);
trim_str(cur_section, " ");
continue;
}

// add pair
if (!add_pair(cur_section, line)) {
f.close();
throw std::runtime_error{"line parse error: " + line};
}
}
}

void write(const std::string &file_name,
const std::ios_base::openmode mode) const {
std::fstream f(file_name, std::ios::out | mode); // NOLINT
if (f.fail()) {
throw std::runtime_error{"file open error: " + file_name};
}

for (const auto &section : m_data) {
if (!section.first.empty()) {
f << "\n[" << section.first << "]\n";
}
for (const auto &pair : section.second) {
f << pair.first << " = " << pair.second << '\n';
}
}

f.close();
}

auto export_all() const { return m_data; }

private:
std::map<std::string, std::map<std::string, std::string>> m_data;

static void split_str(const std::string &s,
std::vector<std::string> &tokens,
const std::string &delimiters) {
auto last_pos = s.find_first_not_of(delimiters, 0);
auto pos = s.find_first_of(delimiters, last_pos);
while (std::string::npos != pos || std::string::npos != last_pos) {
tokens.emplace_back(s.substr(last_pos, pos - last_pos));
last_pos = s.find_first_not_of(delimiters, pos);
pos = s.find_first_of(delimiters, last_pos);
}
}

static void clean_str(std::string &str) {
std::ranges::replace(str, '\n', ' ');
std::ranges::replace(str, '\r', ' ');
std::ranges::replace(str, '\t', ' ');
}

static void trim_str(std::string &s, const std::string &delimiters) {
if (s.empty()) return;

s.erase(0, s.find_first_not_of(delimiters));
s.erase(s.find_last_not_of(delimiters) + 1);
}

bool add_pair(const std::string &cur_section, const std::string &line) {
std::vector<std::string> str_buff;

split_str(line, str_buff, "=");

if (str_buff.size() == 2) {
std::string key = str_buff[0];
trim_str(key, " ");

std::string value = str_buff[1];
trim_str(value, " ");

m_data[cur_section][key] = value;
return true;
}

if (str_buff.size() == 1) {
std::string key = str_buff[0];
trim_str(key, " ");

m_data[cur_section][key] = ""; // value = ""
return true;
}

return false;
}
};

测试代码如下

test.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "ini_parser.hpp"

#include <iostream>
#include <string>

int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <ini_file_path>\n";
return 1;
}

std::string ini_path = argv[1];
auto ini = IniParser{};

try {
ini.read(ini_path);
std::cout << "n: " << ini.get("n").value_or("[x]") << '\n';
std::cout << "pi: " << ini.get("pi").value_or("[x]") << '\n';
std::cout << "msg: " << ini.get("msg").value_or("[x]") << '\n';
std::cout << "symbol: " << ini.get("symbol").value_or("[x]") << '\n';
std::cout << "settings.enabled: "
<< ini.get("settings", "enabled").value_or("[x]") << '\n';
std::cout << "settings.count: "
<< ini.get("settings", "count").value_or("[x]") << '\n';

std::cout << "\nexport all\n";

auto data = ini.export_all();
for (const auto &section : data) {
if (!section.first.empty()) {
std::cout << "[" << section.first << "]" << '\n';
}
for (const auto &pair : section.second) {
std::cout << pair.first << " = " << pair.second << '\n';
}
}

ini.set("new_section", "new_key", "new_value");
ini.write("tmp_config.ini", std::ios::trunc);
}
catch (const std::exception &e) {
std::cerr << e.what() << '\n';
}
catch (...) {
std::cout << "unknown exception\n";
}

return 0;
}

测试使用的 ini 文件内容如下,需要把 ini 文件路径通过命令行参数传递。

config.ini
1
2
3
4
5
6
7
8
9
10
11
12
n = 42
pi = 3.14159
msg = "abcd"
symbol = # nothing
str = hello world
multi-line = hello \
world


[settings]
enabled = true
count = 200

运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
n: 42
pi: 3.14159
msg: "abcd"
symbol:
settings.enabled: true
settings.count: 200

export all
msg = "abcd"
multi-line = hello world
n = 42
pi = 3.14159
str = hello world
symbol =
[settings]
count = 200
enabled = true