Mercurial > code
changeset 427:aa9cc55338be
Ini: rewrite the parse/analyze process
author | David Demelier <markand@malikania.fr> |
---|---|
date | Wed, 14 Oct 2015 15:08:45 +0200 |
parents | cee5c74c1c83 |
children | c2b02b5f32a9 |
files | C++/modules/Ini/Ini.cpp C++/modules/Ini/Ini.h C++/tests/Ini/main.cpp |
diffstat | 3 files changed, 610 insertions(+), 687 deletions(-) [+] |
line wrap: on
line diff
--- a/C++/modules/Ini/Ini.cpp Wed Oct 14 10:04:24 2015 +0200 +++ b/C++/modules/Ini/Ini.cpp Wed Oct 14 15:08:45 2015 +0200 @@ -17,15 +17,11 @@ */ #include <cctype> -#include <cerrno> -#include <cstring> -#include <fstream> #include <iostream> #include <iterator> -#include <memory> -#include <ostream> +#include <fstream> #include <sstream> -#include <vector> +#include <stdexcept> #if defined(_WIN32) # include <Shlwapi.h> // for PathIsRelative @@ -33,430 +29,371 @@ #include "Ini.h" -namespace ini { - namespace { -/* -------------------------------------------------------- - * Tokens - * -------------------------------------------------------- */ +using namespace ini; + +using Iterator = std::istreambuf_iterator<char>; +using TokenIterator = std::vector<Token>::const_iterator; + +#if defined(_WIN32) +inline bool isAbsolute(const std::string &path) noexcept +{ + return !PathIsRelative(path.c_str()); +} +#else +inline bool isAbsolute(const std::string &path) noexcept +{ + return path.size() > 0 && path[0] == '/'; +} +#endif + +inline bool isQuote(char c) noexcept +{ + return c == '\'' || c == '"'; +} -enum class TokenType { - Comment = '#', - SectionBegin = '[', - SectionEnd = ']', - Escape = '\\', - QuoteSimple = '\'', - QuoteDouble = '"', - NewLine = '\n', - Assign = '=', - Include = '@', - Word, - Space -}; +inline bool isSpace(char c) noexcept +{ + /* Custom version because std::isspace includes \n as space */ + return c == ' ' || c == '\t'; +} + +inline bool isReserved(char c) noexcept +{ + return c == '[' || c == ']' || c == '@' || c == '#' || c == '=' || c == '\'' || c == '"'; +} -class Token { -private: - TokenType m_type; - int m_line; - int m_position; - std::string m_value; +void analyzeLine(Tokens &list, int &line, int &column, Iterator &it) +{ + list.push_back({ Token::Line, line++, column }); + ++ it; + column = 0; +} -public: - inline Token(TokenType type, int line, int position, std::string value = "") - : m_type(type) - , m_line(line) - , m_position(position) - , m_value(std::move(value)) - { +void analyzeComment(Tokens &list, int &line, int &column, Iterator &it, Iterator end) +{ + std::string value{1, *it}; + int save = column; + + while (it != end && *it != '\n') { + ++ column; + value += *it++; } - inline TokenType type() const noexcept - { - return m_type; + list.push_back({ Token::Comment, line, save, std::move(value) }); +} + +void analyzeSection(Tokens &list, int &line, int &column, Iterator &it, Iterator end) +{ + std::string value; + int save = column; + + /* Read section name */ + ++ it; + while (it != end && *it != ']') { + if (*it == '\n') { + throw Error{line, column, "section not terminated, missing ']'"}; + } + if (isReserved(*it)) { + throw Error{line, column, "section name expected after '[', got '" + std::string(1, *it) + "'"}; + } + ++ column; + value += *it++; + } + + if (it == end) { + throw Error{line, column, "section name expected after '[', got <EOF>"}; } - inline int line() const noexcept - { - return m_line; + /* Remove ']' */ + ++ it; + + list.push_back({ Token::Section, line, save, std::move(value) }); +} + +void analyzeAssign(Tokens &list, int &line, int &column, Iterator &it) +{ + list.push_back({ Token::Assign, line, column++ }); + ++ it; +} + +void analyzeSpaces(Tokens &list, int &line, int &column, Iterator &it, Iterator end) +{ + std::string value; + int save = column; + + while (it != end && (*it == ' ' || *it == '\t')) { + ++ column; + value += *it++; } - inline int position() const noexcept - { - return m_position; + list.push_back({ Token::Space, line, save, std::move(value) }); +} + +void analyzeQuotedWord(Tokens &list, int &line, int &column, Iterator &it, Iterator end) +{ + std::string value; + int save = column; + char quote = *it++; + + while (it != end && *it != quote) { + // TODO: escape sequence + ++ column; + value += *it++; + } + + if (it == end) { + throw Error{line, column, "undisclosed '" + std::string(1, quote) + "', got <EOF>"}; + } + + /* Remove quote */ + ++ it; + + list.push_back({ Token::QuotedWord, line, save, std::move(value) }); +} + +void analyzeWord(Tokens &list, int &line, int &column, Iterator &it, Iterator end) +{ + std::string value; + int save = column; + + while (it != end && !std::isspace(*it) && !isReserved(*it)) { + ++ column; + value += *it++; } - inline std::string value() const - { - switch (m_type) { - case TokenType::Comment: - return "#"; - case TokenType::SectionBegin: - return "["; - case TokenType::SectionEnd: - return "]"; - case TokenType::QuoteSimple: - return "'"; - case TokenType::QuoteDouble: - return "\""; - case TokenType::NewLine: - return "\n"; - case TokenType::Assign: - return "="; - case TokenType::Include: - return "@"; - case TokenType::Space: - return m_value; - case TokenType::Word: - return m_value; - default: - break; + list.push_back({ Token::Word, line, save, std::move(value) }); +} + +void analyzeInclude(Tokens &list, int &line, int &column, Iterator &it, Iterator end) +{ + std::string include; + int save = column; + + /* Read include */ + ++ it; + while (it != end && !isSpace(*it)) { + ++ column; + include += *it++; + } + + if (include != "include") { + throw Error{line, column, "expected include after '@' token"}; + } + + list.push_back({ Token::Include, line, save }); +} + +Tokens analyze(std::istreambuf_iterator<char> &it, std::istreambuf_iterator<char> end) +{ + Tokens list; + int line = 1; + int column = 0; + + while (it != end) { + if (*it == '\n') { + analyzeLine(list, line, column, it); + } else if (*it == '#') { + analyzeComment(list, line, column, it, end); + } else if (*it == '[') { + analyzeSection(list, line, column, it, end); + } else if (*it == '=') { + analyzeAssign(list, line, column, it); + } else if (isSpace(*it)) { + analyzeSpaces(list, line, column, it, end); + } else if (*it == '@') { + analyzeInclude(list, line, column, it, end); + } else if (isQuote(*it)) { + analyzeQuotedWord(list, line, column, it, end); + } else { + analyzeWord(list, line, column, it, end); } - - return ""; } - inline std::string toString() const - { - switch (m_type) { - case TokenType::Comment: - return "'#'"; - case TokenType::SectionBegin: - return "'['"; - case TokenType::SectionEnd: - return "']'"; - case TokenType::QuoteSimple: - return "'"; - case TokenType::QuoteDouble: - return "\""; - case TokenType::NewLine: - return "<newline>"; - case TokenType::Assign: - return "="; - case TokenType::Include: - return "@"; - case TokenType::Space: - return "<blank>"; - case TokenType::Word: - return "`" + m_value + "'"; + return list; +} + +void parseSpaces(TokenIterator &it, TokenIterator end) +{ + while (it != end && it->type() == Token::Space) { + ++ it; + } +} + +void parseOption(Section &sc, TokenIterator &it, TokenIterator end) +{ + std::string key = it->value(); + std::string value; + + TokenIterator save = it; + + /* Optional spaces before '=' */ + parseSpaces(++it, end); + + /* No '=' or something else? */ + if (it == end) { + throw Error{save->line(), save->column(), "expected '=' assignment, got <EOF>"}; + } + if (it->type() != Token::Assign) { + throw Error{it->line(), it->column(), "expected '=' assignment, got " + it->value()}; + } + + /* Optional spaces after '=' */ + parseSpaces(++it, end); + + /* Empty options are allowed so just test for words */ + if (it != end) { + if (it->type() == Token::Word || it->type() == Token::QuotedWord) { + value = it++->value(); + } + } + + sc.emplace_back(std::move(key), std::move(value)); +} + +void parseInclude(Document &doc, TokenIterator &it, TokenIterator end) +{ + TokenIterator save = it; + + if (++it == end) { + throw Error{save->line(), save->column(), "expected file name after '@include' statement, got <EOF>"}; + } + + /* Get file name */ + parseSpaces(it, end); + + if (it->type() != Token::Word && it->type() != Token::QuotedWord) { + throw Error{it->line(), it->column(), "expected file name after '@include' statement, got " + it->value()}; + } + + if (doc.path().empty()) { + throw Error{it->line(), it->column(), "'@include' statement invalid with buffer documents"}; + } + + std::string value = (it++)->value(); + std::string file; + + if (!isAbsolute(value)) { +#if defined(_WIN32) + file = doc.path() + "\\" + value; +#else + file = doc.path() + "/" + value; +#endif + } else { + file = value; + } + + Document child{File{file}}; + + for (const auto &sc : child) { + doc.push_back(sc); + } +} + +void parseSection(Document &doc, TokenIterator &it, TokenIterator end) +{ + Section sc{it->value()}; + + /* Skip [section] */ + ++ it; + + /* Read until next section */ + while (it != end && it->type() != Token::Section) { + switch (it->type()) { + case Token::Line: + case Token::Comment: + case Token::Space: + it ++; + continue; default: break; } - return ""; - } -}; - -using TokenStack = std::vector<Token>; - -/* -------------------------------------------------------- - * Builder - * -------------------------------------------------------- */ - -class Builder { -private: - std::string m_path; - std::string m_base; - Document &m_ini; - -private: - inline bool isReserved(char c) const noexcept - { - return c == '\n' || c == '#' || c == '"' || c == '\'' || c == '=' || c == '[' || c == ']' || c == '@'; - } - - std::string base(std::string path) - { - auto pos = path.find_last_of("/\\"); - - if (pos != std::string::npos) { - path.erase(pos); - } else { - path = "."; - } - - return path; - } - -#if defined(_WIN32) - bool isAbsolute(const std::string &path) - { - return !PathIsRelative(path.c_str()); - } -#else - bool isAbsolute(const std::string &path) - { - return path.size() > 0 && path[0] == '/'; - } -#endif - - std::vector<Token> analyze(std::istream &stream) const - { - std::istreambuf_iterator<char> it(stream); - std::istreambuf_iterator<char> end; - std::vector<Token> tokens; - - int lineno{1}; - int position{0}; - - while (it != end) { - std::string value; - - if (isReserved(*it)) { - while (it != end && isReserved(*it)) { - // Single character tokens - switch (*it) { - case '#': - /* Skip comments */ - while (it != end && *it != '\n') { - ++ it; - } - tokens.push_back({ TokenType::Comment, lineno, position }); - position = 0; - break; - case '\n': - ++lineno; - position = 0; - case '[': - case ']': - case '\'': - case '"': - case '=': - case '@': - tokens.push_back({ static_cast<TokenType>(*it), lineno, position }); - ++it; - ++position; - default: - break; - } - } - } else if (std::isspace(*it)) { - while (it != end && std::isspace(*it) && *it != '\n') { - value.push_back(*it++); - ++position; - } - - tokens.push_back({ TokenType::Space, lineno, position, std::move(value) }); - } else { - while (it != end && !std::isspace(*it) && !isReserved(*it)) { - value.push_back(*it++); - ++position; - } - - tokens.push_back({ TokenType::Word, lineno, position, std::move(value) }); - } - } - - return tokens; - } - - void readComment(TokenStack::iterator &it, TokenStack::iterator) - { - ++ it; - } - - void readSpace(TokenStack::iterator &it, TokenStack::iterator end) - { - while (it != end && it->type() == TokenType::Space) { - ++ it; - } - } - - void readNewLine(TokenStack::iterator &it, TokenStack::iterator end) - { - while (it != end && it->type() == TokenType::NewLine) { - ++ it; - } - } - - Section readSection(TokenStack::iterator &it, TokenStack::iterator end) - { - // Empty [ - if (++it == end) { - throw Error{it[-1].line(), it[-1].position(), "section declaration expected, got <EOF>"}; - } - - // Get the section name - if (it->type() != TokenType::Word) { - throw Error{it->line(), it->position(), "word expected after [, got " + it->toString()}; - } - - Section section(it->value()); - - // [unterminated - if (++it == end) { - throw Error{it[-1].line(), it[-1].position(), "unterminated section"}; - } - - // Check if terminated - if (it->type() != TokenType::SectionEnd) { - throw Error{it->line(), it->position(), "] expected, got " + it->toString()}; + if (it->type() != Token::Word) { + throw Error{it->line(), it->column(), "unexpected token '" + it->value() + "' in section definition"}; } - // Remove ] - ++ it; - - if (it == end) { - return section; - } - - // Now read all that is allowed to be in a section - while (it != end && it->type() != TokenType::SectionBegin) { - if (it->type() == TokenType::Space) { - readSpace(it, end); - } else if (it->type() == TokenType::NewLine) { - readNewLine(it, end); - } else if (it->type() == TokenType::Comment) { - readComment(it, end); - } else if (it->type() == TokenType::Word) { - section.push_back(readOption(it, end)); - } else { - throw Error{it->line(), it->position(), "unexpected token " + it->toString()}; - } - } - - return section; - } - - Option readOption(TokenStack::iterator &it, TokenStack::iterator end) - { - std::string key = it++->value(); - - readSpace(it, end); - - if (it == end) { - throw Error{it[-1].line(), it[-1].position(), "expected '=' after option declaration, got <EOF>"}; - } - - if (it->type() != TokenType::Assign) { - ++ it; - throw Error{it[-1].line(), it[-1].position(), "expected '=' after option declaration, got " + it[-1].toString()}; - } - - readSpace(++it, end); - - std::ostringstream oss; - - if (it->type() == TokenType::QuoteSimple || it->type() == TokenType::QuoteDouble) { - TokenStack::iterator save = it++; - - while (it != end && it->type() != save->type()) { - oss << it++->value(); - } - - if (it == end) { - throw Error{save->line(), save->position(), "undisclosed quote: " + save->toString() + " expected"}; - } - - ++ it; - } else if (it->type() == TokenType::Word) { - oss << it++->value(); - } else if (it->type() != TokenType::NewLine && it->type() != TokenType::Comment) { - // No value requested, must be NewLine or comment - throw Error{it->line(), it->position(), "expected option value after '=', got " + it->toString()}; - } - - return Option{std::move(key), oss.str()}; + parseOption(sc, it, end); } - void readInclude(TokenStack::iterator &it, TokenStack::iterator end) - { - if (++it == end) { - throw Error{it[-1].line(), it[-1].position(), "expected `include` after '@' token, got <EOF>"}; - } - - if (it->type() != TokenType::Word && it->value() != "include") { - throw Error{it->line(), it->position(), "expected `include' after '@' token, got " + it->toString()}; - } - - readSpace(++it, end); - - TokenStack::iterator save = it; - - if (it == end) { - throw Error{it[-1].line(), it[-1].position(), "expected filename after @include statement, got <EOF>"}; - } + doc.push_back(std::move(sc)); +} - // First quote - if (it->type() != TokenType::QuoteSimple && it->type() != TokenType::QuoteDouble) { - throw Error{it->line(), it->position(), "expected filename after @include statement"}; - } - - // Filename - if (++it == end) { - throw Error{it[-1].line(), it[-1].position(), "expected filename after @include statement, got <EOF>"}; - } - - if (it->type() != TokenType::Word) { - throw Error{it->line(), it->position(), "expected filename after @include statement"}; - } - - std::string value = it->value(); - std::string fullpath; - - if (isAbsolute(value)) { - fullpath = value; - } else { - fullpath = m_base + "/" + it->value(); - } +void parse(Document &doc, const Tokens &tokens) +{ + TokenIterator it = tokens.cbegin(); + TokenIterator end = tokens.cend(); - // Must be closed with the same quote - if (++it == end) { - throw Error{save->line(), save->position(), "undiclosed quote: " + save->toString() + " expected, got <EOF>"}; - } - if (it->type() != save->type()) { - throw Error{save->line(), save->position(), "undiclosed quote: " + save->toString() + " expected"}; - } - - // Remove quote - ++ it; - - Builder(m_ini, fullpath); - } - -public: - Builder(Document &ini, std::string path) - : m_path(path) - , m_base(base(std::move(path))) - , m_ini(ini) - { - std::ifstream file(m_path); - - if (!file.is_open()) - throw std::runtime_error(std::strerror(errno)); - - std::vector<Token> ts = analyze(file); - - auto it = ts.begin(); - auto end = ts.end(); - - while (it != end) { - if (it->type() == TokenType::Space) { - readSpace(it, end); - } else if (it->type() == TokenType::NewLine) { - readNewLine(it, end); - } else if (it->type() == TokenType::Comment) { - readComment(it, end); - } else if (it->type() == TokenType::Include) { - readInclude(it, end); - } else if (it->type() == TokenType::SectionBegin) { - m_ini.push_back(readSection(it, end)); - } else { - throw Error(it->line(), it->position(), "unexpected " + it->toString() + " on root document"); - } + while (it != end) { + /* Just ignore this */ + switch (it->type()) { + case Token::Include: + parseInclude(doc, it, end); + break; + case Token::Section: + parseSection(doc, it, end); + break; + case Token::Comment: + case Token::Line: + case Token::Space: + ++ it; + break; + default: + throw Error{it->line(), it->column(), "unexpected '" + it->value() + "' on root document"}; } } -}; +} } // !namespace -/* -------------------------------------------------------- - * Document - * -------------------------------------------------------- */ +namespace ini { + +Tokens Document::analyze(const File &file) +{ + std::fstream stream{file.path}; + std::istreambuf_iterator<char> it{stream}; + std::istreambuf_iterator<char> end{}; + + return ::analyze(it, end); +} + +Tokens Document::analyze(const Buffer &buffer) +{ + std::istringstream stream{buffer.text}; + std::istreambuf_iterator<char> it{stream}; + std::istreambuf_iterator<char> end{}; + + return ::analyze(it, end); +} -Document::Document(const std::string &path) +Document::Document(const File &file) + : m_path{file.path} { - Builder(*this, path); + /* Update path */ + auto pos = m_path.find_last_of("/\\"); + + if (pos != std::string::npos) { + m_path.erase(pos); + } else { + m_path = "."; + } + + parse(*this, analyze(file)); +} + +Document::Document(const Buffer &buffer) +{ + dump(analyze(buffer)); + parse(*this, analyze(buffer)); +} + +void Document::dump(const Tokens &tokens) +{ + for (const Token &token: tokens) { + // TODO: type + std::cout << token.line() << ":" << token.column() << ": " << token.value() << std::endl; + } } } // !ini \ No newline at end of file
--- a/C++/modules/Ini/Ini.h Wed Oct 14 10:04:24 2015 +0200 +++ b/C++/modules/Ini/Ini.h Wed Oct 14 15:08:45 2015 +0200 @@ -21,43 +21,45 @@ /** * @file Ini.h - * @brief Configuration file parser + * @brief Configuration file parser. */ #include <algorithm> -#include <deque> -#include <stdexcept> +#include <exception> #include <string> +#include <vector> namespace ini { +class Document; + /** * @class Error * @brief Error in a file */ class Error : public std::exception { private: - int m_line; - int m_position; - std::string m_error; + int m_line; //!< line number + int m_column; //!< line column + std::string m_message; //!< error message public: /** - * Construct an error. + * Constructor. * - * @param line the line - * @param position the position - * @param error the error + * @param l the line + * @param c the column + * @param m the message */ - inline Error(int line, int position, std::string error) - : m_line(line) - , m_position(position) - , m_error(std::move(error)) + inline Error(int l, int c, std::string m) noexcept + : m_line{l} + , m_column{c} + , m_message{std::move(m)} { } /** - * Return the line number. + * Get the line number. * * @return the line */ @@ -67,29 +69,146 @@ } /** - * Return the position in the current line. + * Get the column number. * - * @return the position + * @return the column */ - inline int position() const noexcept + inline int column() const noexcept { - return m_position; + return m_column; } /** - * Get the error string. + * Return the raw error message (no line and column shown). * - * @return the string + * @return the error message */ - inline const char *what() const noexcept + const char *what() const noexcept override { - return m_error.c_str(); + return m_message.c_str(); } }; /** + * @class Token + * @brief Describe a token read in the .ini source + * + * This class can be used when you want to parse a .ini file yourself. + * + * @see Document::analyze + */ +class Token { +public: + /** + * @brief Token type + */ + enum Type { + Include, //!< include statement + Section, //!< [section] + Word, //!< word without quotes + QuotedWord, //!< word with quotes + Comment, //!< # comment like this + Assign, //!< = assignment + Space, //!< space or tabs + Line //!< '\n' + }; + +private: + Type m_type; + int m_line; + int m_column; + std::string m_value; + +public: + /** + * Construct a token. + * + * @param type the type + * @param line the line + * @param column the column + * @param value the value + */ + Token(Type type, int line, int column, std::string value = "") noexcept + : m_type{type} + , m_line{line} + , m_column{column} + { + switch (type) { + case Include: + m_value = "@include"; + break; + case Section: + case Word: + case QuotedWord: + m_value = value; + break; + case Comment: + m_value = "comment"; + break; + case Assign: + m_value = "="; + break; + case Line: + m_value = "<newline>"; + break; + case Space: + m_value = "<space>"; + break; + default: + break; + } + } + + /** + * Get the type. + * + * @return the type + */ + inline Type type() const noexcept + { + return m_type; + } + + /** + * Get the line. + * + * @return the line + */ + inline int line() const noexcept + { + return m_line; + } + + /** + * Get the column. + * + * @return the column + */ + inline int column() const noexcept + { + return m_column; + } + + /** + * Get the value. For words, quoted words and section, the value is the content. Otherwise it's the + * characters parsed. + * + * @return the value + */ + inline const std::string &value() const noexcept + { + return m_value; + } +}; + +/** + * List of tokens in order they are analyzed. + */ +using Tokens = std::vector<Token>; + +/** * @class Option - * @brief Option definition + * @brief Option definition. */ class Option { private: @@ -103,9 +222,9 @@ * @param key the key * @param value the value */ - inline Option(std::string key, std::string value) - : m_key(std::move(key)) - , m_value(std::move(value)) + inline Option(std::string key, std::string value) noexcept + : m_key{std::move(key)} + , m_value{std::move(value)} { } @@ -132,41 +251,34 @@ /** * @class Section - * @brief Section that contains one or more options + * @brief Section that contains one or more options. */ -class Section { +class Section : public std::vector<Option> { private: std::string m_key; - std::deque<Option> m_options; template <typename T> T find(const std::string &key) const { - auto it = std::find_if(m_options.begin(), m_options.end(), [&] (const Option &o) { + auto it = std::find_if(begin(), end(), [&] (const Option &o) { return o.key() == key; }); - if (it == m_options.end()) - throw std::out_of_range("option " + key + " not found"); + if (it == end()) { + throw std::out_of_range{"option " + key + " not found"}; + } return const_cast<T>(*it); } public: /** - * Default constructor has no sections and no values. - */ - Section() = default; - - /** - * Construct a section with a set of options. + * Construct a section with its name. * - * @param key the section name - * @param options the list of options + * @param key the key */ - inline Section(std::string key, std::deque<Option> options = {}) noexcept - : m_key(std::move(key)) - , m_options(std::move(options)) + inline Section(std::string key) noexcept + : m_key{std::move(key)} { } @@ -181,117 +293,14 @@ } /** - * Get an iterator to the beginning. - * - * @return the iterator - */ - inline auto begin() noexcept - { - return m_options.begin(); - } - - /** - * Overloaded function. + * Check if the section contains a specific option. * - * @return the iterator - */ - inline auto begin() const noexcept - { - return m_options.begin(); - } - - /** - * Overloaded function. - * - * @return the iterator + * @param key the option key + * @return true if the option exists */ - inline auto cbegin() const noexcept - { - return m_options.cbegin(); - } - - /** - * Get an iterator to the end. - * - * @return the iterator - */ - inline auto end() noexcept - { - return m_options.end(); - } - - /** - * Overloaded function. - * - * @return the iterator - */ - inline auto end() const noexcept - { - return m_options.end(); - } - - /** - * Overloaded function. - * - * @return the iterator - */ - inline auto cend() const noexcept + inline bool contains(const std::string &key) const noexcept { - return m_options.cend(); - } - - /** - * Append an option. - * - * @param option the option to add - */ - inline void push_back(Option option) - { - m_options.push_back(std::move(option)); - } - - /** - * Push an option to the beginning. - * - * @param option the option to add - */ - inline void push_front(Option option) - { - m_options.push_front(std::move(option)); - } - - /** - * Get the number of options in that section. - * - * @return the size - */ - inline unsigned size() const noexcept - { - return m_options.size(); - } - - /** - * Access an option at the specified index. - * - * @param index the index - * @return the option - * @warning No bounds checking is performed - */ - inline Option &operator[](int index) noexcept - { - return m_options[index]; - } - - /** - * Access an option at the specified index. - * - * @param index the index - * @return the option - * @warning No bounds checking is performed - */ - inline const Option &operator[](int index) const noexcept - { - return m_options[index]; + return std::find_if(begin(), end(), [&] (const auto &opt) { return opt.key() == key; }) != end(); } /** @@ -317,155 +326,140 @@ { return find<const Option &>(key); } + + /** + * Inherited operators. + */ + using std::vector<Option>::operator[]; +}; + +/** + * @class File + * @brief Source for reading .ini files. + */ +class File { +public: + /** + * Path to the file. + */ + std::string path; + + /** + * Load the file into the document. + * + * @param doc the document + * @throw Error on errors + */ + void load(Document &doc); +}; + +/** + * @class Buffer + * @brief Source for reading ini from text. + * @note the include statement is not supported with buffers. + */ +class Buffer { +public: + /** + * The ini content. + */ + std::string text; + + /** + * Load the file into the document. + * + * @param doc the document + * @throw Error on errors + */ + void load(Document &doc); }; /** * @class Document * @brief Ini config file loader */ -class Document { +class Document : public std::vector<Section> { private: - std::deque<Section> m_sections; + std::string m_path; template <typename T> T find(const std::string &key) const { - auto it = std::find_if(m_sections.begin(), m_sections.end(), [&] (const Section &s) { + auto it = std::find_if(begin(), end(), [&] (const Section &s) { return s.key() == key; }); - if (it == m_sections.end()) - throw std::out_of_range("section " + key + " not found"); + if (it == end()) { + throw std::out_of_range{"section " + key + " not found"}; + } return const_cast<T>(*it); } public: /** - * Default constructor with an empty configuration. + * Analyze a file and extract tokens. If the function succeeds, that does not mean the content is valid, + * it just means that there are no syntax error. + * + * For example, this class does not allow adding options under no sections and this function will not + * detect that issue. + * + * @param file the file to read + * @return the list of tokens + * @throws Error on errors */ - Document() = default; + static Tokens analyze(const File &file); /** - * Open the path as the configuration file. + * Overloaded function for buffers. * - * @param path the path - * @throw Error on any error + * @param buffer the buffer to read + * @return the list of tokens + * @throws Error on errors */ - Document(const std::string &path); - - /** - * Get an iterator to the beginning. - * - * @return the iterator - */ - inline auto begin() noexcept - { - return m_sections.begin(); - } + static Tokens analyze(const Buffer &buffer); /** - * Overloaded function. + * Show all tokens and their description. * - * @return the iterator + * @param tokens the tokens */ - inline auto begin() const noexcept - { - return m_sections.begin(); - } + static void dump(const Tokens &tokens); + + /** + * Construct a document from a file. + * + * @param file the file to read + * @throws Error on errors + */ + Document(const File &file); /** - * Overloaded function. + * Overloaded constructor for buffers. * - * @return the iterator + * @param buffer the buffer to read + * @throws Error on errors */ - inline auto cbegin() const noexcept - { - return m_sections.cbegin(); - } + Document(const Buffer &buffer); /** - * Get an iterator to the end. + * Get the current document path, only useful when constructed from File source. * - * @return the iterator + * @return the path */ - inline auto end() noexcept + inline const std::string &path() const noexcept { - return m_sections.end(); + return m_path; } /** - * Overloaded function. + * Check if a document has a specific section. * - * @return the iterator - */ - inline auto end() const noexcept - { - return m_sections.end(); - } - - /** - * Overloaded function. - * - * @return the iterator + * @param key the key */ - inline auto cend() const noexcept - { - return m_sections.cend(); - } - - /** - * Get the number of sections in the configuration. - * - * @return the size - */ - inline unsigned size() const noexcept - { - return m_sections.size(); - } - - /** - * Append a section to the end. - * - * @param section the section to add - */ - inline void push_back(Section section) + inline bool contains(const std::string &key) const noexcept { - m_sections.push_back(std::move(section)); - } - - /** - * Add a section to the beginning. - * - * @param section the section to add - */ - inline void push_front(Section section) - { - m_sections.push_front(std::move(section)); - } - - /** - * Access a section at the specified index. - * - * @param index the index - * @return the section - * @warning No bounds checking is performed - */ - inline Section &operator[](int index) noexcept - { - return m_sections[index]; - } - - /** - * Access a section at the specified index. - * - * @param index the index - * @return the section - * @warning No bounds checking is performed - */ - inline const Section &operator[](int index) const noexcept - { - return m_sections[index]; + return std::find_if(begin(), end(), [&] (const auto &sc) { return sc.key() == key; }) != end(); } /** @@ -491,8 +485,13 @@ { return find<Section &>(key); } + + /** + * Inherited operators. + */ + using std::vector<Section>::operator[]; }; } // !ini -#endif // !_INI_H_ +#endif // !_INI_H_ \ No newline at end of file
--- a/C++/tests/Ini/main.cpp Wed Oct 14 10:04:24 2015 +0200 +++ b/C++/tests/Ini/main.cpp Wed Oct 14 15:08:45 2015 +0200 @@ -28,7 +28,7 @@ public: BasicTest() - : m_ini("Ini/simple.conf") + : m_ini{ini::File{"Ini/simple.conf"}} { } @@ -88,7 +88,7 @@ TEST(Tokens, reserved) { try { - ini::Document doc("Ini/tokens.conf"); + ini::Document doc{ini::File{"Ini/tokens.conf"}}; ASSERT_EQ("I have [brackets]", doc["tokens"]["bracket"].value()); ASSERT_EQ("I have foo@at", doc["tokens"]["at"].value()); @@ -107,7 +107,7 @@ public: MultiTest() - : m_ini("Ini/multi.conf") + : m_ini{ini::File{"Ini/multi.conf"}} { } }; @@ -135,7 +135,7 @@ public: NoValueTest() - : m_ini("Ini/novalue.conf") + : m_ini{ini::File{"Ini/novalue.conf"}} { } }; @@ -158,7 +158,7 @@ public: IncludeTest() - : m_ini("Ini/includes.conf") + : m_ini{ini::File{"Ini/includes.conf"}} { } }; @@ -183,7 +183,7 @@ TEST(Compact, test) { try { - ini::Document doc("Ini/compact.conf"); + ini::Document doc{ini::File{"Ini/compact.conf"}}; ASSERT_EQ(2, static_cast<int>(doc.size())); ASSERT_EQ("true", doc["general"]["verbose"].value()); @@ -201,9 +201,9 @@ TEST(Empty, test) { try { - ini::Document doc{"Ini/empty.conf"}; + ini::Document doc{ini::File{"Ini/empty.conf"}}; } catch (const ini::Error &error) { - FAIL() << error.line() << ":" << error.position() << ": " << error.what(); + FAIL() << error.line() << ":" << error.column() << ": " << error.what(); } } @@ -215,12 +215,12 @@ { // An option outside a section is not allowed try { - ini::Document doc("Ini/error-nosection.conf"); + ini::Document doc{ini::File{"Ini/error-nosection.conf"}}; FAIL() << "Failure expected, got success"; } catch (const ini::Error &ex) { ASSERT_EQ(3, ex.line()); - ASSERT_EQ(7, ex.position()); + ASSERT_EQ(0, ex.column()); } } @@ -228,12 +228,12 @@ { // The = assignment must be on the same line as the option key try { - ini::Document doc("Ini/error-lineassigment.conf"); + ini::Document doc{ini::File{"Ini/error-lineassigment.conf"}}; FAIL() << "Failure expected, got success"; } catch (const ini::Error &ex) { - ASSERT_EQ(3, ex.line()); - ASSERT_EQ(0, ex.position()); + ASSERT_EQ(2, ex.line()); + ASSERT_EQ(4, ex.column()); } } @@ -241,12 +241,12 @@ { // Comment can't between option-key and = assigment try { - ini::Document doc("Ini/error-badcomment.conf"); + ini::Document doc{ini::File{"Ini/error-badcomment.conf"}}; FAIL() << "Failure expected, got success"; } catch (const ini::Error &ex) { ASSERT_EQ(2, ex.line()); - ASSERT_EQ(9, ex.position()); + ASSERT_EQ(8, ex.column()); } } @@ -254,12 +254,12 @@ { // Bad section naming try { - ini::Document doc("Ini/error-badsection.conf"); + ini::Document doc{ini::File{"Ini/error-badsection.conf"}}; FAIL() << "Failure expected, got success"; } catch (const ini::Error &ex) { ASSERT_EQ(1, ex.line()); - ASSERT_EQ(1, ex.position()); + ASSERT_EQ(0, ex.column()); } } @@ -267,25 +267,12 @@ { // Section unfinished try { - ini::Document doc("Ini/error-unterminatedsection.conf"); + ini::Document doc{ini::File{"Ini/error-unterminatedsection.conf"}}; FAIL() << "Failure expected, got success"; } catch (const ini::Error &ex) { ASSERT_EQ(2, ex.line()); - ASSERT_EQ(8, ex.position()); - } -} - -TEST(Errors, badinclude) -{ - // Section unfinished - try { - ini::Document doc("Ini/error-badinclude.conf"); - - FAIL() << "Failure expected, got success"; - } catch (const ini::Error &ex) { - ASSERT_EQ(1, ex.line()); - ASSERT_EQ(17, ex.position()); + ASSERT_EQ(6, ex.column()); } }