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();
+}