Mercurial > code
changeset 327:78e8f9a3b233
Ini:
- Add support for @include
- Add more tests
- Added lot of documentation
TODO:
- Add lots of test with errored files
author | David Demelier <markand@malikania.fr> |
---|---|
date | Tue, 03 Mar 2015 21:21:11 +0100 |
parents | fb6c42173634 |
children | 02e5ff7b9890 |
files | C++/Ini.cpp C++/Ini.h C++/Tests/Ini/main.cpp |
diffstat | 3 files changed, 410 insertions(+), 81 deletions(-) [+] |
line wrap: on
line diff
--- a/C++/Ini.cpp Tue Mar 03 19:43:23 2015 +0100 +++ b/C++/Ini.cpp Tue Mar 03 21:21:11 2015 +0100 @@ -17,6 +17,8 @@ */ #include <cctype> +#include <cerrno> +#include <cstring> #include <iostream> #include <iterator> #include <memory> @@ -24,6 +26,10 @@ #include <sstream> #include <vector> +#if defined(_WIN32) +# include <Shlwapi.h> // for PathIsRelative +#endif + #include "Ini.h" namespace { @@ -185,10 +191,11 @@ { out << token.type(); - if (token.type() == TokenType::Space) + if (token.type() == TokenType::Space) { out << ": size = " << token.value().size(); - else if (token.type() == TokenType::Word) + } else if (token.type() == TokenType::Word) { out << ": value = [" << token.value() << "]"; + } return out; } @@ -199,43 +206,43 @@ * IniBuilder * -------------------------------------------------------- */ -class Error : public std::exception { +class IniBuilder { private: - int m_line; - int m_offset; - std::string m_error; - -public: - inline Error(const Token &token, std::string error) - : m_line(token.line()) - , m_offset(token.position()) - , m_error(error) - { - } + std::string m_path; + std::string m_base; + Ini &m_ini; - int line() const noexcept - { - return m_line; - } - - int offset() const noexcept - { - return m_offset; - } - - const char *what() const noexcept - { - return m_error.c_str(); - } -}; - -class IniBuilder { 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); @@ -270,13 +277,15 @@ } } } else if (std::isspace(*it)) { - while (it != end && std::isspace(*it) && *it != '\n') + while (it != end && std::isspace(*it) && *it != '\n') { value.push_back(*it++); + } tokens.push_back({ TokenType::Space, lineno, position, std::move(value) }); } else { - while (it != end && !std::isspace(*it) && !isReserved(*it)) + while (it != end && !std::isspace(*it) && !isReserved(*it)) { value.push_back(*it++); + } tokens.push_back({ TokenType::Word, lineno, position, std::move(value) }); } @@ -287,8 +296,9 @@ void readComment(TokenStack::iterator &it, TokenStack::iterator end) { - while (it != end && it->type() != TokenType::NewLine) + while (it != end && it->type() != TokenType::NewLine) { ++ it; + } // remove new line ++ it; @@ -296,43 +306,48 @@ void readSpace(TokenStack::iterator &it, TokenStack::iterator end) { - while (it != end && it->type() == TokenType::Space) + while (it != end && it->type() == TokenType::Space) { ++ it; + } } void readNewLine(TokenStack::iterator &it, TokenStack::iterator end) { - while (it != end && it->type() == TokenType::NewLine) + while (it != end && it->type() == TokenType::NewLine) { ++ it; + } } IniSection readSection(TokenStack::iterator &it, TokenStack::iterator end) { - if (++it == end || it->type() != TokenType::Word) - throw Error(*it, "word expected after [, got " + it->toString()); + if (++it == end || it->type() != TokenType::Word) { + throw IniError(it->line(), it->position(), "word expected after [, got " + it->toString()); + } IniSection section(it->value()); - if (++it == end || it->type() != TokenType::SectionEnd) - throw Error(*it, "] expected, got " + it->toString()); + if (++it == end || it->type() != TokenType::SectionEnd) { + throw IniError(it->line(), it->position(), "] expected, got " + it->toString()); + } // Remove ] ++ it; - if (it == end) + if (it == end) { return section; + } while (it != end && it->type() != TokenType::SectionBegin) { - if (it->type() == TokenType::Space) + if (it->type() == TokenType::Space) { readSpace(it, end); - else if (it->type() == TokenType::NewLine) + } else if (it->type() == TokenType::NewLine) { readNewLine(it, end); - else if (it->type() == TokenType::Comment) + } else if (it->type() == TokenType::Comment) { readComment(it, end); - else if (it->type() == TokenType::Word) + } else if (it->type() == TokenType::Word) { section.push_back(readOption(it, end)); - else { - throw Error(*it, "unexpected token " + it->toString()); + } else { + throw IniError(it->line(), it->position(), "unexpected token " + it->toString()); } } @@ -344,13 +359,13 @@ std::string key = it->value(); if (++it == end) { - throw Error(*it, "expected '=' after option declaration, got <EOF>"); + throw IniError(it->line(), it->position(), "expected '=' after option declaration, got <EOF>"); } readSpace(it, end); if (it == end || it++->type() != TokenType::Assign) { - throw Error(*it, "expected '=' after option declaration, got " + it->toString()); + throw IniError(it->line(), it->position(), "expected '=' after option declaration, got " + it->toString()); } readSpace(it, end); @@ -365,43 +380,90 @@ } if (it == end) - throw Error(*save, "undisclosed quote: " + save->toString() + " expected"); + throw IniError(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, "expected option value after '=', got " + it->toString()); + throw IniError(it->line(), it->position(), "expected option value after '=', got " + it->toString()); } return IniOption(std::move(key), oss.str()); } + void readInclude(TokenStack::iterator &it, TokenStack::iterator end) + { + if (++it == end || (it->type() != TokenType::Word || it->value() != "include")) { + throw IniError(it->line(), it->position(), "expected `include' after '@' token, got " + it->toString()); + } + + readSpace(++it, end); + + // Quotes mandatory + TokenStack::iterator save = it; + + if (it == end || (it->type() != TokenType::QuoteSimple && it->type() != TokenType::QuoteDouble)) { + throw IniError(it->line(), it->position(), "expected filename after @include statement"); + } + + // Filename + if (++it == end || it->type() != TokenType::Word) { + throw IniError(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(); + } + + // Must be closed with the same quote + if (++it == end || it->type() != save->type()) { + throw IniError(save->line(), save->position(), "undiclosed quote: " + save->toString() + " expected"); + } + + // Remove quote + ++ it; + + IniBuilder(m_ini, fullpath); + } + public: - IniBuilder(Ini &ini, std::istream &stream) + IniBuilder(Ini &ini, std::string path) + : m_path(path) + , m_base(base(std::move(path))) + , m_ini(ini) { - std::vector<Token> ts = analyze(stream); + 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(); - try { - 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::SectionBegin) - ini.push_back(readSection(it, end)); - else - throw Error(*it, "unexpected " + it->toString() + " on root document"); + 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 IniError(it->line(), it->position(), "unexpected " + it->toString() + " on root document"); } - } catch (const Error &ex) { - std::cerr << ex.line() << ":" << ex.offset() << ":" << ex.what() << std::endl; } } }; @@ -412,7 +474,7 @@ * Ini * -------------------------------------------------------- */ -Ini::Ini(std::istream &stream) +Ini::Ini(const std::string &path) { - IniBuilder builder(*this, stream); + IniBuilder(*this, path); }
--- a/C++/Ini.h Tue Mar 03 19:43:23 2015 +0100 +++ b/C++/Ini.h Tue Mar 03 21:21:11 2015 +0100 @@ -19,14 +19,73 @@ #ifndef _INI_H_ #define _INI_H_ +/** + * @file Ini.h + * @brief Configuration file parser + */ + #include <algorithm> #include <deque> -#include <fstream> -#include <istream> #include <stdexcept> #include <string> /** + * @class IniError + * @brief Error in a file + */ +class IniError : public std::exception { +private: + int m_line; + int m_position; + std::string m_error; + +public: + /** + * Construct an error. + * + * @param line the line + * @param position the position + * @param error the error + */ + inline IniError(int line, int position, std::string error) + : m_line(line) + , m_position(position) + , m_error(std::move(error)) + { + } + + /** + * Return the line number. + * + * @return the line + */ + inline int line() const noexcept + { + return m_line; + } + + /** + * Return the position in the current line. + * + * @return the position + */ + inline int position() const noexcept + { + return m_position; + } + + /** + * Get the error string. + * + * @return the string + */ + inline const char *what() const noexcept + { + return m_error.c_str(); + } +}; + +/** * @class IniOption * @brief Option definition */ @@ -36,17 +95,33 @@ std::string m_value; public: + /** + * Construct an option. + * + * @param key the key + * @param value the value + */ inline IniOption(std::string key, std::string value) : m_key(std::move(key)) , m_value(std::move(value)) { } + /** + * Get the option key. + * + * @return the key + */ inline const std::string &key() const noexcept { return m_key; } + /** + * Get the option value. + * + * @return the value + */ inline const std::string &value() const noexcept { return m_value; @@ -76,79 +151,166 @@ } public: + /** + * Default constructor has no sections and no values. + */ IniSection() = default; - inline IniSection(std::string key, std::deque<IniOption> options = {}) + /** + * Construct a section with a set of options. + * + * @param key the section name + * @param options the list of options + */ + inline IniSection(std::string key, std::deque<IniOption> options = {}) noexcept : m_key(std::move(key)) , m_options(std::move(options)) { } + /** + * Get the section key. + * + * @return the key + */ inline const std::string &key() const noexcept { return m_key; } + /** + * Get an iterator to the beginning. + * + * @return the iterator + */ inline auto begin() noexcept { return m_options.begin(); } + /** + * Overloaded function. + * + * @return the iterator + */ inline auto begin() const noexcept { return m_options.begin(); } + /** + * Overloaded function. + * + * @return the iterator + */ 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 { return m_options.cend(); } + /** + * Append an option. + * + * @param option the option to add + */ inline void push_back(IniOption option) { m_options.push_back(std::move(option)); } + /** + * Push an option to the beginning. + * + * @param option the option to add + */ inline void push_front(IniOption 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 IniOption &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 IniOption &operator[](int index) const noexcept { return m_options[index]; } + /** + * Access an option at the specified key. + * + * @param key the key + * @return the option + * @warning No bounds checking is performed + */ inline IniOption &operator[](const std::string &key) { return find<IniOption &>(key); } + /** + * Access an option at the specified key. + * + * @param key the key + * @return the option + * @warning No bounds checking is performed + */ inline const IniOption &operator[](const std::string &key) const { return find<const IniOption &>(key); @@ -177,75 +339,152 @@ } public: + /** + * Default constructor with an empty configuration. + */ Ini() = default; - Ini(std::istream &stream); + /** + * Open the path as the configuration file. + * + * @param path the path + * @throw IniError on any error + */ + Ini(const std::string &path); - inline Ini(std::istream &&stream) - : Ini(stream) - { - } - + /** + * Get an iterator to the beginning. + * + * @return the iterator + */ inline auto begin() noexcept { return m_sections.begin(); } + /** + * Overloaded function. + * + * @return the iterator + */ inline auto begin() const noexcept { return m_sections.begin(); } + /** + * Overloaded function. + * + * @return the iterator + */ inline auto cbegin() const noexcept { return m_sections.cbegin(); } + /** + * Get an iterator to the end. + * + * @return the iterator + */ inline auto end() noexcept { return m_sections.end(); } + /** + * Overloaded function. + * + * @return the iterator + */ inline auto end() const noexcept { return m_sections.end(); } + /** + * Overloaded function. + * + * @return the iterator + */ 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(IniSection section) { m_sections.push_back(std::move(section)); } + /** + * Add a section to the beginning. + * + * @param section the section to add + */ inline void push_front(IniSection 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 IniSection &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 IniSection &operator[](int index) const noexcept { return m_sections[index]; } + /** + * Access a section at the specified key. + * + * @param key the key + * @return the section + * @warning No bounds checking is performed + */ inline IniSection &operator[](const std::string &key) { return find<IniSection &>(key); } + /** + * Access a section at the specified key. + * + * @param key the key + * @return the section + * @warning No bounds checking is performed + */ inline const IniSection &operator[](const std::string &key) const { return find<IniSection &>(key);
--- a/C++/Tests/Ini/main.cpp Tue Mar 03 19:43:23 2015 +0100 +++ b/C++/Tests/Ini/main.cpp Tue Mar 03 21:21:11 2015 +0100 @@ -28,7 +28,7 @@ public: BasicTest() - : m_ini(std::ifstream("simple.conf")) + : m_ini("simple.conf") { } @@ -91,7 +91,7 @@ public: MultiTest() - : m_ini(std::ifstream("multi.conf")) + : m_ini("multi.conf") { } }; @@ -119,7 +119,7 @@ public: NoValueTest() - : m_ini(std::ifstream("novalue.conf")) + : m_ini("novalue.conf") { } }; @@ -132,6 +132,34 @@ ASSERT_EQ("", m_ini["plugins"]["general"].value()); } +/* -------------------------------------------------------- + * Include tests + * -------------------------------------------------------- */ + +class IncludeTest : public testing::Test { +protected: + Ini m_ini; + +public: + IncludeTest() + : m_ini("includes.conf") + { + } +}; + +TEST_F(IncludeTest, all) +{ + ASSERT_EQ(2, static_cast<int>(m_ini.size())); + + // from include + ASSERT_EQ("1", m_ini[0][0].value()); + ASSERT_EQ("2", m_ini[0][1].value()); + ASSERT_EQ("3", m_ini[0][2].value()); + + // from standard + ASSERT_EQ("false", m_ini[1][0].value()); +} + int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv);