changeset 327:78e8f9a3b233

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