view modules/ini/ini.hpp @ 580:6082ef1fa880

Ini: reduce column limit
author David Demelier <markand@malikania.fr>
date Wed, 20 Jul 2016 16:33:15 +0200
parents 8012eb3ef579
children
line wrap: on
line source

/*
 * 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.
 *
 * ## Export macros
 *
 * You must define `INI_DLL` globally and `INI_BUILDING_DLL` when compiling the
 * library if you want a DLL, alternatively you can provide your own
 * `INI_EXPORT` macro instead.
 *
 *   - \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
 * ````
 */

/**
 * \cond INI_HIDDEN_SYMBOLS
 */

#if !defined(INI_EXPORT)
#   if defined(INI_DLL)
#       if defined(_WIN32)
#           if defined(INI_BUILDING_DLL)
#               define INI_EXPORT __declspec(dllexport)
#           else
#               define INI_EXPORT __declspec(dllimport)
#           endif
#       else
#           define INI_EXPORT
#       endif
#   else
#       define INI_EXPORT
#   endif
#endif

/**
 * \endcond
 */

#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 find(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
 */
INI_EXPORT 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
 */
INI_EXPORT 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
 */
INI_EXPORT Document parse(const Tokens &tokens, const std::string &path = ".");

/**
 * Parse a file.
 *
 * \param filename the file name
 * \return the document
 * \throw Error on errors
 */
INI_EXPORT 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
 */
INI_EXPORT Document readString(const std::string &buffer);

/**
 * Show all tokens and their description.
 *
 * \param tokens the tokens
 */
INI_EXPORT void dump(const Tokens &tokens);

} // !ini

#endif // !INI_HPP