Add AdvancedIniFile for better preserving certain aspects like comments
This commit is contained in:
parent
fe5dc5e6a7
commit
43550b6d55
390
io/inifile.cpp
390
io/inifile.cpp
|
@ -1,74 +1,78 @@
|
|||
#include "./inifile.h"
|
||||
|
||||
#include "../conversion/stringconversion.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace CppUtilities {
|
||||
|
||||
/// \cond
|
||||
static void addChar(char c, std::string &to, std::size_t &padding)
|
||||
{
|
||||
if (c == ' ') {
|
||||
++padding;
|
||||
return;
|
||||
}
|
||||
if (!to.empty()) {
|
||||
while (padding) {
|
||||
to += ' ';
|
||||
--padding;
|
||||
}
|
||||
} else {
|
||||
padding = 0;
|
||||
}
|
||||
to += c;
|
||||
};
|
||||
/// \endcond
|
||||
|
||||
/*!
|
||||
* \class IniFile
|
||||
* \brief The IniFile class parses and makes INI files.
|
||||
* \todo
|
||||
* - Preserve difference between absence of equal sign and empty value after equal sign.
|
||||
* - Preserve order within section.
|
||||
* - Preserve comments.
|
||||
* \brief The IniFile class allows parsing and writing INI files.
|
||||
* \sa See AdvancedIniFile for a more advanced version which preserves more particularities of the original file.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \brief Parses all data from the specified \a inputStream.
|
||||
* \throws Throws an std::ios_base::failure when an IO error (other than end-of-file) occurs.
|
||||
*/
|
||||
void IniFile::parse(std::istream &inputStream)
|
||||
{
|
||||
// define variable for state machine
|
||||
enum State { Init, Comment, ScopeName, Key, Value } state = Init;
|
||||
// current character
|
||||
char c;
|
||||
// number of postponed whitespaces
|
||||
unsigned int whitespace = 0;
|
||||
// current scope, key and value
|
||||
string scope, key, value;
|
||||
scope.reserve(16);
|
||||
inputStream.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
|
||||
// define variables for state machine
|
||||
enum State { Init, Comment, SectionName, Key, Value } state = Init;
|
||||
char currentCharacter;
|
||||
|
||||
// keep track of current scope, key and value and number of postponed whitespaces
|
||||
std::size_t whitespace = 0;
|
||||
string sectionName, key, value;
|
||||
sectionName.reserve(16);
|
||||
key.reserve(16);
|
||||
value.reserve(256);
|
||||
// define actions for state machine
|
||||
// called when key/value pair is complete
|
||||
const auto finishKeyValue = [&state, &scope, &key, &value, &whitespace, this] {
|
||||
|
||||
// define function to add a key/value pair
|
||||
const auto finishKeyValue = [&state, §ionName, &key, &value, &whitespace, this] {
|
||||
if (key.empty() && value.empty() && state != Value) {
|
||||
return;
|
||||
}
|
||||
if (m_data.empty() || m_data.back().first != scope) {
|
||||
m_data.emplace_back(make_pair(scope, decltype(m_data)::value_type::second_type()));
|
||||
if (m_data.empty() || m_data.back().first != sectionName) {
|
||||
m_data.emplace_back(make_pair(sectionName, decltype(m_data)::value_type::second_type()));
|
||||
}
|
||||
m_data.back().second.insert(make_pair(key, value));
|
||||
key.clear();
|
||||
value.clear();
|
||||
whitespace = 0;
|
||||
};
|
||||
// called to add current character to current key or value
|
||||
const auto addChar = [&whitespace, &c](string &to) {
|
||||
if (c == ' ') {
|
||||
++whitespace;
|
||||
} else {
|
||||
if (!to.empty()) {
|
||||
while (whitespace) {
|
||||
to += ' ';
|
||||
--whitespace;
|
||||
}
|
||||
} else {
|
||||
whitespace = 0;
|
||||
}
|
||||
to += c;
|
||||
}
|
||||
};
|
||||
// thorw an exception when an IO error occurs
|
||||
inputStream.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
|
||||
// parse the file char by char
|
||||
try {
|
||||
while (inputStream.get(c)) {
|
||||
while (inputStream.get(currentCharacter)) {
|
||||
// handle next character
|
||||
switch (state) {
|
||||
case Init:
|
||||
switch (c) {
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
break;
|
||||
case '#':
|
||||
|
@ -79,16 +83,16 @@ void IniFile::parse(std::istream &inputStream)
|
|||
state = Value;
|
||||
break;
|
||||
case '[':
|
||||
scope.clear();
|
||||
state = ScopeName;
|
||||
sectionName.clear();
|
||||
state = SectionName;
|
||||
break;
|
||||
default:
|
||||
addChar(key);
|
||||
addChar(currentCharacter, key, whitespace);
|
||||
state = Key;
|
||||
}
|
||||
break;
|
||||
case Key:
|
||||
switch (c) {
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
finishKeyValue();
|
||||
state = Init;
|
||||
|
@ -102,28 +106,28 @@ void IniFile::parse(std::istream &inputStream)
|
|||
state = Value;
|
||||
break;
|
||||
default:
|
||||
addChar(key);
|
||||
addChar(currentCharacter, key, whitespace);
|
||||
}
|
||||
break;
|
||||
case Comment:
|
||||
switch (c) {
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
state = Init;
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
break;
|
||||
case ScopeName:
|
||||
switch (c) {
|
||||
case SectionName:
|
||||
switch (currentCharacter) {
|
||||
case ']':
|
||||
state = Init;
|
||||
break;
|
||||
default:
|
||||
scope += c;
|
||||
sectionName += currentCharacter;
|
||||
}
|
||||
break;
|
||||
case Value:
|
||||
switch (c) {
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
finishKeyValue();
|
||||
state = Init;
|
||||
|
@ -133,7 +137,7 @@ void IniFile::parse(std::istream &inputStream)
|
|||
state = Comment;
|
||||
break;
|
||||
default:
|
||||
addChar(value);
|
||||
addChar(currentCharacter, value, whitespace);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -142,26 +146,300 @@ void IniFile::parse(std::istream &inputStream)
|
|||
if (!inputStream.eof()) {
|
||||
throw;
|
||||
}
|
||||
// we just reached the end of the file
|
||||
// don't forget to save the last key/value pair
|
||||
|
||||
// handle eof
|
||||
finishKeyValue();
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Write the current data to the specified \a outputStream.
|
||||
* \throws Throws an std::ios_base::failure when an IO error occurs.
|
||||
*/
|
||||
void IniFile::make(ostream &outputStream)
|
||||
{
|
||||
// thorw an exception when an IO error occurs
|
||||
outputStream.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
for (const auto &scope : m_data) {
|
||||
outputStream << '[' << scope.first << ']' << '\n';
|
||||
for (const auto &field : scope.second) {
|
||||
for (const auto §ion : m_data) {
|
||||
outputStream << '[' << section.first << ']' << '\n';
|
||||
for (const auto &field : section.second) {
|
||||
outputStream << field.first << '=' << field.second << '\n';
|
||||
}
|
||||
outputStream << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \class AdvancedIniFile
|
||||
* \brief The AdvancedIniFile class allows parsing and writing INI files.
|
||||
*
|
||||
* In contrast to IniFile this class preserves
|
||||
* - the difference between absence of an equal sign and an empty value after equal sign.
|
||||
* - the order of the fields within a section.
|
||||
* - alignment of equal signs via extra spaces between key and equal sign.
|
||||
* - comments.
|
||||
*
|
||||
* In the following description the word "element" is used to refer to a section or a field.
|
||||
*
|
||||
* The parsed data or data to be written is directly exposed via the "sections" member variable of this class. There are also
|
||||
* convenience functions to find a particular element.
|
||||
*
|
||||
* Since the preceding comment of an element usually belongs to it each element has a "precedingCommentBlock" member variable.
|
||||
* If a file ends with a comment an implicit section is added at the end which contains that comment as "precedingCommentBlock".
|
||||
* Comments are not stripped from newline and '#' characters. When altering/adding comments be sure to use newline and '#'
|
||||
* characters as needed.
|
||||
*
|
||||
* Comments appearing in the same line as an element are stored using the "followingInlineComment" member variable.
|
||||
*
|
||||
* \remarks
|
||||
* The AdvancedIniFile class is still experimental. It might be modified in an incompatible way or even removed
|
||||
* in the next minor or patch release.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \class AdvancedIniFile::Section
|
||||
* \brief The AdvancedIniFile::Section class represents a section within an INI file.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \class AdvancedIniFile::Field
|
||||
* \brief The AdvancedIniFile::Field class represents a field within an INI file.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \brief Parses all data from the specified \a inputStream.
|
||||
* \remarks
|
||||
* Does *not* strip newline and '#' characters from comments. So far there is no option (or separate function) to help with that.
|
||||
* \throws Throws an std::ios_base::failure when an IO error (other than end-of-file) occurs.
|
||||
*/
|
||||
void AdvancedIniFile::parse(std::istream &inputStream, IniFileParseOptions)
|
||||
{
|
||||
inputStream.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
|
||||
// define variables for state machine
|
||||
enum State { Init, CommentBlock, InlineComment, SectionInlineComment, SectionName, SectionEnd, Key, Value } state = Init;
|
||||
char currentCharacter;
|
||||
|
||||
// keep track of current comment, section, key and value
|
||||
std::string commentBlock, inlineComment, sectionName, key, value;
|
||||
std::size_t keyPadding = 0, valuePadding = 0;
|
||||
commentBlock.reserve(256);
|
||||
inlineComment.reserve(256);
|
||||
sectionName.reserve(16);
|
||||
key.reserve(16);
|
||||
value.reserve(256);
|
||||
|
||||
// define function to add entry
|
||||
const auto finishKeyValue = [&, this] {
|
||||
if (key.empty() && value.empty() && state != Value) {
|
||||
return;
|
||||
}
|
||||
if (sections.empty()) {
|
||||
sections.emplace_back(Section{ .flags = IniFileSectionFlags::Implicit });
|
||||
}
|
||||
sections.back().fields.emplace_back(Field{ .key = key,
|
||||
.value = value,
|
||||
.precedingCommentBlock = commentBlock,
|
||||
.followingInlineComment = inlineComment,
|
||||
.paddedKeyLength = key.size() + keyPadding,
|
||||
.flags = (!value.empty() || state == Value ? IniFileFieldFlags::HasValue : IniFileFieldFlags::None) });
|
||||
key.clear();
|
||||
value.clear();
|
||||
commentBlock.clear();
|
||||
inlineComment.clear();
|
||||
keyPadding = valuePadding = 0;
|
||||
};
|
||||
|
||||
// parse the file char by char
|
||||
try {
|
||||
while (inputStream.get(currentCharacter)) {
|
||||
// handle next character
|
||||
switch (state) {
|
||||
case Init:
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
commentBlock += currentCharacter;
|
||||
break;
|
||||
case '#':
|
||||
commentBlock += currentCharacter;
|
||||
state = CommentBlock;
|
||||
break;
|
||||
case '=':
|
||||
keyPadding = valuePadding = 0;
|
||||
state = Value;
|
||||
break;
|
||||
case '[':
|
||||
sectionName.clear();
|
||||
state = SectionName;
|
||||
break;
|
||||
default:
|
||||
addChar(currentCharacter, key, keyPadding);
|
||||
state = Key;
|
||||
}
|
||||
break;
|
||||
case Key:
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
finishKeyValue();
|
||||
state = Init;
|
||||
break;
|
||||
case '#':
|
||||
state = InlineComment;
|
||||
inlineComment += currentCharacter;
|
||||
break;
|
||||
case '=':
|
||||
valuePadding = 0;
|
||||
state = Value;
|
||||
break;
|
||||
default:
|
||||
addChar(currentCharacter, key, keyPadding);
|
||||
}
|
||||
break;
|
||||
case CommentBlock:
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
state = Init;
|
||||
[[fallthrough]];
|
||||
default:
|
||||
commentBlock += currentCharacter;
|
||||
}
|
||||
break;
|
||||
case InlineComment:
|
||||
case SectionInlineComment:
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
switch (state) {
|
||||
case InlineComment:
|
||||
finishKeyValue();
|
||||
break;
|
||||
case SectionInlineComment:
|
||||
sections.back().followingInlineComment = inlineComment;
|
||||
inlineComment.clear();
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
state = Init;
|
||||
break;
|
||||
default:
|
||||
inlineComment += currentCharacter;
|
||||
}
|
||||
break;
|
||||
case SectionName:
|
||||
switch (currentCharacter) {
|
||||
case ']':
|
||||
state = SectionEnd;
|
||||
sections.emplace_back(Section{ .name = sectionName });
|
||||
sections.back().precedingCommentBlock = commentBlock;
|
||||
sectionName.clear();
|
||||
commentBlock.clear();
|
||||
break;
|
||||
default:
|
||||
sectionName += currentCharacter;
|
||||
}
|
||||
break;
|
||||
case SectionEnd:
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
state = Init;
|
||||
break;
|
||||
case '#':
|
||||
state = SectionInlineComment;
|
||||
inlineComment += currentCharacter;
|
||||
break;
|
||||
case '=':
|
||||
keyPadding = valuePadding = 0;
|
||||
state = Value;
|
||||
break;
|
||||
case ' ':
|
||||
break;
|
||||
default:
|
||||
state = Key;
|
||||
addChar(currentCharacter, key, keyPadding);
|
||||
}
|
||||
break;
|
||||
case Value:
|
||||
switch (currentCharacter) {
|
||||
case '\n':
|
||||
finishKeyValue();
|
||||
state = Init;
|
||||
break;
|
||||
case '#':
|
||||
state = InlineComment;
|
||||
inlineComment += currentCharacter;
|
||||
break;
|
||||
default:
|
||||
addChar(currentCharacter, value, valuePadding);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (const std::ios_base::failure &) {
|
||||
if (!inputStream.eof()) {
|
||||
throw;
|
||||
}
|
||||
|
||||
// handle eof
|
||||
switch (state) {
|
||||
case Init:
|
||||
case CommentBlock:
|
||||
sections.emplace_back(Section{ .precedingCommentBlock = commentBlock, .flags = IniFileSectionFlags::Implicit });
|
||||
break;
|
||||
case SectionName:
|
||||
sections.emplace_back(Section{ .name = sectionName, .precedingCommentBlock = commentBlock, .flags = IniFileSectionFlags::Implicit });
|
||||
break;
|
||||
case SectionEnd:
|
||||
case SectionInlineComment:
|
||||
sections.emplace_back(Section{ .name = sectionName, .precedingCommentBlock = commentBlock, .followingInlineComment = inlineComment });
|
||||
break;
|
||||
case Key:
|
||||
case Value:
|
||||
case InlineComment:
|
||||
finishKeyValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Write the current data to the specified \a outputStream.
|
||||
* \throws Throws an std::ios_base::failure when an IO error occurs.
|
||||
* \remarks
|
||||
* Might write garbage if comments do not contain newline and '#' characters as needed. So far there is no option to insert these characters
|
||||
* automatically as needed.
|
||||
*/
|
||||
void AdvancedIniFile::make(ostream &outputStream, IniFileMakeOptions)
|
||||
{
|
||||
outputStream.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
for (const auto §ion : sections) {
|
||||
if (!section.precedingCommentBlock.empty()) {
|
||||
outputStream << section.precedingCommentBlock;
|
||||
}
|
||||
if (!(section.flags & IniFileSectionFlags::Implicit)) {
|
||||
outputStream << '[' << section.name << ']';
|
||||
if (!section.followingInlineComment.empty()) {
|
||||
outputStream << ' ' << section.followingInlineComment;
|
||||
}
|
||||
outputStream << '\n';
|
||||
}
|
||||
for (const auto &field : section.fields) {
|
||||
if (!field.precedingCommentBlock.empty()) {
|
||||
outputStream << field.precedingCommentBlock;
|
||||
}
|
||||
outputStream << field.key;
|
||||
for (auto charsWritten = field.key.size(); charsWritten < field.paddedKeyLength; ++charsWritten) {
|
||||
outputStream << ' ';
|
||||
}
|
||||
if (field.flags & IniFileFieldFlags::HasValue) {
|
||||
outputStream << '=' << ' ' << field.value;
|
||||
}
|
||||
if (!field.followingInlineComment.empty()) {
|
||||
if (field.flags & IniFileFieldFlags::HasValue) {
|
||||
outputStream << ' ';
|
||||
}
|
||||
outputStream << field.followingInlineComment;
|
||||
}
|
||||
outputStream << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace CppUtilities
|
||||
|
|
187
io/inifile.h
187
io/inifile.h
|
@ -2,9 +2,12 @@
|
|||
#define IOUTILITIES_INIFILE_H
|
||||
|
||||
#include "../global.h"
|
||||
#include "../misc/flagenumclass.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iosfwd>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
@ -54,6 +57,190 @@ inline const IniFile::ScopeList &IniFile::data() const
|
|||
return m_data;
|
||||
}
|
||||
|
||||
enum class IniFileParseOptions {
|
||||
None = 0,
|
||||
};
|
||||
|
||||
enum class IniFileMakeOptions {
|
||||
None = 0,
|
||||
};
|
||||
|
||||
enum class IniFileFieldFlags {
|
||||
None = 0,
|
||||
HasValue = (1 << 0),
|
||||
};
|
||||
|
||||
enum class IniFileSectionFlags {
|
||||
None = 0,
|
||||
Implicit = (1 << 0),
|
||||
Truncated = (1 << 1),
|
||||
};
|
||||
|
||||
struct CPP_UTILITIES_EXPORT AdvancedIniFile {
|
||||
struct Field {
|
||||
std::string key;
|
||||
std::string value;
|
||||
std::string precedingCommentBlock;
|
||||
std::string followingInlineComment;
|
||||
std::size_t paddedKeyLength = 0;
|
||||
IniFileFieldFlags flags = IniFileFieldFlags::None;
|
||||
};
|
||||
using FieldList = std::vector<Field>;
|
||||
struct Section {
|
||||
FieldList::iterator findField(std::string_view key);
|
||||
FieldList::const_iterator findField(std::string_view key) const;
|
||||
FieldList::iterator findField(FieldList::iterator after, std::string_view key);
|
||||
FieldList::const_iterator findField(FieldList::iterator after, std::string_view key) const;
|
||||
FieldList::iterator fieldEnd();
|
||||
FieldList::const_iterator fieldEnd() const;
|
||||
|
||||
std::string name;
|
||||
FieldList fields;
|
||||
std::string precedingCommentBlock;
|
||||
std::string followingInlineComment;
|
||||
IniFileSectionFlags flags = IniFileSectionFlags::None;
|
||||
};
|
||||
using SectionList = std::vector<Section>;
|
||||
|
||||
SectionList::iterator findSection(std::string_view sectionName);
|
||||
SectionList::const_iterator findSection(std::string_view sectionName) const;
|
||||
SectionList::iterator findSection(SectionList::iterator after, std::string_view sectionName);
|
||||
SectionList::const_iterator findSection(SectionList::iterator after, std::string_view sectionName) const;
|
||||
SectionList::iterator sectionEnd();
|
||||
SectionList::const_iterator sectionEnd() const;
|
||||
std::optional<FieldList::iterator> findField(std::string_view sectionName, std::string_view key);
|
||||
std::optional<FieldList::const_iterator> findField(std::string_view sectionName, std::string_view key) const;
|
||||
void parse(std::istream &inputStream, IniFileParseOptions options = IniFileParseOptions::None);
|
||||
void make(std::ostream &outputStream, IniFileMakeOptions options = IniFileMakeOptions::None);
|
||||
|
||||
SectionList sections;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first section with the name \a sectionName.
|
||||
*/
|
||||
inline AdvancedIniFile::SectionList::iterator AdvancedIniFile::findSection(std::string_view sectionName)
|
||||
{
|
||||
return std::find_if(sections.begin(), sections.end(), [§ionName](const auto &scope) { return scope.name == sectionName; });
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first section with the name \a sectionName.
|
||||
*/
|
||||
inline AdvancedIniFile::SectionList::const_iterator AdvancedIniFile::findSection(std::string_view sectionName) const
|
||||
{
|
||||
return const_cast<AdvancedIniFile *>(this)->findSection(sectionName);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first section with the name \a sectionName which comes after \a after.
|
||||
*/
|
||||
inline AdvancedIniFile::SectionList::iterator AdvancedIniFile::findSection(SectionList::iterator after, std::string_view sectionName)
|
||||
{
|
||||
return std::find_if(after + 1, sections.end(), [§ionName](const auto &scope) { return scope.name == sectionName; });
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first section with the name \a sectionName which comes after \a after.
|
||||
*/
|
||||
inline AdvancedIniFile::SectionList::const_iterator AdvancedIniFile::findSection(SectionList::iterator after, std::string_view sectionName) const
|
||||
{
|
||||
return const_cast<AdvancedIniFile *>(this)->findSection(after, sectionName);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator that points one past the last section.
|
||||
*/
|
||||
inline AdvancedIniFile::SectionList::iterator AdvancedIniFile::sectionEnd()
|
||||
{
|
||||
return sections.end();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator that points one past the last section.
|
||||
*/
|
||||
inline AdvancedIniFile::SectionList::const_iterator AdvancedIniFile::sectionEnd() const
|
||||
{
|
||||
return sections.end();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first field within the first section with matching \a sectionName and \a key.
|
||||
*/
|
||||
inline std::optional<AdvancedIniFile::FieldList::iterator> AdvancedIniFile::findField(std::string_view sectionName, std::string_view key)
|
||||
{
|
||||
const SectionList::iterator scope = findSection(sectionName);
|
||||
if (scope == sectionEnd()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const FieldList::iterator field = scope->findField(key);
|
||||
if (field == scope->fieldEnd()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first field within the first section with matching \a sectionName and \a key.
|
||||
*/
|
||||
inline std::optional<AdvancedIniFile::FieldList::const_iterator> AdvancedIniFile::findField(std::string_view sectionName, std::string_view key) const
|
||||
{
|
||||
return const_cast<AdvancedIniFile *>(this)->findField(sectionName, key);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first field with the key \a key.
|
||||
*/
|
||||
inline AdvancedIniFile::FieldList::iterator AdvancedIniFile::Section::findField(std::string_view key)
|
||||
{
|
||||
return std::find_if(fields.begin(), fields.end(), [&key](const auto &field) { return field.key == key; });
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first field with the key \a key.
|
||||
*/
|
||||
inline AdvancedIniFile::FieldList::const_iterator AdvancedIniFile::Section::findField(std::string_view key) const
|
||||
{
|
||||
return const_cast<Section *>(this)->findField(key);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first field with the key \a key which comes after \a after.
|
||||
*/
|
||||
inline AdvancedIniFile::FieldList::iterator AdvancedIniFile::Section::findField(FieldList::iterator after, std::string_view key)
|
||||
{
|
||||
return std::find_if(after + 1, fields.end(), [&key](const auto &field) { return field.key == key; });
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator to the first field with the key \a key which comes after \a after.
|
||||
*/
|
||||
inline AdvancedIniFile::FieldList::const_iterator AdvancedIniFile::Section::findField(FieldList::iterator after, std::string_view key) const
|
||||
{
|
||||
return const_cast<Section *>(this)->findField(after, key);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator that points one past the last field.
|
||||
*/
|
||||
inline AdvancedIniFile::FieldList::iterator AdvancedIniFile::Section::fieldEnd()
|
||||
{
|
||||
return fields.end();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns an iterator that points one past the last field.
|
||||
*/
|
||||
inline AdvancedIniFile::FieldList::const_iterator AdvancedIniFile::Section::fieldEnd() const
|
||||
{
|
||||
return fields.end();
|
||||
}
|
||||
|
||||
} // namespace CppUtilities
|
||||
|
||||
CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(CppUtilities, IniFileParseOptions);
|
||||
CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(CppUtilities, IniFileMakeOptions);
|
||||
CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(CppUtilities, IniFileFieldFlags);
|
||||
CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(CppUtilities, IniFileSectionFlags);
|
||||
|
||||
#endif // IOUTILITIES_INIFILE_H
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
# Based on config file from https://git.archlinux.org/svntogit/packages.git/?h=packages/pacman
|
||||
# Used here as an example to test the advanced INI (de)serializer
|
||||
#
|
||||
# /etc/pacman.conf
|
||||
#
|
||||
# See the pacman.conf(5) manpage for option and repository directives
|
||||
|
||||
#
|
||||
# GENERAL OPTIONS
|
||||
#
|
||||
[options]
|
||||
# The following paths are commented out with their default values listed.
|
||||
# If you wish to use different paths, uncomment and update the paths.
|
||||
#RootDir = /
|
||||
#DBPath = /var/lib/pacman/
|
||||
#CacheDir = /var/cache/pacman/pkg/
|
||||
#LogFile = /var/log/pacman.log
|
||||
#GPGDir = /etc/pacman.d/gnupg/
|
||||
#HookDir = /etc/pacman.d/hooks/
|
||||
HoldPkg = pacman glibc
|
||||
#XferCommand = /usr/bin/curl -L -C - -f -o %o %u
|
||||
#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u
|
||||
#CleanMethod = KeepInstalled
|
||||
Foo = bar # inline comment
|
||||
Architecture = auto
|
||||
|
||||
# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup
|
||||
#IgnorePkg =
|
||||
#IgnoreGroup =
|
||||
|
||||
#NoUpgrade =
|
||||
#NoExtract =
|
||||
|
||||
# Misc options
|
||||
#UseSyslog
|
||||
#Color
|
||||
#TotalDownload
|
||||
CheckSpace
|
||||
Bar # another inline comment
|
||||
#VerbosePkgLists
|
||||
|
||||
# By default, pacman accepts packages signed by keys that its local keyring
|
||||
# trusts (see pacman-key and its man page), as well as unsigned packages.
|
||||
SigLevel = Required DatabaseOptional
|
||||
LocalFileSigLevel = Optional
|
||||
#RemoteFileSigLevel = Required
|
||||
|
||||
# NOTE: You must run `pacman-key --init` before first using pacman; the local
|
||||
# keyring can then be populated with the keys of all official Arch Linux
|
||||
# packagers with `pacman-key --populate archlinux`.
|
||||
|
||||
#
|
||||
# REPOSITORIES
|
||||
# - can be defined here or included from another file
|
||||
# - pacman will search repositories in the order defined here
|
||||
# - local/custom mirrors can be added here or in separate files
|
||||
# - repositories listed first will take precedence when packages
|
||||
# have identical names, regardless of version number
|
||||
# - URLs will have $repo replaced by the name of the current repo
|
||||
# - URLs will have $arch replaced by the name of the architecture
|
||||
#
|
||||
# Repository entries are of the format:
|
||||
# [repo-name]
|
||||
# Server = ServerName
|
||||
# Include = IncludePath
|
||||
#
|
||||
# The header [repo-name] is crucial - it must be present and
|
||||
# uncommented to enable the repo.
|
||||
#
|
||||
|
||||
# The testing repositories are disabled by default. To enable, uncomment the
|
||||
# repo name header and Include lines. You can add preferred servers immediately
|
||||
# after the header, and they will be used before the default mirrors.
|
||||
|
||||
#[testing]
|
||||
#Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[core]
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[extra] # an inline comment after a scope name
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
#[community-testing]
|
||||
#Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[community]
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
# If you want to run 32 bit applications on your x86_64 system,
|
||||
# enable the multilib repositories as required here.
|
||||
|
||||
#[multilib-testing]
|
||||
#Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
#[multilib]
|
||||
#Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
# An example of a custom package repository. See the pacman manpage for
|
||||
# tips on creating your own repositories.
|
||||
#[custom]
|
||||
#SigLevel = Optional TrustAll
|
||||
#Server = file:///home/custompkgs
|
|
@ -45,6 +45,7 @@ class IoTests : public TestFixture {
|
|||
CPPUNIT_TEST(testBitReader);
|
||||
CPPUNIT_TEST(testPathUtilities);
|
||||
CPPUNIT_TEST(testIniFile);
|
||||
CPPUNIT_TEST(testAdvancedIniFile);
|
||||
CPPUNIT_TEST(testCopy);
|
||||
CPPUNIT_TEST(testReadFile);
|
||||
CPPUNIT_TEST(testWriteFile);
|
||||
|
@ -63,6 +64,7 @@ public:
|
|||
void testBitReader();
|
||||
void testPathUtilities();
|
||||
void testIniFile();
|
||||
void testAdvancedIniFile();
|
||||
void testCopy();
|
||||
void testReadFile();
|
||||
void testWriteFile();
|
||||
|
@ -316,6 +318,74 @@ void IoTests::testIniFile()
|
|||
CPPUNIT_ASSERT(ini.data() == ini2.data());
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests AdvancedIniFile.
|
||||
*/
|
||||
void IoTests::testAdvancedIniFile()
|
||||
{
|
||||
// prepare reading test file
|
||||
fstream inputFile;
|
||||
inputFile.exceptions(ios_base::failbit | ios_base::badbit);
|
||||
inputFile.open(testFilePath("pacman.conf"), ios_base::in);
|
||||
|
||||
// parse the test file
|
||||
AdvancedIniFile ini;
|
||||
ini.parse(inputFile);
|
||||
|
||||
// check whether scope data is as expected
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("5 scopes (taking implicit empty section at the end into account)", 5_st, ini.sections.size());
|
||||
auto options = ini.findSection("options");
|
||||
CPPUNIT_ASSERT(options != ini.sectionEnd());
|
||||
TESTUTILS_ASSERT_LIKE_FLAGS(
|
||||
"comment block before section", "# Based on.*\n.*# GENERAL OPTIONS\n#\n"s, std::regex::extended, options->precedingCommentBlock);
|
||||
CPPUNIT_ASSERT_EQUAL(7_st, options->fields.size());
|
||||
CPPUNIT_ASSERT_EQUAL("HoldPkg"s, options->fields[0].key);
|
||||
CPPUNIT_ASSERT_EQUAL("pacman glibc"s, options->fields[0].value);
|
||||
CPPUNIT_ASSERT_MESSAGE("value present", options->fields[0].flags & IniFileFieldFlags::HasValue);
|
||||
TESTUTILS_ASSERT_LIKE_FLAGS("comment block between section header and first field",
|
||||
"# The following paths are.*\n.*#HookDir = /etc/pacman\\.d/hooks/\n"s, std::regex::extended, options->fields[0].precedingCommentBlock);
|
||||
CPPUNIT_ASSERT_EQUAL(""s, options->fields[0].followingInlineComment);
|
||||
CPPUNIT_ASSERT_EQUAL("Foo"s, options->fields[1].key);
|
||||
CPPUNIT_ASSERT_EQUAL("bar"s, options->fields[1].value);
|
||||
CPPUNIT_ASSERT_MESSAGE("value present", options->fields[1].flags & IniFileFieldFlags::HasValue);
|
||||
TESTUTILS_ASSERT_LIKE_FLAGS("comment block between fields", "#XferCommand.*\n.*#CleanMethod = KeepInstalled\n"s, std::regex::extended,
|
||||
options->fields[1].precedingCommentBlock);
|
||||
CPPUNIT_ASSERT_EQUAL("# inline comment"s, options->fields[1].followingInlineComment);
|
||||
CPPUNIT_ASSERT_EQUAL("CheckSpace"s, options->fields[3].key);
|
||||
CPPUNIT_ASSERT_EQUAL(""s, options->fields[3].value);
|
||||
CPPUNIT_ASSERT_MESSAGE("no value present", !(options->fields[3].flags & IniFileFieldFlags::HasValue));
|
||||
TESTUTILS_ASSERT_LIKE_FLAGS("empty lines in comments preserved", "\n# Pacman.*\n.*\n\n#NoUpgrade =\n.*#TotalDownload\n"s, std::regex::extended,
|
||||
options->fields[3].precedingCommentBlock);
|
||||
CPPUNIT_ASSERT_EQUAL(""s, options->fields[3].followingInlineComment);
|
||||
auto extraScope = ini.findSection(options, "extra");
|
||||
CPPUNIT_ASSERT(extraScope != ini.sectionEnd());
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("comment block which is only an empty line", "\n"s, extraScope->precedingCommentBlock);
|
||||
CPPUNIT_ASSERT_EQUAL_MESSAGE("inline comment after scope", "# an inline comment after a scope name"s, extraScope->followingInlineComment);
|
||||
CPPUNIT_ASSERT_EQUAL(1_st, extraScope->fields.size());
|
||||
CPPUNIT_ASSERT(ini.sections.back().flags & IniFileSectionFlags::Implicit);
|
||||
TESTUTILS_ASSERT_LIKE_FLAGS("comment block after last field present in implicitly added last scope", "\n# If you.*\n.*custompkgs\n"s,
|
||||
std::regex::extended, ini.sections.back().precedingCommentBlock);
|
||||
|
||||
// test finding a field from file level and const access
|
||||
const auto *const constIniFile = &ini;
|
||||
auto includeField = constIniFile->findField("extra", "Include");
|
||||
CPPUNIT_ASSERT(includeField.has_value());
|
||||
CPPUNIT_ASSERT_EQUAL("Include"s, includeField.value()->key);
|
||||
CPPUNIT_ASSERT_EQUAL("/etc/pacman.d/mirrorlist"s, includeField.value()->value);
|
||||
CPPUNIT_ASSERT_MESSAGE("field not present", !constIniFile->findField("extra", "Includ").has_value());
|
||||
CPPUNIT_ASSERT_MESSAGE("scope not present", !constIniFile->findField("extr", "Includ").has_value());
|
||||
|
||||
// write values again; there shouldn't be a difference as the parser and the writer are supposed to
|
||||
// preserve the order of all elements and comments
|
||||
std::stringstream newFile;
|
||||
ini.make(newFile);
|
||||
std::string originalContents;
|
||||
inputFile.clear();
|
||||
inputFile.seekg(std::ios_base::beg);
|
||||
originalContents.assign((istreambuf_iterator<char>(inputFile)), istreambuf_iterator<char>());
|
||||
CPPUNIT_ASSERT_EQUAL(originalContents, newFile.str());
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests CopyHelper.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue