Mercurial > code
changeset 521:b604d3dd45b7
Ini: resurrection
author | David Demelier <markand@malikania.fr> |
---|---|
date | Wed, 01 Jun 2016 16:44:55 +0200 |
parents | b698e591b43a |
children | adc2dccc3ee6 |
files | CMakeLists.txt modules/ini/CMakeLists.txt modules/ini/doc/mainpage.cpp modules/ini/ini.cpp modules/ini/ini.hpp modules/ini/test/configs/compact.conf modules/ini/test/configs/empty.conf modules/ini/test/configs/error-badcomment.conf modules/ini/test/configs/error-badinclude.conf modules/ini/test/configs/error-badsection.conf modules/ini/test/configs/error-nosection.conf modules/ini/test/configs/error-unterminatedsection.conf modules/ini/test/configs/includes.conf modules/ini/test/configs/lists.conf modules/ini/test/configs/multi.conf modules/ini/test/configs/novalue.conf modules/ini/test/configs/simple.conf modules/ini/test/configs/tokens.conf modules/ini/test/main.cpp |
diffstat | 19 files changed, 1417 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Wed Jun 01 16:42:14 2016 +0200 +++ b/CMakeLists.txt Wed Jun 01 16:44:55 2016 +0200 @@ -46,4 +46,5 @@ add_subdirectory(modules/dynlib) add_subdirectory(modules/fs) add_subdirectory(modules/hash) +add_subdirectory(modules/ini) add_subdirectory(modules/options)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/CMakeLists.txt Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,24 @@ +# +# CMakeLists.txt -- code building for common code +# +# Copyright (c) 2013-2016 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. +# + +code_define_module( + NAME ini + SOURCES ini.cpp ini.hpp + LIBRARIES $<$<BOOL:${WIN32}>:shlwapi> + FLAGS DIRECTORY=\"${CMAKE_CURRENT_SOURCE_DIR}/test/configs/\" +) \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/doc/mainpage.cpp Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,21 @@ +/** + * \mainpage + * + * Welcome to the ini library. + * + * ## Introduction + * + * This module let you parse `.ini` files. It also includes few extensions like: + * + * - include statement, + * - list constructs. + * + * ## Requirements + * + * - C++11, + * - On Windows, you must link against shlwapi library. + * + * ## Installation + * + * Just copy the two files ini.hpp and ini.cpp and add them to your project. + */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/ini.cpp Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,412 @@ +/* + * ini.cpp -- extended .ini file parser + * + * Copyright (c) 2013-2016 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 <cstring> +#include <iostream> +#include <iterator> +#include <fstream> +#include <sstream> +#include <stdexcept> + +// for PathIsRelative. +#if defined(_WIN32) +# include <Shlwapi.h> +#endif + +#include "ini.hpp" + +namespace { + +using namespace ini; + +using StreamIterator = std::istreambuf_iterator<char>; +using TokenIterator = std::vector<Token>::const_iterator; + +inline bool isAbsolute(const std::string &path) noexcept +{ +#if defined(_WIN32) + return !PathIsRelative(path.c_str()); +#else + return path.size() > 0 && path[0] == '/'; +#endif +} + +inline bool isQuote(char c) noexcept +{ + return c == '\'' || c == '"'; +} + +inline bool isSpace(char c) noexcept +{ + /* Custom version because std::isspace includes \n as space */ + return c == ' ' || c == '\t'; +} + +inline bool isList(char c) noexcept +{ + return c == '(' || c == ')' || c == ','; +} + +inline bool isReserved(char c) noexcept +{ + return isList(c) || isQuote(c) || c == '[' || c == ']' || c == '@' || c == '#' || c == '='; +} + +void analyseLine(int &line, int &column, StreamIterator &it) noexcept +{ + assert(*it == '\n'); + + ++ line; + ++ it; + column = 0; +} + +void analyseComment(int &column, StreamIterator &it, StreamIterator end) noexcept +{ + assert(*it == '#'); + + while (it != end && *it != '\n') { + ++ column; + ++ it; + } +} + +void analyseSpaces(int &column, StreamIterator &it, StreamIterator end) noexcept +{ + assert(isSpace(*it)); + + while (it != end && isSpace(*it)) { + ++ column; + ++ it; + } +} + +void analyseList(Tokens &list, int line, int &column, StreamIterator &it) noexcept +{ + assert(isList(*it)); + + switch (*it++) { + case '(': + list.emplace_back(Token::ListBegin, line, column++); + break; + case ')': + list.emplace_back(Token::ListEnd, line, column++); + break; + case ',': + list.emplace_back(Token::Comma, line, column++); + break; + default: + break; + } +} + +void analyseSection(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +{ + assert(*it == '['); + + 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>"); + if (value.empty()) + throw Error(line, column, "empty section name"); + + // Remove ']'. + ++ it; + + list.emplace_back(Token::Section, line, save, std::move(value)); +} + +void analyseAssign(Tokens &list, int &line, int &column, StreamIterator &it) +{ + assert(*it == '='); + + list.push_back({ Token::Assign, line, column++ }); + ++ it; +} + +void analyseQuotedWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator 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 analyseWord(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +{ + assert(!isReserved(*it)); + + std::string value; + int save = column; + + while (it != end && !std::isspace(*it) && !isReserved(*it)) { + ++ column; + value += *it++; + } + + list.push_back({ Token::Word, line, save, std::move(value) }); +} + +void analyseInclude(Tokens &list, int &line, int &column, StreamIterator &it, StreamIterator end) +{ + assert(*it == '@'); + + 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 }); +} + +void parseOptionValueSimple(Option &option, TokenIterator &it) +{ + assert(it->type() == Token::Word || it->type() == Token::QuotedWord); + + option.push_back((it++)->value()); +} + +void parseOptionValueList(Option &option, TokenIterator &it, TokenIterator end) +{ + assert(it->type() == Token::ListBegin); + + TokenIterator save = it++; + + while (it != end && it->type() != Token::ListEnd) { + switch (it->type()) { + case Token::Comma: + // Previous must be a word. + if (it[-1].type() != Token::Word && it[-1].type() != Token::QuotedWord) + throw Error(it->line(), it->column(), "unexpected comma after '" + it[-1].value() + "'"); + + ++ it; + break; + case Token::Word: + case Token::QuotedWord: + option.push_back((it++)->value()); + break; + default: + throw Error(it->line(), it->column(), "unexpected '" + it[-1].value() + "' in list construct"); + break; + } + } + + if (it == end) + throw Error(save->line(), save->column(), "unterminated list construct"); + + // Remove ). + ++ it; +} + +void parseOption(Section &sc, TokenIterator &it, TokenIterator end) +{ + Option option(it->value()); + + TokenIterator save = it; + + // 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()); + + // Empty options are allowed so just test for words. + if (++it != end) { + if (it->type() == Token::Word || it->type() == Token::QuotedWord) + parseOptionValueSimple(option, it); + else if (it->type() == Token::ListBegin) + parseOptionValueList(option, it, end); + } + + sc.push_back(std::move(option)); +} + +void parseInclude(Document &doc, const std::string &path, TokenIterator &it, TokenIterator end) +{ + TokenIterator save = it; + + if (++it == end) + throw Error(save->line(), save->column(), "expected file name after '@include' statement, got <EOF>"); + if (it->type() != Token::Word && it->type() != Token::QuotedWord) + throw Error(it->line(), it->column(), "expected file name after '@include' statement, got " + it->value()); + + std::string value = (it++)->value(); + std::string file; + + if (!isAbsolute(value)) +#if defined(_WIN32) + file = path + "\\" + value; +#else + file = path + "/" + value; +#endif + else + file = value; + + for (const auto &sc : readFile(file)) + 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) { + if (it->type() != Token::Word) + throw Error(it->line(), it->column(), "unexpected token '" + it->value() + "' in section definition"); + + parseOption(sc, it, end); + } + + doc.push_back(std::move(sc)); +} + +} // !namespace + +namespace ini { + +Tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end) +{ + Tokens list; + int line = 1; + int column = 0; + + while (it != end) { + if (*it == '\n') + analyseLine(line, column, it); + else if (*it == '#') + analyseComment(column, it, end); + else if (*it == '[') + analyseSection(list, line, column, it, end); + else if (*it == '=') + analyseAssign(list, line, column, it); + else if (isSpace(*it)) + analyseSpaces(column, it, end); + else if (*it == '@') + analyseInclude(list, line, column, it, end); + else if (isQuote(*it)) + analyseQuotedWord(list, line, column, it, end); + else if (isList(*it)) + analyseList(list, line, column, it); + else + analyseWord(list, line, column, it, end); + } + + return list; +} + +Tokens analyse(std::istream &stream) +{ + return analyse(std::istreambuf_iterator<char>(stream), {}); +} + +Document parse(const Tokens &tokens, const std::string &path) +{ + Document doc; + TokenIterator it = tokens.cbegin(); + TokenIterator end = tokens.cend(); + + while (it != end) { + switch (it->type()) { + case Token::Include: + parseInclude(doc, path, it, end); + break; + case Token::Section: + parseSection(doc, it, end); + break; + default: + throw Error(it->line(), it->column(), "unexpected '" + it->value() + "' on root document"); + } + } + + return doc; +} + +Document readFile(const std::string &filename) +{ + /* Get parent path */ + auto parent = filename; + auto pos = parent.find_last_of("/\\"); + + if (pos != std::string::npos) + parent.erase(pos); + else + parent = "."; + + std::ifstream input(filename); + + if (!input) + throw Error(0, 0, std::strerror(errno)); + + return parse(analyse(input), parent); +} + +Document readString(const std::string &buffer) +{ + std::istringstream iss(buffer); + + return parse(analyse(iss)); +} + +void dump(const Tokens &tokens) +{ + for (const Token &token: tokens) + // TODO: add better description + std::cout << token.line() << ":" << token.column() << ": " << token.value() << std::endl; +} + +} // !ini
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/ini.hpp Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,610 @@ +/* + * ini.hpp -- extended .ini file parser + * + * Copyright (c) 2013-2016 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_HPP +#define INI_HPP + +/** + * \file ini.hpp + * \brief Extended .ini file parser. + * \author David Demelier <markand@malikania.fr> + */ + +/** + * \page Ini Ini + * \brief Extended .ini file parser. + * + * - \subpage ini-syntax + */ + +/** + * \page ini-syntax Syntax + * \brief File syntax. + * + * The syntax is similar to most of `.ini` implementations as: + * + * - a section is delimited by `[name]` can be redefined multiple times, + * - an option **must** always be defined in a section, + * - empty options must be surrounded by quotes, + * - lists can not includes trailing commas, + * - include statement must always be at the beginning of files (in no sections), + * - comments starts with # until the end of line, + * - options with spaces **must** use quotes. + * + * # Basic file + * + * ````ini + * # This is a comment. + * [section] + * option1 = value1 + * option2 = "value 2 with spaces" # comment is also allowed here + * ```` + * + * # Redefinition + * + * Sections can be redefined multiple times and are kept the order they are seen. + * + * ````ini + * [section] + * value = "1" + * + * [section] + * value = "2" + * ```` + * + * The ini::Document object will contains two ini::Section. + * + * # Lists + * + * Lists are defined using `()` and commas, like values, they may have quotes. + * + * ````ini + * [section] + * names = ( "x1", "x2" ) + * + * # This is also allowed + * biglist = ( + * "abc", + * "def" + * ) + * ```` + * + * # Include statement + * + * You can split a file into several pieces, if the include statement contains a relative path, the path will be relative + * to the current file being parsed. + * + * You **must** use the include statement before any section. + * + * If the file contains spaces, use quotes. + * + * ````ini + * # main.conf + * @include "foo.conf" + * + * # foo.conf + * [section] + * option1 = value1 + * ```` + */ + +#include <algorithm> +#include <cassert> +#include <exception> +#include <stdexcept> +#include <string> +#include <vector> + +/** + * Namespace for ini related classes. + */ +namespace ini { + +class Document; + +/** + * \class Error + * \brief Error in a file. + */ +class Error : public std::exception { +private: + int m_line; //!< line number + int m_column; //!< line column + std::string m_message; //!< error message + +public: + /** + * Constructor. + * + * \param line the line + * \param column the column + * \param msg the message + */ + inline Error(int line, int column, std::string msg) noexcept + : m_line(line) + , m_column(column) + , m_message(std::move(msg)) + { + } + + /** + * Get the line number. + * + * \return the line + */ + inline int line() const noexcept + { + return m_line; + } + + /** + * Get the column number. + * + * \return the column + */ + inline int column() const noexcept + { + return m_column; + } + + /** + * Return the raw error message (no line and column shown). + * + * \return the error message + */ + const char *what() const noexcept override + { + 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 analyze + */ +class Token { +public: + /** + * \brief Token type. + */ + enum Type { + Include, //!< include statement + Section, //!< [section] + Word, //!< word without quotes + QuotedWord, //!< word with quotes + Assign, //!< = assignment + ListBegin, //!< begin of list ( + ListEnd, //!< end of list ) + Comma //!< list separation + }; + +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 Assign: + m_value = "="; + break; + case ListBegin: + m_value = "("; + break; + case ListEnd: + m_value = ")"; + break; + case Comma: + m_value = ","; + 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. + */ +class Option : public std::vector<std::string> { +private: + std::string m_key; + +public: + /** + * Construct an empty option. + * + * \pre key must not be empty + * \param key the key + */ + inline Option(std::string key) noexcept + : std::vector<std::string>() + , m_key(std::move(key)) + { + assert(!m_key.empty()); + } + + /** + * Construct a single option. + * + * \pre key must not be empty + * \param key the key + * \param value the value + */ + inline Option(std::string key, std::string value) noexcept + : m_key(std::move(key)) + { + assert(!m_key.empty()); + + push_back(std::move(value)); + } + + /** + * Construct a list option. + * + * \pre key must not be empty + * \param key the key + * \param values the values + */ + inline Option(std::string key, std::vector<std::string> values) noexcept + : std::vector<std::string>(std::move(values)) + , m_key(std::move(key)) + { + assert(!m_key.empty()); + } + + /** + * 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 + { + static std::string dummy; + + return empty() ? dummy : (*this)[0]; + } +}; + +/** + * \class Section + * \brief Section that contains one or more options. + */ +class Section : public std::vector<Option> { +private: + std::string m_key; + +public: + /** + * Construct a section with its name. + * + * \pre key must not be empty + * \param key the key + */ + inline Section(std::string key) noexcept + : m_key(std::move(key)) + { + assert(!m_key.empty()); + } + + /** + * Get the section key. + * + * \return the key + */ + inline const std::string &key() const noexcept + { + return m_key; + } + + /** + * Check if the section contains a specific option. + * + * \param key the option key + * \return true if the option exists + */ + inline bool contains(const std::string &key) const noexcept + { + return find(key) != end(); + } + + /** + * Find an option by key and return an iterator. + * + * \param key the key + * \return the iterator or end() if not found + */ + inline iterator find(const std::string &key) noexcept + { + return std::find_if(begin(), end(), [&] (const auto &o) { + return o.key() == key; + }); + } + + /** + * Find an option by key and return an iterator. + * + * \param key the key + * \return the iterator or end() if not found + */ + inline const_iterator find(const std::string &key) const noexcept + { + return std::find_if(cbegin(), cend(), [&] (const auto &o) { + return o.key() == key; + }); + } + + /** + * Access an option at the specified key. + * + * \param key the key + * \return the option + * \pre contains(key) must return true + */ + inline Option &operator[](const std::string &key) + { + assert(contains(key)); + + return *find(key); + } + + /** + * Overloaded function. + * + * \param key the key + * \return the option + * \pre contains(key) must return true + */ + inline const Option &operator[](const std::string &key) const + { + assert(contains(key)); + + return *find(key); + } + + /** + * Inherited operators. + */ + using std::vector<Option>::operator[]; +}; + +/** + * \class Document + * \brief Ini document description. + * \see readFile + * \see readString + */ +class Document : public std::vector<Section> { +public: + /** + * Check if a document has a specific section. + * + * \param key the key + * \return true if the document contains the section + */ + inline bool contains(const std::string &key) const noexcept + { + return std::find_if(begin(), end(), [&] (const auto &sc) { return sc.key() == key; }) != end(); + } + + /** + * Find a section by key and return an iterator. + * + * \param key the key + * \return the iterator or end() if not found + */ + inline iterator find(const std::string &key) noexcept + { + return std::find_if(begin(), end(), [&] (const auto &o) { + return o.key() == key; + }); + } + + /** + * Find a section by key and return an iterator. + * + * \param key the key + * \return the iterator or end() if not found + */ + inline const_iterator find(const std::string &key) const noexcept + { + return std::find_if(cbegin(), cend(), [&] (const auto &o) { + return o.key() == key; + }); + } + + /** + * Access a section at the specified key. + * + * \param key the key + * \return the section + * \pre contains(key) must return true + */ + inline Section &operator[](const std::string &key) + { + assert(contains(key)); + + return *find(key); + } + + /** + * Overloaded function. + * + * \param key the key + * \return the section + * \pre contains(key) must return true + */ + inline const Section &operator[](const std::string &key) const + { + assert(contains(key)); + + return *find(key); + } + + /** + * Inherited operators. + */ + using std::vector<Section>::operator[]; +}; + +/** + * Analyse a stream and detect potential syntax errors. This does not parse the file like including other + * files in include statement. + * + * It does only analysis, for example if an option is defined under no section, this does not trigger an + * error while it's invalid. + * + * \param it the iterator + * \param end where to stop + * \return the list of tokens + * \throws Error on errors + */ +Tokens analyse(std::istreambuf_iterator<char> it, std::istreambuf_iterator<char> end); + +/** + * Overloaded function for stream. + * + * \param stream the stream + * \return the list of tokens + * \throws Error on errors + */ +Tokens analyse(std::istream &stream); + +/** + * Parse the produced tokens. + * + * \param tokens the tokens + * \param path the parent path + * \return the document + * \throw Error on errors + */ +Document parse(const Tokens &tokens, const std::string &path = "."); + +/** + * Parse a file. + * + * \param filename the file name + * \return the document + * \throw Error on errors + */ +Document readFile(const std::string &filename); + +/** + * Parse a string. + * + * If the string contains include statements, they are relative to the current working directory. + * + * \param buffer the buffer + * \return the document + * \throw Error on errors + */ +Document readString(const std::string &buffer); + +/** + * Show all tokens and their description. + * + * \param tokens the tokens + */ +void dump(const Tokens &tokens); + +} // !ini + +#endif // !INI_HPP
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/compact.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,1 @@ +[general]verbose=true foreground=false[server]host=google.fr
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/empty.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,1 @@ +# this file is completely empty \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/error-badcomment.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,2 @@ +[general] +verbose #hello = xyz
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/error-badinclude.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,1 @@ +@include noquotes \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/error-badsection.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,2 @@ +[[general] +verbose = false
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/error-nosection.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,3 @@ +# this file has no section +# and it's not valid +option = value
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/error-unterminatedsection.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,2 @@ +# This file has unterminated section +[forgot \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/includes.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,5 @@ +# With some includes +@include "simple.conf" # comments also work here + +[standard] +verbose = false
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/lists.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,9 @@ +[rule1] +servers = ( "abc", "bcd" ) + +[rule2] +servers = +( + xyz, + poi +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/multi.conf Wed Jun 01 16:44:55 2016 +0200 @@ -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/modules/ini/test/configs/novalue.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,6 @@ +[plugins] +histedit= "" +highlight= "" #empty +general = "" + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/simple.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,6 @@ +[general] +option1=1 +option2 =2 +option3 = 3 + +# This file ends with a comment. \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/configs/tokens.conf Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,3 @@ +[tokens] +bracket = "I have [brackets]" +at = "I have foo@at"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/ini/test/main.cpp Wed Jun 01 16:44:55 2016 +0200 @@ -0,0 +1,301 @@ +/* + * main.cpp -- main test file for Ini + * + * Copyright (c) 2013-2016 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.hpp> + +class BasicTest : public testing::Test { +protected: + ini::Document m_ini; + +public: + BasicTest() + : m_ini(ini::readFile(DIRECTORY "simple.conf")) + { + } +}; + +TEST_F(BasicTest, simple) +{ + ASSERT_EQ(1, static_cast<int>(m_ini.size())); +} + +TEST_F(BasicTest, operators) +{ + 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, sectionOperators) +{ + 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(); + } +} + +/* + * Reserved tokens in words. + * ------------------------------------------------------------------ + */ + +TEST(Tokens, reserved) +{ + try { + ini::Document doc = ini::readFile(DIRECTORY "tokens.conf"); + + ASSERT_EQ("I have [brackets]", doc["tokens"]["bracket"].value()); + ASSERT_EQ("I have foo@at", doc["tokens"]["at"].value()); + } catch (const std::exception &ex) { + FAIL() << ex.what(); + } +} + +/* + * Multiple definitions. + * ------------------------------------------------------------------ + */ + +class MultiTest : public testing::Test { +protected: + ini::Document m_ini; + +public: + MultiTest() + : m_ini(ini::readFile(DIRECTORY "multi.conf")) + { + } +}; + +TEST_F(MultiTest, defined) +{ + ASSERT_EQ(2, static_cast<int>(m_ini.size())); + ASSERT_EQ("name", m_ini[0]["name"].key()); + ASSERT_EQ("Player", m_ini[0]["name"].value()); + ASSERT_EQ("version", m_ini[0]["version"].key()); + ASSERT_EQ("1.0", m_ini[0]["version"].value()); + ASSERT_EQ("name", m_ini[1]["name"].key()); + ASSERT_EQ("Subwinner", m_ini[1]["name"].value()); + ASSERT_EQ("version", m_ini[1]["version"].key()); + ASSERT_EQ("2.0", m_ini[1]["version"].value()); +} + +/* + * Option with no values. + * ------------------------------------------------------------------ + */ + +class NoValueTest : public testing::Test { +protected: + ini::Document m_ini; + +public: + NoValueTest() + : m_ini(ini::readFile(DIRECTORY "novalue.conf")) + { + } +}; + +TEST_F(NoValueTest, isDefined) +{ + ASSERT_EQ("plugins", m_ini[0].key()); + ASSERT_EQ("", m_ini["plugins"]["histedit"].value()); + ASSERT_EQ("", m_ini["plugins"]["highlight"].value()); + ASSERT_EQ("", m_ini["plugins"]["general"].value()); +} + +/* + * Include tests. + * ------------------------------------------------------------------ + */ + +class IncludeTest : public testing::Test { +protected: + ini::Document m_ini; + +public: + IncludeTest() + : m_ini(ini::readFile(DIRECTORY "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()); +} + +/* + * Compact. + * ------------------------------------------------------------------ + */ + +TEST(Compact, test) +{ + try { + ini::Document doc = ini::readFile(DIRECTORY "compact.conf"); + + ASSERT_EQ(2, static_cast<int>(doc.size())); + ASSERT_EQ("true", doc["general"]["verbose"].value()); + ASSERT_EQ("false", doc["general"]["foreground"].value()); + ASSERT_EQ("google.fr", doc["server"]["host"].value()); + } catch (const std::exception &ex) { + FAIL() << ex.what(); + } +} + +/* + * Empty. + * ------------------------------------------------------------------ + */ + +TEST(Empty, test) +{ + try { + ini::Document doc = ini::readFile(DIRECTORY "empty.conf"); + } catch (const ini::Error &error) { + FAIL() << error.line() << ":" << error.column() << ": " << error.what(); + } +} + +/* + * List. + * ------------------------------------------------------------------ + */ + +TEST(List, test) +{ + try { + std::vector<std::string> rule1{"abc", "bcd"}; + std::vector<std::string> rule2{"xyz", "poi"}; + ini::Document doc = ini::readFile(DIRECTORY "lists.conf"); + + ASSERT_EQ(rule1, doc[0][0]); + ASSERT_EQ(rule2, doc[1][0]); + } catch (const ini::Error &error) { + FAIL() << error.line() << ":" << error.column() << ": " << error.what(); + } +} + +/* + * Errors. + * ------------------------------------------------------------------ + */ + +TEST(Errors, nosection) +{ + // An option outside a section is not allowed. + try { + ini::Document doc = ini::readFile(DIRECTORY "error-nosection.conf"); + + FAIL() << "Failure expected, got success"; + } catch (const ini::Error &ex) { + ASSERT_EQ(3, ex.line()); + ASSERT_EQ(0, ex.column()); + } +} + +TEST(Errors, badcomment) +{ + // Comment can't between option-key and = assigment. + try { + ini::Document doc = ini::readFile(DIRECTORY "error-badcomment.conf"); + + FAIL() << "Failure expected, got success"; + } catch (const ini::Error &ex) { + ASSERT_EQ(2, ex.line()); + ASSERT_EQ(0, ex.column()); + } +} + +TEST(Errors, badsection) +{ + // Bad section naming + try { + ini::Document doc = ini::readFile(DIRECTORY "error-badsection.conf"); + + FAIL() << "Failure expected, got success"; + } catch (const ini::Error &ex) { + ASSERT_EQ(1, ex.line()); + ASSERT_EQ(0, ex.column()); + } +} + +TEST(Errors, unterminatedsection) +{ + // Section unfinished. + try { + ini::Document doc = ini::readFile(DIRECTORY "error-unterminatedsection.conf"); + + FAIL() << "Failure expected, got success"; + } catch (const ini::Error &ex) { + ASSERT_EQ(2, ex.line()); + ASSERT_EQ(6, ex.column()); + } +} + +TEST(Errors, notFound) +{ + ASSERT_ANY_THROW(ini::readFile("does not exists")); +} + +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +}