Mercurial > code
changeset 325:d52a69f9f029
Add Ini, brand new replacement for Parser
author | David Demelier <markand@malikania.fr> |
---|---|
date | Sat, 28 Feb 2015 18:53:27 +0100 |
parents | 0e80e4589533 |
children | fb6c42173634 |
files | C++/Ini.cpp C++/Ini.h C++/Tests/Ini/CMakeLists.txt C++/Tests/Ini/configs/multi.conf C++/Tests/Ini/configs/simple.conf C++/Tests/Ini/main.cpp CMakeLists.txt |
diffstat | 7 files changed, 812 insertions(+), 1 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/C++/Ini.cpp Sat Feb 28 18:53:27 2015 +0100 @@ -0,0 +1,414 @@ +/* + * Ini.cpp -- .ini file parsing + * + * Copyright (c) 2013, 2014 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <cctype> +#include <iostream> +#include <iterator> +#include <memory> +#include <ostream> +#include <sstream> +#include <vector> + +#include "Ini.h" + +/* -------------------------------------------------------- + * Tokens + * -------------------------------------------------------- */ + +enum class TokenType { + Comment = '#', + SectionBegin = '[', + SectionEnd = ']', + Escape = '\\', + QuoteSimple = '\'', + QuoteDouble = '"', + NewLine = '\n', + Assign = '=', + Include = '@', + Word, + Space +}; + +std::ostream &operator<<(std::ostream &out, const TokenType &type) +{ + switch (type) { + case TokenType::Comment: + out << "Comment"; + break; + case TokenType::SectionBegin: + out << "SectionBegin"; + break; + case TokenType::SectionEnd: + out << "SectionEnd"; + break; + case TokenType::Escape: + out << "Escape"; + break; + case TokenType::QuoteSimple: + out << "QuoteSimple"; + break; + case TokenType::QuoteDouble: + out << "QuoteDouble"; + break; + case TokenType::NewLine: + out << "NewLine"; + break; + case TokenType::Assign: + out << "Assign"; + break; + case TokenType::Include: + out << "Include"; + break; + case TokenType::Word: + out << "Word"; + break; + case TokenType::Space: + out << "Space"; + break; + default: + break; + } + + return out; +} + +class Token { +private: + TokenType m_type; + int m_line; + int m_position; + std::string m_value; + +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)) + { + } + + inline TokenType type() const noexcept + { + return m_type; + } + + inline int line() const noexcept + { + return m_line; + } + + inline int position() const noexcept + { + return m_position; + } + + 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::Space: + return m_value; + case TokenType::Word: + return m_value; + default: + break; + } + + 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::Space: + return "<blank>"; + case TokenType::Word: + return "`" + m_value + "'"; + default: + break; + } + + return ""; + } +}; + +std::ostream &operator<<(std::ostream &out, const Token &token) +{ + out << token.type(); + + if (token.type() == TokenType::Space) + out << ": size = " << token.value().size(); + else if (token.type() == TokenType::Word) + out << ": value = [" << token.value() << "]"; + + return out; +} + +using TokenStack = std::vector<Token>; + +/* -------------------------------------------------------- + * IniBuilder + * -------------------------------------------------------- */ + +class Error : public std::exception { +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) + { + } + + 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::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 '\n': + ++lineno; + position = 0; + case '#': + 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++); + + tokens.push_back({ TokenType::Space, lineno, position, std::move(value) }); + } else { + while (it != end && !std::isspace(*it) && !isReserved(*it)) + value.push_back(*it++); + + tokens.push_back({ TokenType::Word, lineno, position, std::move(value) }); + } + } + + return tokens; + } + + void readComment(TokenStack::iterator &it, TokenStack::iterator end) + { + while (it != end && it->type() != TokenType::NewLine) + ++ it; + + // remove new line + ++ 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; + } + + IniSection readSection(TokenStack::iterator &it, TokenStack::iterator end) + { + if (++it == end || it->type() != TokenType::Word) + throw Error(*it, "word expected after [, got " + it->toString()); + + IniSection section(it->value()); + + if (++it == end || it->type() != TokenType::SectionEnd) + throw Error(*it, "] expected, got " + it->toString()); + + // Remove ] + ++ it; + + if (it == end) + return 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, "unexpected token " + it->toString()); + } + } + + return section; + } + + IniOption readOption(TokenStack::iterator &it, TokenStack::iterator end) + { + std::string key = it->value(); + + if (++it == end) { + throw Error(*it, "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()); + } + + readSpace(it, end); + + if (it == end) { + throw Error(*it, "expected option value after '=', got <EOF>"); + } + + 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, "undisclosed quote: " + save->toString() + " expected"); + + ++ it; + } else if (it->type() == TokenType::Word) { + oss << it++->value(); + } else { + throw Error(*it, "expected option value after '=', got " + it->toString()); + } + + return IniOption(std::move(key), oss.str()); + } + +public: + IniBuilder(Ini &ini, std::istream &stream) + { + std::vector<Token> ts = analyze(stream); + + 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"); + } + } catch (const Error &ex) { + std::cerr << ex.line() << ":" << ex.offset() << ":" << ex.what() << std::endl; + } + } +}; + +/* -------------------------------------------------------- + * Ini + * -------------------------------------------------------- */ + +Ini::Ini(std::istream &stream) +{ + IniBuilder builder(*this, stream); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/C++/Ini.h Sat Feb 28 18:53:27 2015 +0100 @@ -0,0 +1,255 @@ +/* + * Ini.h -- .ini file parsing + * + * Copyright (c) 2013, 2014 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _INI_H_ +#define _INI_H_ + +#include <algorithm> +#include <deque> +#include <fstream> +#include <istream> +#include <stdexcept> +#include <string> + +/** + * @class IniOption + * @brief Option definition + */ +class IniOption { +private: + std::string m_key; + std::string m_value; + +public: + inline IniOption(std::string key, std::string value) + : m_key(std::move(key)) + , m_value(std::move(value)) + { + } + + inline const std::string &key() const noexcept + { + return m_key; + } + + inline const std::string &value() const noexcept + { + return m_value; + } +}; + +/** + * @class IniSection + * @brief Section that contains one or more options + */ +class IniSection { +private: + std::string m_key; + std::deque<IniOption> m_options; + + template <typename T> + T find(const std::string &key) const + { + auto it = std::find_if(m_options.begin(), m_options.end(), [&] (const IniOption &o) { + return o.key() == key; + }); + + if (it == m_options.end()) + throw std::out_of_range("option " + key + " not found"); + + return const_cast<T>(*it); + } + +public: + IniSection() = default; + + inline IniSection(std::string key, std::deque<IniOption> options = {}) + : m_key(std::move(key)) + , m_options(std::move(options)) + { + } + + inline const std::string &key() const noexcept + { + return m_key; + } + + inline auto begin() noexcept + { + return m_options.begin(); + } + + inline auto begin() const noexcept + { + return m_options.begin(); + } + + inline auto cbegin() const noexcept + { + return m_options.cbegin(); + } + + inline auto end() noexcept + { + return m_options.end(); + } + + inline auto end() const noexcept + { + return m_options.end(); + } + + inline auto cend() const noexcept + { + return m_options.cend(); + } + + inline void push_back(IniOption option) + { + m_options.push_back(std::move(option)); + } + + inline void push_front(IniOption option) + { + m_options.push_front(std::move(option)); + } + + inline unsigned size() const noexcept + { + return m_options.size(); + } + + inline IniOption &operator[](int index) noexcept + { + return m_options[index]; + } + + inline const IniOption &operator[](int index) const noexcept + { + return m_options[index]; + } + + inline IniOption &operator[](const std::string &key) + { + return find<IniOption &>(key); + } + + inline const IniOption &operator[](const std::string &key) const + { + return find<const IniOption &>(key); + } +}; + +/** + * @class Ini + * @brief Ini config file loader + */ +class Ini { +private: + std::deque<IniSection> m_sections; + + template <typename T> + T find(const std::string &key) const + { + auto it = std::find_if(m_sections.begin(), m_sections.end(), [&] (const IniSection &s) { + return s.key() == key; + }); + + if (it == m_sections.end()) + throw std::out_of_range("section " + key + " not found"); + + return const_cast<T>(*it); + } + +public: + Ini() = default; + + Ini(std::istream &stream); + + inline Ini(std::istream &&stream) + : Ini(stream) + { + } + + inline auto begin() noexcept + { + return m_sections.begin(); + } + + inline auto begin() const noexcept + { + return m_sections.begin(); + } + + inline auto cbegin() const noexcept + { + return m_sections.cbegin(); + } + + inline auto end() noexcept + { + return m_sections.end(); + } + + inline auto end() const noexcept + { + return m_sections.end(); + } + + inline auto cend() const noexcept + { + return m_sections.cend(); + } + + inline unsigned size() const noexcept + { + return m_sections.size(); + } + + inline void push_back(IniSection section) + { + m_sections.push_back(std::move(section)); + } + + inline void push_front(IniSection section) + { + m_sections.push_front(std::move(section)); + } + + inline IniSection &operator[](int index) noexcept + { + return m_sections[index]; + } + + inline const IniSection &operator[](int index) const noexcept + { + return m_sections[index]; + } + + inline IniSection &operator[](const std::string &key) + { + return find<IniSection &>(key); + } + + inline const IniSection &operator[](const std::string &key) const + { + return find<IniSection &>(key); + } +}; + +#endif // !_INI_H_
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/C++/Tests/Ini/CMakeLists.txt Sat Feb 28 18:53:27 2015 +0100 @@ -0,0 +1,36 @@ +# +# CMakeLists.txt -- tests for Ini +# +# Copyright (c) 2013, 2014 David Demelier <markand@malikania.fr> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +set( + SOURCES + ${code_SOURCE_DIR}/C++/Ini.cpp + ${code_SOURCE_DIR}/C++/Ini.h + main.cpp + configs/simple.conf + configs/multi.conf +) + +define_test(ini "${SOURCES}") + +add_custom_command( + TARGET ini + POST_BUILD + COMMENT "Copying examples files" + COMMAND + ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/configs $<TARGET_FILE_DIR:ini> +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/C++/Tests/Ini/configs/multi.conf Sat Feb 28 18:53:27 2015 +0100 @@ -0,0 +1,7 @@ +[entity] +name = "Player" +version = 1.0 + +[entity] +name = "Subwinner" +version = 2.0 \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/C++/Tests/Ini/configs/simple.conf Sat Feb 28 18:53:27 2015 +0100 @@ -0,0 +1,4 @@ +[general] +option1=1 +option2 =2 +option3 = 3
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/C++/Tests/Ini/main.cpp Sat Feb 28 18:53:27 2015 +0100 @@ -0,0 +1,90 @@ +/* + * main.cpp -- main test file for Ini + * + * Copyright (c) 2013, 2014 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <iostream> + +#include <gtest/gtest.h> + +#include <Ini.h> + +class BasicTest : public testing::Test { +protected: + Ini m_ini; + +public: + BasicTest() + : m_ini(std::ifstream("simple.conf")) + { + } + +}; + +TEST_F(BasicTest, simple) +{ + ASSERT_EQ(1, static_cast<int>(m_ini.size())); +} + +TEST_F(BasicTest, iniOperators) +{ + try { + ASSERT_EQ(3, static_cast<int>(m_ini[0].size())); + ASSERT_EQ("general", m_ini[0].key()); + ASSERT_EQ("general", m_ini["general"].key()); + } catch (const std::exception &ex) { + FAIL() << ex.what(); + } +} + +TEST_F(BasicTest, iniSectionOperators) +{ + try { + // option1=1 (indexes) + ASSERT_EQ("option1", m_ini[0][0].key()); + ASSERT_EQ("1", m_ini[0][0].value()); + + // option1=1 (keys) + ASSERT_EQ("option1", m_ini["general"]["option1"].key()); + ASSERT_EQ("1", m_ini["general"]["option1"].value()); + + // option2 =2 (indexes) + ASSERT_EQ("option2", m_ini[0][1].key()); + ASSERT_EQ("2", m_ini[0][1].value()); + + // option2 =2 (keys) + ASSERT_EQ("option2", m_ini["general"]["option2"].key()); + ASSERT_EQ("2", m_ini["general"]["option2"].value()); + + // option3 = 3 (indexes) + ASSERT_EQ("option3", m_ini[0][2].key()); + ASSERT_EQ("3", m_ini[0][2].value()); + + // option3 = 3 (keys) + ASSERT_EQ("option3", m_ini["general"]["option3"].key()); + ASSERT_EQ("3", m_ini["general"]["option3"].value()); + } catch (const std::exception &ex) { + FAIL() << ex.what(); + } +} + + +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +}
--- a/CMakeLists.txt Fri Feb 27 18:15:33 2015 +0100 +++ b/CMakeLists.txt Sat Feb 28 18:53:27 2015 +0100 @@ -52,10 +52,11 @@ option(WITH_DYNLIB "Enable DynLib tests" On) option(WITH_FLAGS "Enable Flags tests" On) option(WITH_HASH "Enable hash functions tests" On) +option(WITH_INI "Enable .ini parser" On) option(WITH_JSON "Enable Jansson wrapper tests" On) option(WITH_OPTIONPARSER "Enable option parser tests" On) option(WITH_PACK "Enable pack functions" On) -option(WITH_PARSER "Enable parser tests" On) +option(WITH_PARSER "Enable parser tests (deprecated)" On) option(WITH_SOCKETS "Enable sockets tests" On) option(WITH_TREENODE "Enable treenode tests" On) option(WITH_UTF8 "Enable Utf8 functions tests" On) @@ -89,6 +90,10 @@ add_subdirectory(C++/Tests/Json) endif () +if (WITH_INI) + add_subdirectory(C++/Tests/Ini) +endif () + if (WITH_OPTIONPARSER) add_subdirectory(C++/Tests/OptionParser) endif ()