Program Listing for File mcfp.hpp

Return to documentation for file (mcfp/mcfp.hpp)

/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2022-2025 Maarten L. hekkelman
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#pragma once


// IWYU pragma: begin_exports
#include "mcfp/error.hpp"
#include "mcfp/options.hpp"
#include "mcfp/sections.hpp"
#include "mcfp/text.hpp"
// IWYU pragma: end_exports

#include <algorithm>
#include <cassert>
#include <cstring>
#include <filesystem>
#include <memory>
#include <optional>
#include <system_error>
#include <type_traits>
#include <utility>
#include <vector>

namespace mcfp
{

// --------------------------------------------------------------------
class config
{
  public:
    void set_usage(std::string usage)
    {
        m_usage = std::move(usage);
    }

    template <typename... Options>
        requires(std::is_base_of_v<option_base, Options> and ...)
    config &init(std::string usage, Options &&...options)
    {
        using std::operator""sv;

        m_sections.clear();

        m_usage = std::move(usage);
        m_ignore_unknown = false;

        add_section("", std::forward<Options>(options)...);

        for (auto &f : get_section_factories())
        {
            std::unique_ptr<section> sp(f->create());

            auto si = std::lower_bound(m_sections.begin(), m_sections.end(), sp->name(),
                [](const std::unique_ptr<section> &s, std::string_view name)
                { return s->name().compare(name) < 0; });

            if (si == m_sections.end())
                m_sections.insert(si, std::move(sp));
        }

        return *this;
    }

    template <typename... Options>
        requires(std::is_base_of_v<option_base, Options> and ...)
    config &add_section(const std::string &section_name, Options &&...options)
    {
        auto si = std::lower_bound(m_sections.begin(), m_sections.end(), section_name,
            [](const std::unique_ptr<section> &s, std::string_view name)
            { return s->name().compare(name) < 0; });

        auto s = std::make_unique<section>(section_name, std::forward<Options>(options)...);

        if (si != m_sections.end())
            *si = std::move(s);
        else
            m_sections.insert(si, std::move(s));

        return *this;
    }

    template <typename... Options>
        requires(std::is_base_of_v<option_base, Options> and ...)
    static void init_lib(std::string section_name, Options &&...options)
    {
        get_section_factories().emplace_back(new section_factory(std::move(section_name), std::forward<Options>(options)...));
    }

    void set_ignore_unknown(bool ignore_unknown)
    {
        m_ignore_unknown = ignore_unknown;
    }

    static config &instance()
    {
        static std::unique_ptr<config> s_instance;
        if (not s_instance)
            s_instance.reset(new config);
        return *s_instance;
    }

    [[nodiscard]] std::string get_last_option() const
    {
        return s_last_option;
    }

    [[nodiscard]] bool has(std::string_view name) const
    {
        auto opt = get_option(name);
        return opt != nullptr and (opt->m_seen > 0 or opt->m_default_value.has_value());
    }

    [[nodiscard]] int count(std::string_view name) const
    {
        auto opt = get_option(name);
        return opt ? opt->m_seen : 0;
    }

    template <typename T>
    [[nodiscard]] auto get(std::string_view name) const
    {
        using return_type = std::remove_cv_t<T>;

        std::error_code ec;
        return_type result = get<T>(name, ec);

        if (ec)
            throw std::system_error(ec, "while getting option '" + std::string{ name } + '\'');

        return result;
    }

    template <typename T>
    auto get(std::string_view name, std::error_code &ec) const
    {
        using return_type = std::remove_cv_t<T>;

        // store name for inspection later on
        s_last_option = name;

        return_type result{};
        auto opt = get_option(name);

        // if opt is null, the programmer has made an error requesting
        // an option that was not specified in the config::init call.
        if (opt == nullptr)
            ec = make_error_code(config_error::unknown_option);
        else
            result = opt->get_value<T>(ec);

        return result;
    }

    [[nodiscard]] std::string get(std::string_view name) const
    {
        return get<std::string>(name);
    }

    template <typename T>
    [[nodiscard]] auto get_optional(std::string_view name) const
    {
        using return_type = std::optional<std::remove_cv_t<T>>;

        std::error_code ec;
        return_type result = get<T>(name, ec);
        if (ec and ec != config_error::option_not_specified)
            throw std::system_error(ec, "while getting option '" + std::string{ name } + '\'');

        return result;
    }

    [[nodiscard]] auto get_optional(std::string_view name) const
    {
        return get_optional<std::string>(name);
    }

    [[nodiscard]] std::string get(std::string_view name, std::error_code &ec) const
    {
        return get<std::string>(name, ec);
    }

    [[nodiscard]] const std::vector<std::string> &operands() const
    {
        return m_operands;
    }

    friend std::ostream &operator<<(std::ostream &os, const config &conf);

    // --------------------------------------------------------------------

    void parse(int argc, const char *const argv[]);

    void parse_config_file(std::string_view config_option, std::string_view config_file_name,
        std::initializer_list<std::string_view> search_dirs);

    void parse_config_file(std::string_view config_option, std::string_view config_file_name,
        std::initializer_list<std::string_view> search_dirs, std::error_code &ec);

    void parse_config_file(const std::filesystem::path &file, std::error_code &ec);

  private:
    static bool is_name_char(int ch)
    {
        return std::isalnum(ch) or ch == '_' or ch == '-';
    }

    static constexpr bool is_eoln(int ch)
    {
        return ch == '\n' or ch == '\r' or ch == std::char_traits<char>::eof();
    }

  public:
    void parse_config_file(std::istream &is, std::error_code &ec);

    void parse(int argc, const char *const argv[], std::error_code &ec);

    // --------------------------------------------------------------------

    constexpr static std::tuple<std::string_view, std::string_view> split_name(std::string_view name) noexcept
    {
        using std::operator""sv;

        auto p = name.find('.');
        return p == std::string_view::npos ? std::make_tuple(""sv, name) : std::make_tuple(name.substr(0, p), name.substr(p + 1));
    }

    // --------------------------------------------------------------------

  public:
    config(const config &) = delete;
    config &operator=(const config &) = delete;

  private:
    config() = default;

    // --------------------------------------------------------------------

    [[nodiscard]] option_base *get_option(std::string_view section_name, std::string_view option_name) const
    {
        option_base *result = nullptr;

        for (auto &s : m_sections)
        {
            if (s->name() != section_name)
                continue;

            result = s->get_option(option_name);
            break;
        }

        return result;
    }

    [[nodiscard]] option_base *get_option(std::string_view name) const
    {
        auto [section_name, option_name] = split_name(name);
        return get_option(section_name, option_name);
    }

    [[nodiscard]] option_base *get_option(char short_name) const
    {
        option_base *result = nullptr;

        for (auto &s : m_sections)
        {
            result = s->get_option(short_name);

            if (result != nullptr)
                break;
        }

        return result;
    }

    [[nodiscard]] size_t get_option_width() const
    {
        size_t result = 0;
        for (auto &s : m_sections)
        {
            auto w = s->get_option_width();
            if (result < w)
                result = w;
        }

        return result;
    }

    // --------------------------------------------------------------------

    class section_factory_base
    {
      public:
        virtual ~section_factory_base() = default;

        [[nodiscard]] virtual section *create() const = 0;
    };

    template <typename... Options>
    class section_factory : public section_factory_base
    {
      public:
        explicit section_factory(std::string name, Options &&...options)
            : m_name(std::move(name))
            , m_options(std::forward<Options>(options)...)
        {
        }

        [[nodiscard]] section *create() const override
        {
            return std::apply([this](Options const &...opts)
                { return new section(m_name, opts...); }, m_options);
        }

        std::string m_name;
        std::tuple<Options...> m_options;
    };

    static std::vector<std::unique_ptr<const section_factory_base>> &get_section_factories()
    {
        static std::vector<std::unique_ptr<const section_factory_base>> s_factories;
        return s_factories;
    }

    // --------------------------------------------------------------------

    bool m_ignore_unknown = false;
    std::string m_usage;

    std::vector<std::string> m_operands;
    std::vector<std::unique_ptr<section>> m_sections;

    static thread_local std::string s_last_option;

};

// --------------------------------------------------------------------

template <typename T = void>
auto make_option(ostring name, std::string description)
    requires(not is_container_type_v<T>)
{
    return option<T>(name.m_long, name.m_short, std::move(description), false);
}

template <typename T>
auto make_option(ostring name, std::string description)
    requires(is_container_type_v<T>)
{
    return multiple_option<T>(name.m_long, name.m_short, std::move(description), false);
}
template <typename T>
auto make_option(ostring name, const T &v, std::string description)
    requires(not is_container_type_v<T>)
{
    return option<T>(name.m_long, name.m_short, v, std::move(description), false);
}

template <typename T = void>
auto make_hidden_option(ostring name, std::string description)
    requires(not is_container_type_v<T>)
{
    return option<T>(name.m_long, name.m_short, description, true);
}

template <typename T>
auto make_hidden_option(ostring name, std::string description)
    requires(is_container_type_v<T>)
{
    return multiple_option<T>(name.m_long, name.m_short, description, true);
}
template <typename T>
auto make_hidden_option(ostring name, const T &v, std::string description)
    requires(not is_container_type_v<T>)
{
    return option<T>(name.m_long, name.m_short, v, description, true);
}

} // namespace mcfp