449 lines
15 KiB
C++
449 lines
15 KiB
C++
#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 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)
|
|
{
|
|
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 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 != 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;
|
|
};
|
|
|
|
// parse the file char by char
|
|
try {
|
|
while (inputStream.get(currentCharacter)) {
|
|
// handle next character
|
|
switch (state) {
|
|
case Init:
|
|
switch (currentCharacter) {
|
|
case '\n':
|
|
break;
|
|
case '#':
|
|
state = Comment;
|
|
break;
|
|
case '=':
|
|
whitespace = 0;
|
|
state = Value;
|
|
break;
|
|
case '[':
|
|
sectionName.clear();
|
|
state = SectionName;
|
|
break;
|
|
default:
|
|
addChar(currentCharacter, key, whitespace);
|
|
state = Key;
|
|
}
|
|
break;
|
|
case Key:
|
|
switch (currentCharacter) {
|
|
case '\n':
|
|
finishKeyValue();
|
|
state = Init;
|
|
break;
|
|
case '#':
|
|
finishKeyValue();
|
|
state = Comment;
|
|
break;
|
|
case '=':
|
|
whitespace = 0;
|
|
state = Value;
|
|
break;
|
|
default:
|
|
addChar(currentCharacter, key, whitespace);
|
|
}
|
|
break;
|
|
case Comment:
|
|
switch (currentCharacter) {
|
|
case '\n':
|
|
state = Init;
|
|
break;
|
|
default:;
|
|
}
|
|
break;
|
|
case SectionName:
|
|
switch (currentCharacter) {
|
|
case ']':
|
|
state = Init;
|
|
break;
|
|
default:
|
|
sectionName += currentCharacter;
|
|
}
|
|
break;
|
|
case Value:
|
|
switch (currentCharacter) {
|
|
case '\n':
|
|
finishKeyValue();
|
|
state = Init;
|
|
break;
|
|
case '#':
|
|
finishKeyValue();
|
|
state = Comment;
|
|
break;
|
|
default:
|
|
addChar(currentCharacter, value, whitespace);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} catch (const std::ios_base::failure &) {
|
|
if (!inputStream.eof()) {
|
|
throw;
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
outputStream.exceptions(ios_base::failbit | ios_base::badbit);
|
|
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.
|
|
* \todo
|
|
* Support "line continuation", where a backslash followed immediately by EOL (end-of-line) causes the line break to be ignored,
|
|
* and the "logical line" to be continued on the next actual line from the INI file.
|
|
*/
|
|
|
|
/*!
|
|
* \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
|