changeset 427:aa9cc55338be

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