WSL/SLF GitLab Repository

INIParser.cc 32.1 KB
Newer Older
1
2
3
4
5
/*****************************************************************************/
/*  Copyright 2019 WSL Institute for Snow and Avalanche Research  SLF-DAVOS  */
/*****************************************************************************/
/* This file is part of INIshell.
   INIshell is free software: you can redistribute it and/or modify
6
   it under the terms of the GNU General Public License as published by
7
8
9
10
11
12
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   INIshell is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
   GNU General Public License for more details.
14

15
16
   You should have received a copy of the GNU General Public License
   along with INIshell.  If not, see <http://www.gnu.org/licenses/>.
17
18
*/

19
#include "INIParser.h"
20
#include "src/main/colors.h"
21
#include "src/main/Error.h"
22
#include "src/main/inishell.h"
23
#include "src/main/settings.h"
24

25
#include <QDir>
26
27
#include <QFile>

28
#include <array>
29
#include <iostream> //for logging to cerr
30
31
32
33
34
35
36
37
38
#include <utility> //for std::move semantics

#ifdef DEBUG
	#include <QDebug>
#endif

////////////////////////////////////////
///          KEYVALUE class          ///
////////////////////////////////////////
39

40
41
42
/**
 * @class KeyValue
 * @brief Default constructor for a key/value pair.
43
44
 * @details A KeyValue holds one entry of an INI file, i. e. the key, value and comment,
 * as well as surrounding whitespaces. The key is case insensitive.
45
46
47
48
49
 */
KeyValue::KeyValue() : KeyValue(QString(), QString())
{
	//delegate the constructor
}
50

51
52
53
54
55
56
/**
 * @brief Constructor for a key/value pair with a given key and value.
 * @param[in] key The key to set.
 * @param[in] value The value to set.
 */
KeyValue::KeyValue(QString key, QString value) : key_(std::move(key)), value_(std::move(value))
57
{
58
	static constexpr int nr_of_whitespace_fields_keyval = 4; //(1)key(2)=(3)value(4)#comment
59
	whitespaces_.reserve(nr_of_whitespace_fields_keyval);
60
	whitespaces_.emplace_back(""); //default: no whitespace at beginning of line
61
	for (int ii = 1; ii < nr_of_whitespace_fields_keyval; ++ii)
62
		whitespaces_.emplace_back(" "); //default inbetween whitespace
63
64
}

65
66
/**
 * @brief Assign attributes to a KeyValue.
67
 * @details The key name is set beforehand, here we set the value and secondary properties, i. e.
68
69
70
71
 * the inline comment and surrounding whitespaces. Called when parsing an INI file.
 * @param[in] rexmatch The already parsed regular expression holding all properties, i. e. the full
 * INI line.
 */
72
73
void KeyValue::setKeyValProperties(const QRegularExpressionMatch &rexmatch)
{
74
75
76
	static constexpr size_t idx_value = 5; //indices of the respective capturing groups
	static constexpr size_t idx_comment = 7;
	static constexpr int nr_of_whitespace_fields_keyval = 4; //(1)key(2)=(3)value(4)#comment
77
	static constexpr std::array<size_t, nr_of_whitespace_fields_keyval> indices_whitespaces( {1, 3, 4, 6} );
78

79
80
	this->setValue(rexmatch.captured(idx_value));
	this->setInlineComment(rexmatch.captured(idx_comment));
81
	if (getSetting("user::inireader::whitespaces", "value") == "USER") {
Michael Reisecker's avatar
Michael Reisecker committed
82
83
84
		for (size_t ii = 0; ii < nr_of_whitespace_fields_keyval; ++ii)
			whitespaces_.at(ii) = rexmatch.captured(static_cast<int>(indices_whitespaces.at(ii)));
	}
85
}
86

87
88
89
90
/**
 * @brief Print the key/value pair to a text stream.
 * @param[in,out] out_ss The stream to print to.
 */
91
void KeyValue::print(QTextStream &out_ss)
92
{
93
94
95
96
97
98
	out_ss << getBlockComment();
	out_ss << whitespaces_.at(0) << getKey() << whitespaces_.at(1) << "=" <<
	    whitespaces_.at(2) << getValue();
	if (!getInlineComment().isEmpty())
		out_ss << whitespaces_.at(3) << getInlineComment();
	out_ss << "\n";
99
100
}

101
102
103
104
105
106
107
108
109
110
111
112
////////////////////////////////////////
///           SECTION class          ///
////////////////////////////////////////

/**
 * @class Section
 * @brief Default constructor for a Section.
 * @details A Section holds one section of an INI file. This includes the section's properties, i. e.
 * the name, inline comment and surrounding whitespaces, as well as a list of all KeyValues
 * contained in the section. The section name is case insensitive.
 * KeyValues are stored in a map with their INI key names as container key, so the map works with
 * a copy of KeyValues.getName() as the map key for comfortable access and checking of existence.
113
 * Furthermore, when new KeyValues are inserted their map key is stored in order of insertion
114
115
 * in a vector so that the map can be iterated through unsorted, i. e. how it was read from the INI file.
 */
116
117
Section::Section()
{
118
	static constexpr int nr_of_whitespace_fields_section = 2; //(1)[SECTION](2)#comment
119
	whitespaces_.reserve(nr_of_whitespace_fields_section);
120
121
122
123
	whitespaces_.emplace_back(""); //default whitespaces at beginning of line
	whitespaces_.emplace_back(" "); //default whitespaces before comment
}

124
125
126
/**
 * @brief Convenience call to retrieve a KeyValue from a section.
 * @details This function accesses KeyValues by their key names.
127
 * @param[in] str_key The key name to find.
128
129
 * @return A reference to the found KeyValue, or nullptr if non-existent.
 */
130
131
132
133
134
KeyValue * Section::operator[] (const QString &str_key)
{
	return getKeyValue(str_key);
}

135
136
137
138
139
140
141
/**
 * @brief Retrieve a KeyValue by order of insertion.
 * @details This function allows access of the KeyValues in order of insertion,
 * for example to reproduce the ordering of an input INI file.
 * @param[in] index The KeyValue's number of insertion.
 * @return A reference to the found KeyValue, or nullptr if non-existent.
 */
142
143
KeyValue * Section::operator[] (const size_t &index)
{
144
	const QString key( ordered_key_values_.at(index) );
145
	return &key_values_[key];
146
147
}

148
149
150
151
152
/**
 * @brief Lesser-operator to have Sections function as map keys.
 * @param[in] other The Section to compare to.
 * @return True if this section's name is lexicographically smaller than the other's.
 */
153
bool Section::operator<(const Section &other) const
154
155
{
	return QString::compare(this->getName(), other.getName(), Qt::CaseInsensitive) < 0;
156
157
}

158
159
160
161
162
163
164
165
166
167
168
169
/**
 * @brief Assign attributes to a Section.
 * @details Set the Section's properties, i. e. name, comment, and whitespaces.
 * Called when parsing an INI file.
 * @param[in] rexmatch The already parsed regular expression holding all properties, i. e. the full
 * INI line.
 */
void Section::setSectionProperties(const QRegularExpressionMatch &rexmatch)
{
	static constexpr size_t idx_name = 2; //indices of respective capturing groups
	static constexpr size_t idx_comment = 4;
	static constexpr int nr_of_whitespace_fields_section = 2;
170
	static constexpr std::array<size_t, nr_of_whitespace_fields_section> indices_whitespaces( {1, 3} );
171
172
173

	this->setName(rexmatch.captured(idx_name));
	this->setInlineComment(rexmatch.captured(idx_comment));
174
175
	//if getMainWindow is NULL then we are in command line mode - no user settings available
	if (getMainWindow() == nullptr || getSetting("user::inireader::whitespaces", "value") == "USER") {
Michael Reisecker's avatar
Michael Reisecker committed
176
177
178
		for (size_t ii = 0; ii < nr_of_whitespace_fields_section; ++ii)
			whitespaces_.at(ii) = rexmatch.captured(static_cast<int>(indices_whitespaces.at(ii)));
	}
179
180
181
182
183
184
185
}

/**
 * @brief Check if the Section contains a certain KeyValue.
 * @param[in] str_key The INI key to check for.
 * @return True if the INI key was found in this Section.
 */
Michael Reisecker's avatar
Michael Reisecker committed
186
bool Section::hasKeyValue(const QString &str_key) const
187
{
188
	return key_values_.count(str_key) > 0;
189
}
190

191
192
193
194
195
/**
 * @brief Retrieve a KeyValue from a Section.
 * @param[in] str_key The INI key to look for.
 * @return A reference to the found KeyValue, or nullptr if non-existent.
 */
196
KeyValue * Section::getKeyValue(const QString &str_key)
197
{
198
	if (key_values_.find(str_key) == key_values_.end())
199
		return nullptr;
200
	return &key_values_[str_key];
201
202
}

203
204
205
206
207
208
209
210
/**
 * @brief Add a key/value pair to the Section.
 * @details This function checks if an INI key already exists in the Section, and if so, returns it.
 * If not, the (new) KeyValue is added to the Section for further use. Properties are set from outside
 * after this check; attributes such as whitespaces are not propagated here.
 * @param[in] keyval An already constructed KeyValue.
 * @return A reference to either the found or the newly inserted KeyValue.
 */
211
212
213
214
KeyValue * Section::addKeyValue(const KeyValue &keyval)
{
	std::pair<std::map<QString, KeyValue>::iterator, bool> result;
	result = key_values_.insert(std::make_pair(keyval.getKey(), keyval));
215
	if (result.second) //a new item was inserted
216
		ordered_key_values_.push_back(keyval.getKey()); //store key in order of insertion
217
218
219
	return &result.first->second;
}

220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
/**
 * @brief Remove a key from this INI section.
 * @param[in] key The key to remove.
 * @return False if the key was not found.
 */
bool Section::removeKey(const QString &key)
{
	auto it(key_values_.find(key));
	if (it == key_values_.end()) {
		return false;
	} else {
		key_values_.erase(it);
		ordered_key_values_.erase(
		    std::find(ordered_key_values_.begin(), ordered_key_values_.end(), key));
		return true;
	}
}

238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/**
 * @brief Print the section header to a text stream.
 * @param[in,out] out_ss The stream to print to.
 */
void Section::print(QTextStream &out_ss)
{
	if (default_name_set_) //user did not provide the (default) section --> do not output it
		return;
	if (!this->getBlockComment().isEmpty())
		out_ss << this->getBlockComment();
	out_ss << whitespaces_.at(0) << Cst::section_open << this->getName() << Cst::section_close <<
	    (this->getInlineComment().isEmpty()? "" : whitespaces_.at(1) + this->getInlineComment()) << "\n";
}

/**
 * @brief Print all of this Section's KeyValues to a text stream.
 * @details This can either be done in container sorting order (alphabetical), or in order of
 * insertion (tracked in a separate map).
 * @param[in,out] out_ss The stream to print to.
 * @param[in] alphabetical If true, INI keys are writtin in alphabetical order.
 */
259
void Section::printKeyValues(QTextStream &out_ss, const bool &alphabetical)
260
261
262
{
	if (alphabetical) { //range based loop with implicit container sorting
		for (auto &keyval : key_values_) {
263
			if (!keyval.second.getValue().isEmpty())
264
265
				keyval.second.print(out_ss);
		}
266
	} else { //for loop through a vector that stores map keys in order of insertion
267
		for (size_t ii = 0; ii < ordered_key_values_.size(); ++ii) {
268
269
			if (!key_values_[ordered_key_values_[ii]].getValue().isEmpty())
				key_values_[ordered_key_values_[ii]].print(out_ss);
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
		}
	} //end if alphabetical
}

////////////////////////////////////////
///         SECTIONLIST class        ///
////////////////////////////////////////

/**
 * @class SectionList
 * @brief A SectionList collects sections and therefore composes a complete INI file, save for
 * a trailing block comment.
 * @details The SectionList inherits from std::list to enable range loops on its main container,
 * a std::list of Sections. In a secondary container, an std::set, the section names are collected
 * in order of insertion to be able to reproduce user INI files and for easy lookup.
 */

/**
 * @brief Check if a section name already exists in the collection of Sections.
 * @param[in] section_name The INI file's name for the section.
 * @return True if the section exists.
 */
292
293
bool SectionList::hasSection(const QString &section_name) const
{
294
	return (ordered_section_set_.find(section_name) != ordered_section_set_.end());
295
}
296

297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/**
 * @brief Retrieve a Section by its name.
 * @param[in] str_section The INI file's name for the section.
 * @return A reference to the found Section, or nullptr if it does not exist.
 */
Section * SectionList::getSection(const QString &str_section)
{ //Look for the section by name, not by equality (different comments are still the same section)
	for (auto &sec : section_list_) { //only called once per section in parse()
		if (QString::compare(sec.getName(), str_section, Qt::CaseInsensitive) == 0)
			return &sec;
	}
	return nullptr;
}

/**
 * @brief Add a Section to the list of sections.
 * @details This function checks if a Section already exists and if so returns it.
 * If not, the (new) Section is added for further use. Properties are set from outside
 * after this check; attributes such as whitespaces are not propagated here.
 * @param[in] section An already constructed Section.
 * @return A reference to either the found or the newly inserted Section.
 */
319
320
Section * SectionList::addSection(const Section &section)
{
321
	Section *out_section( getSection(section.getName()) );
322
323
	if (out_section == nullptr) {
		section_list_.push_back(section);
324
		out_section = &section_list_.back();
325
		ordered_section_set_.insert(section.getName());
326
327
328
329
	}
	return out_section;
}

330
331
332
333
334
/**
 * @brief Remove a Section from the collection.
 * @param[in] str_section The INI file's name for the section.
 * @return True if successful, false if the Section did not exist.
 */
335
336
bool SectionList::removeSection(const QString &str_section)
{
337
	//find Section in the list:
338
	const auto lit = std::find_if(section_list_.begin(), section_list_.end(),
339
	    [str_section](Section const& section) {return (QString::compare(section.getName(), str_section, Qt::CaseInsensitive) == 0);});
340
	if (lit != section_list_.end()) {
341
		//find section in the ordered list:
342
		for (auto &sec : ordered_section_set_) {
343
			if (QString::compare(sec, str_section, Qt::CaseInsensitive) == 0) {
344
345
346
347
348
				ordered_section_set_.erase(sec);
				break;
			}
		}
		section_list_.erase(lit);
349
		return true;
350
351
352
353
	}
	return false; //section did not exist
}

354
355
356
/**
 * @brief Clear both internal containers for the list of sections.
 */
357
358
359
360
361
362
void SectionList::clear() noexcept
{
	section_list_.clear();
	ordered_section_set_.clear();
}

363
364
365
366
367
368
369
370
371
372
373
////////////////////////////////////////
///            INIPARSER             ///
////////////////////////////////////////

/**
 * @class INIParser
 * @brief The top level interface to read and store INI sections and key/value pairs.
 * @details The INIParser handles everything to do with reading key/value pairs and sections
 * from an INI file on the file system, manipulating them, and writing the result back out.
 */

374
/**
375
 * @brief The equality operator checks sections and keys.
376
377
378
379
380
381
382
 * @details Each section name and key/value-pair is compared, comments and whitespaces do not
 * matter.
 * @param[in] other The INIParser to compare against.
 * @return True if both INIparsers are the same.
 */
bool INIParser::operator==(const INIParser &other)
{
383
	equality_check_msg_.clear(); //store why the assertion is false
384
	auto other_sections( other.getSectionsCopy() );
385
	if (other_sections.size() != sections_.size()) {
386
387
388
389
390
		if (filename_.isEmpty()) {
			equality_check_msg_ = "An application has been opened, but it's values have not been saved yet.\n";
			return false;
		}
		equality_check_msg_ = tr("Different number of sections (%1 vs. %2).\nThis usually implies a different number of keys.\n\n").arg(
391
		    sections_.size()).arg(other_sections.size());
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
		QString new_in_this, new_in_other;
		for (auto &sec : sections_) {
			if (!other_sections.hasSection(sec.getName()))
				new_in_other += sec.getName() + ", ";
		}
		for (auto &sec : other_sections) {
			if (!sections_.hasSection(sec.getName()))
				new_in_this += sec.getName() + ", ";
		}
		new_in_this.chop(2);
		new_in_other.chop(2);
		if (!new_in_this.isEmpty())
			equality_check_msg_ += "Sections not in " + filename_ + ": " + new_in_this + "\n" +
			    "(The loaded application may have inserted missing mandatory keys.)\n";
		if (!new_in_other.isEmpty())
			equality_check_msg_ += "Sections present in original but not in the new file: " + new_in_other + "\n";
408
		return false;
409
	}
410

411
	for (auto &sec : sections_) { //TODO: more info in these messages
412
		const auto other_sec( other_sections[sec.getName()] );
413
414
		if (other_sec == nullptr) {
			equality_check_msg_ = tr(R"(Section "%1" not found)").arg(sec.getName());
415
			return false;
416
		}
417
418
		const auto key_values( sec.getKeyValueList() );
		auto other_key_values( other_sec->getKeyValueList() );
419
420
421
		if (key_values.size() != other_key_values.size()) {
			equality_check_msg_ = tr(R"(Different number of key/value pairs (%1 vs. %2))").arg(
			    key_values.size()).arg(other_key_values.size());
422
			return false;
423
		}
424
		for (auto &keyval : key_values) {
425
426
			if (other_key_values.count(keyval.first) == 0) {
				equality_check_msg_ = tr(R"(Key "%1" not found)").arg(keyval.first);
427
				return false;
428
			}
429
			const auto other_keyval( other_key_values[keyval.first] );
430
			const bool both_empty = (keyval.second.getValue().isEmpty() && other_keyval.getValue().isEmpty());
431
			if (both_empty) //e. g. one is not present (Null) and the other one reported empty
432
				continue;
433
434
			if (QString::compare(keyval.second.getValue(), other_keyval.getValue(), Qt::CaseInsensitive) != 0) {
				equality_check_msg_ = tr(R"((One of) the different key(s) is: "%1")").arg(keyval.first);
435
				return false;
436
			}
437
438
			//note that there is no numeric check against different precisions here (i. e. 1.0 != 1),
			//this must be handled by the Number panel
439
440
441
442
443
		}
	}
	return true;
}

444
/**
445
446
447
 * @brief Parse an INI file and store everything in container classes.
 * @details This opens and parses an INI file from the file system.
 * @param[in] File name to parse.
448
449
450
 * @param[in] fresh Delete existing sections and start afresh.
 * @return True if the parsing was successful.
 */
451
bool INIParser::parseFile(const QString &filename, const bool &fresh)
452
{
453
454
	if (fresh)
		this->clear();
455
	filename_ = filename; //remember last file parsed
456
	first_error_message_ = true;
457
	/* open the file */
458
459
	QFile infile(filename_);
	if (!infile.open(QIODevice::ReadOnly | QIODevice::Text)) {
460
		display_error(tr("Could not open INI file for reading"), QString(),
461
		    QDir::toNativeSeparators(filename_) + ":\n" + infile.errorString());
462
463
464
		return false;
	}
	QTextStream tstream(&infile);
465
	const bool success = parseStream(tstream);
466
	infile.close();
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
	return success;
}

/**
 * @brief Parse INI contents from a string.
 * @details This is used for example in the preview window.
 * @param[in] text Text to parse.
 * @param[in] fresh Delete existing sections and start afresh.
 * @return True if successful.
 */
bool INIParser::parseText(QString text, const bool &fresh)
{
	if (fresh)
		this->clear();
	filename_ = "./preview_ini.ini";
	QTextStream tstream(&text);
	return parseStream(tstream);
484
485
}

486
487
488
489
490
491
/**
 * @brief Retrieve a single INI key's value.
 * @param[in] str_section Section to search for the key/value.
 * @param[in] str_key INI key to find.
 * @return The INI value, or a Null-String if not found.
 */
492
493
QString INIParser::get(const QString &str_section, const QString &str_key)
{
494
	Section *sec( sections_[str_section] );
495
496
	if (sec == nullptr)
		return QString();
497
	const KeyValue *keyval( sec->getKeyValue(str_key) );
498
499
500
501
502
	if (keyval == nullptr)
		return QString();
	return keyval->getValue();
}

503
/**
504
 * @brief Set a key's value, creating the KeyValue if necessary.
505
506
507
508
509
 * @param[in] str_section Section name the key/value belongs to.
 * @param[in] str_key The key to insert or modify.
 * @param[in] str_value The key's value to set, or empty if only comments are changed.
 * @return True if the value was set, false if nothing was changed.
 */
Mathias Bavay's avatar
Mathias Bavay committed
510
bool INIParser::set(QString str_section_in, const QString &str_key, const QString &str_value,
511
    const bool is_mandatory)
512
{
513
	/* find or create the Section and KeyValue */
Mathias Bavay's avatar
Mathias Bavay committed
514
515
	if (str_section_in.isNull())
		str_section_in = Cst::default_section;
516
	bool changed = false;
Mathias Bavay's avatar
Mathias Bavay committed
517
	Section *sec( sections_[str_section_in] );
518
519
	if (sec == nullptr) {
		Section new_section;
Mathias Bavay's avatar
Mathias Bavay committed
520
		new_section.setName(str_section_in);
521
		sec = sections_.addSection(new_section);
522
		changed = true;
523
	}
524
	KeyValue *keyval( sec->getKeyValue(str_key) );
525
	if (keyval == nullptr) {
526
		KeyValue new_keyval(str_key);
527
528
529
		keyval = sec->addKeyValue(new_keyval);
		changed = true;
	}
530
	keyval->setValue(str_value);
531
	keyval->setMandatory(is_mandatory); //injected when parsing XML
532
	return changed;
533
534
}

535
536
537
538
539
/**
 * @brief Check if a certain INI key is present.
 * @param[in] str_key The key to look for.
 * @return True if found.
 */
Michael Reisecker's avatar
Michael Reisecker committed
540
bool INIParser::hasKeyValue(const QString &str_key) const
541
542
{
	for (auto &sec : sections_.getSectionsList()) {
Michael Reisecker's avatar
Michael Reisecker committed
543
		if (sec.hasKeyValue(str_key))
544
545
546
547
548
			return true;
	}
	return false;
}

549
550
551
552
553
554
555
/**
 * @brief Retrieve a section's inline and block comments.
 * @param[in] str_section The section name.
 * @param[out] out_inline_comment The returned inline comment.
 * @param[out] out_block_comment The returned block comment.
 * @return True if the Section exists, false if not.
 */
556
557
bool INIParser::getSectionComment(const QString &str_section, QString &out_inline_comment,
    QString &out_block_comment)
558
{
559
	const Section *sec( sections_.getSection(str_section) );
560
561
562
563
564
565
	if (sec != nullptr) {
		out_inline_comment = sec->getInlineComment();
		out_block_comment = sec->getBlockComment();
		return true;
	}
	return false; //section does not exist
566
567
}

568
569
570
571
572
573
574
/**
 * @brief Set a section's inline and block comments.
 * @param[in] str_section The section name.
 * @param[in] inline_comment The inline comment to set.
 * @param[in] block_comment The block comment to set.
 * @return
 */
575
bool INIParser::setSectionComment(const QString &str_section, const QString &inline_comment,
576
    const QString &block_comment)
577
{
578
	Section *sec( sections_.getSection(str_section) );
579
	if (sec == nullptr)
580
		return false; //section does not exist
581
582
583
584
585
	if (!inline_comment.isNull())
		sec->setInlineComment(inline_comment);
	if (!block_comment.isNull())
		sec->setBlockComment(block_comment);
	return true; //also true if no comment was given, means "section exists"
586
587
}

588
589
590
591
592
593
594
595
/**
 * @brief Print the INIParser's contents to a text stream.
 * @param[in,out] out_ss The stream to write to.
 * @param[in] alphabetical Sort sections and keys in order of insertion or alphabetically?
 */
void INIParser::outputIni(QTextStream &out_ss, const bool &alphabetical)
{
	if (alphabetical) {
596
		SectionList sections_copy(sections_);
597
		sections_copy.sort(); //leave the original and sort a copy
598
599
		for (auto &sec : sections_copy)
			outputSectionIfKeys(sec, out_ss);
600
	} else { //order as inserted
601
602
		for (auto &sec : sections_)
			outputSectionIfKeys(sec, out_ss);
603
	}
Michael Reisecker's avatar
Michael Reisecker committed
604
	out_ss << block_comment_at_end_;
605
606
607
608
609
610
611
612
613
614
615
}

/**
 * @brief Write the INIParser's contents to the file system.
 * @param[in] outfile_name File name to output to.
 * @param[in] alphabetical Sort sections and keys in order of insertion or alphabetically?
 */
void INIParser::writeIni(const QString &outfile_name, const bool &alphabetical)
{
	QFile outfile(outfile_name);
	if (!outfile.open(QIODevice::WriteOnly)) {
616
		const QString msg( tr("Could not open INI file for writing") );
617
		display_error(msg, QString(), QDir::toNativeSeparators(outfile_name) + ":\n" + outfile.errorString());
618
		return;
619
620
621
622
	}
	QTextStream ss(&outfile);
	outputIni(ss, alphabetical);
	outfile.close();
Mathias Bavay's avatar
Mathias Bavay committed
623
	
624
625
626
627
628
629
630
	/*
	 * When creating an INI file from scratch, it is not connected to the file system yet.
	 * So we do this when writing out an INI file here so that the Workflow panel can
	 * access INI keys and so that we are in the same state as if the user had edited this
	 * file with an external program and then loaded it into INIshell.
	 * I. e., we mimick the usual "save as" behaviour.
	 */
Mathias Bavay's avatar
Mathias Bavay committed
631
	if (getMainWindow()->getIni() != this) getMainWindow()->setIni( *this );
632
633
634
}

/**
635
636
 * @brief Clear contents of the INIParser.
 * @param[in] keep_unknown_keys Only clear the keys that are being controlled by the GUI.
637
 */
638
639
640
641
642
643
644
645
646
647
648
649
650
651
void INIParser::clear(const bool &keep_unknown_keys)
{
	if (keep_unknown_keys) { //keep meta info and keys from original INI that are unknown to the GUI
		for (auto &sec : sections_) {
			for (auto &keyval : sec.getKeyValueList()) {
				if (!keyval.second.isUnknownToApp())
					sec.removeKey(keyval.first);
			}
		} //TODO: clear sections that are empty now
	} else { //only the logger is left in place
		sections_.clear();
		filename_ = QString();
		block_comment_at_end_ = QString();
	}
652
653
}

654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
/**
 * @brief Internal function to parse INI contents from a stream.
 * @details This function can be called from a file or text and is the core
 * function to parse INI contents into data containers.
 * @param[in] tstream The input text stream to read.
 * @return True if all went well.
 */
bool INIParser::parseStream(QTextStream &tstream)
{
	first_error_message_ = true; //to print error headers only once

	QString current_block_comment;
	Section *current_section = nullptr;
	bool all_ok = true; //all keys were well-formatted

	/* iterate through lines in file */
	size_t linecount = 0;
	while (!tstream.atEnd()) {
		linecount++; //for logging purposes
		const QString line( tstream.readLine() );

		/* check for comment */
		QString local_block_comment; //collect block comment to append to next section
		if (evaluateComment(line, local_block_comment)) {
			current_block_comment += local_block_comment + "\n"; //this includes empty lines
			continue;
		}

		/* check for section */
		QString section_name;
		QRegularExpressionMatch rex_match;
		const bool section_success = isSection(line, section_name, rex_match);
		if (section_success) {
			const bool has_section = sections_.hasSection(section_name);
			if (has_section) {
				current_section = sections_[section_name];
				//TODO: Think about if it's worth the hassle to allow multiple occurrences of the same section
				//TODO: If not, merge the inline comments
				current_block_comment = current_block_comment.prepend(current_section->getBlockComment());
			} else {
				Section new_section;
				new_section.setSectionProperties(rex_match);
				current_section = sections_.addSection(new_section);
			}
			current_section->setBlockComment(current_block_comment);
			current_block_comment.clear(); //clear the block comment once it's been used for a section
			current_section->sectionIsInIni(); //remember to always output, even if empty
			continue;
		} //endif section_success

		/* check for key/value pair */
		QString key_name;
		const bool keyval_success = isKeyValue(line, key_name, rex_match);
		if (keyval_success) {
			if (current_section == nullptr) {
				Section default_section;
				default_section.setName(Cst::default_section);
				default_section.defaultNameSet(); //remember that the section was set from default name
				current_section = sections_.addSection(default_section);
			}
Michael Reisecker's avatar
Michael Reisecker committed
714
			const bool has_keyval = current_section->hasKeyValue(key_name);
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
			KeyValue *current_keyval = nullptr;
			if (has_keyval) {
				current_keyval = current_section->getKeyValue(key_name);
			} else {
				KeyValue new_keyval(key_name);
				current_keyval = current_section->addKeyValue(new_keyval);
			}
			current_keyval->setKeyValProperties(rex_match);
			current_keyval->setBlockComment(current_block_comment);
			current_block_comment.clear();
		} else if (!line.trimmed().isEmpty()) { //we allow misplaced whitespace characters
			const QString msg( QString(tr("Undefined format on line %1 of file \"%2\"")).arg(linecount).arg(filename_)
			    + ": " + line );
			log(msg, "warning");
			topStatus(tr("Invalid line in file \"") + filename_ + "\"", "warning");
			all_ok = false;
		}
	} //end while tstream

	//is a comment left at the very end that can not be assigned to a following section or key?
	if (!current_block_comment.isEmpty())
		block_comment_at_end_ = current_block_comment;

	return all_ok;
	//TODO: How to handle keys that are valid multiple times (e. g. IMPORT_BEFORE)?
}

742
743
744
745
746
747
/**
 * @brief Check if an INI line is a comment, and if so, return it.
 * @param[in] line One line of the INI file.
 * @param[out] out_comment The comment including the tag (# or ;)
 * @return True if the line was parsed successfully as a comment.
 */
748
bool INIParser::evaluateComment(const QString &line, QString &out_comment)
749
{
750
	if (line.isEmpty())
751
		return true; //reproduce empty lines too
752
753
754
	static const QString regex_comment( R"(^\s*[#;].*)" );
	/*                                          |
	 *                                         (1)
755
756
	 * (1) Any line that starts with ; or #, optionally prefaced by whitespaces
	 */
757
	static constexpr size_t idx_comment = 0;
758

759
760
761
762
	static const QRegularExpression rex(regex_comment);
	const QRegularExpressionMatch matches = rex.match(line);
	out_comment = matches.captured(idx_comment);
	return matches.hasMatch();
763
764
}

765
766
767
768
769
770
771
/**
 * @brief Check if an INI line is a section header, and if so, return its properties.
 * @param[in] line One line of the INI file.
 * @param[out] out_section_name The parsed name of the section.
 * @param[out] out_rexmatch Further properties (comment, whitespaces) as parsed regex matches.
 * @return True if the line was parsed successfully as a section.
 */
772
773
bool INIParser::isSection(const QString &line, QString &out_section_name,
    QRegularExpressionMatch &out_rexmatch)
774
{
Michael Reisecker's avatar
Michael Reisecker committed
775
	static const QString regex_section( R"((\s*)\[([\w+]*)\](\s*)([#;].*)*)" );
776
777
	/*                                                |              |
	 *                                               (1)            (2)
778
779
780
	 * (1) Alphanumeric string (can't be empty) enclosed by brackets
	 * (2) Comment started with ; or #
	 */
781
782
	static constexpr size_t idx_total = 0;
	static constexpr size_t idx_name = 2;
783
	static const QRegularExpression rex(regex_section);
784

785
786
	out_rexmatch = rex.match(line);
	out_section_name = out_rexmatch.captured(idx_name);
787
	return (line == out_rexmatch.captured(idx_total)); //full match?
788
789
}

790
791
792
793
794
795
796
/**
 * @brief Check if an INI line is a key/value pair, and if so, return its properties.
 * @param[in] line One line of the INI file.
 * @param[out] out_key_name The parsed name of the key.
 * @param[out] out_rexmatch Further properties (value, comment, whitespaces) as parsed regex matches.
 * @return True if the line was parsed successfully as a key/value pair.
 */
797
798
bool INIParser::isKeyValue(const QString &line, QString &out_key_name,
    QRegularExpressionMatch &out_rexmatch)
799
{
800
801
#ifdef DEBUG
	if (line.isEmpty()) {
802
		qDebug() << "Empty line should not have reached INIParser::isKeyValue(), since it should be added to a comment.";
803
804
805
806
		return false;
	}
#endif //def DEBUG

807
	static const QString regex_keyval(
808
809
810
811
	    R"((\s*)([\w\*\-:_.]*)(\s*)=(\s*)(;$|#$|.+?)(\s*)(#.*|;.*|$))" );
	/*                  |       \    /        \             |
	 *                 (1)       \  /         (3)          (4)
	 *                            (2)
812
	 * (1) Alphanumeric string for the key (can't be empty)
813
814
815
	 * (3) Any number of whitespacescaround =
	 * (4) Either a single ; or # (e. g. csv delimiter) or any non-empty string for the value
	 * (5) The value is either ended by a comment or the end of the line
816
	 */
817
818
	static constexpr size_t idx_total = 0;
	static constexpr size_t idx_key = 2;
819

820
	static const QRegularExpression rex(regex_keyval);
821
822
	out_rexmatch = rex.match(line);
	out_key_name = out_rexmatch.captured(idx_key);
823
	return (line == out_rexmatch.captured(idx_total)); //full match?
824
825
}

826
827
828
829
830
831
/**
 * @brief Convenience wrapper for the Logger.
 * @details In command line mode the logger is skipped and the message is written to stderr.
 * @param[in] message The message to log.
 * @param[in] color Color of the log text (ignored in command line mode).
 */
832
void INIParser::log(const QString &message, const QString &color)
833
{
834
835
836
	//If all is well, don't log anything. If at least one error occurred, prepend
	//a log message about which file is being read:
	if (logger_instance_ != nullptr) { //GUI mode
837
		if (first_error_message_) {
838
			logger_instance_->log(tr(R"(Reading INI file "%1"...)").arg(filename_));
839
			first_error_message_ = false;
840
		}
841
		logger_instance_->log(message, color);
842
	} else {
843
		std::cerr << "[W] " << message.toStdString() << std::endl;
844
	}
845
}
846

847
848
849
850
851
852
853
854
855
856
/**
 * @brief Helper function to output section and skip section header if empty.
 * @param[in] section The section to print.
 * @param[in] out_ss Text stream to print to.
 */
void INIParser::outputSectionIfKeys(Section &section, QTextStream &out_ss)
{
	QString out_ss_keys;
	QTextStream keys_stream(&out_ss_keys);
	section.printKeyValues(keys_stream);
Michael Reisecker's avatar
Michael Reisecker committed
857
858
	//TODO: small thing: if an input INI section contains only invalid keys, then it will still be printed.
	//This is because we don't keep track of invalid lines and therefore don't know this.
859
860
861
862
863
864
	if (!out_ss_keys.isEmpty() || section.isSectionInIni()) {
		section.print(out_ss);
		out_ss << out_ss_keys;
	}
}

865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
/**
 * @brief Convenience wrapper for errors.
 * @details In command line mode the GUI error is skipped and the error is written to stderr.
 * @param[in] error_msg The error to display.
 * @param[in] error_info Additional info about the error.
 * @param[in] error_details A detailed description of the error.
 */
void INIParser::display_error(const QString &error_msg, const QString &error_info, const QString &error_details)
{
	if (logger_instance_ != nullptr) //GUI mode
		Error(error_msg, error_info, error_details);
	else
		std::cerr << "[E] " << error_msg.toStdString() << (error_info.isEmpty()? "" : ", ") <<
		    error_info.toStdString() << (error_details.isEmpty()? "" : "; ") <<
		    error_details.toStdString() << std::endl;
}