Program Listing for File mcfp.hpp
↰ Return to documentation for file (mcfp/mcfp.hpp
)
/*-
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2022 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
#include <cassert>
#include <cstring>
#include <algorithm>
#include <any>
#include <charconv>
#include <deque>
#include <filesystem>
#include <fstream>
#include <memory>
#include <optional>
#include <type_traits>
#include <vector>
#include <mcfp/error.hpp>
#include <mcfp/text.hpp>
#include <mcfp/utilities.hpp>
#include <mcfp/detail/options.hpp>
namespace mcfp
{
// --------------------------------------------------------------------
class config
{
using option_base = detail::option_base;
public:
void set_usage(std::string_view usage)
{
m_usage = usage;
}
template <typename... Options>
void init(std::string_view usage, Options... options)
{
m_usage = usage;
m_ignore_unknown = false;
m_impl.reset(new config_impl(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;
}
bool has(std::string_view name) const
{
auto opt = m_impl->get_option(name);
return opt != nullptr and (opt->m_seen > 0 or opt->m_has_default);
}
int count(std::string_view name) const
{
auto opt = m_impl->get_option(name);
return opt ? opt->m_seen : 0;
}
template <typename T>
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, 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>;
return_type result{};
auto opt = m_impl->get_option(name);
if (opt == nullptr)
ec = make_error_code(config_error::unknown_option);
else
{
std::any value = opt->get_value();
if (not value.has_value())
ec = make_error_code(config_error::option_not_specified);
else
{
try
{
result = std::any_cast<T>(value);
}
catch (const std::bad_cast &)
{
ec = make_error_code(config_error::wrong_type_cast);
}
}
}
return result;
}
std::string get(std::string_view name) const
{
return get<std::string>(name);
}
std::string get(std::string_view name, std::error_code &ec) const
{
return get<std::string>(name, ec);
}
const std::vector<std::string> &operands() const
{
return m_impl->m_operands;
}
friend std::ostream &operator<<(std::ostream &os, const config &conf)
{
size_t terminal_width = get_terminal_width();
if (not conf.m_usage.empty())
os << conf.m_usage << std::endl;
size_t options_width = conf.m_impl->get_option_width();
if (options_width > terminal_width / 2)
options_width = terminal_width / 2;
conf.m_impl->write(os, options_width);
return os;
}
// --------------------------------------------------------------------
void parse(int argc, const char *const argv[])
{
std::error_code ec;
parse(argc, argv, ec);
if (ec)
throw std::system_error(ec);
}
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;
parse_config_file(config_option, config_file_name, search_dirs, ec);
if (ec)
throw std::system_error(ec);
}
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)
{
std::string file_name{ config_file_name };
bool parsed_config_file = false;
if (has(config_option))
file_name = get<std::string>(config_option);
for (std::filesystem::path dir : search_dirs)
{
std::ifstream file(dir / file_name);
if (not file.is_open())
continue;
parse_config_file(file, ec);
parsed_config_file = true;
break;
}
if (not parsed_config_file and has(config_option))
ec = make_error_code(config_error::config_file_not_found);
}
void parse_config_file(const std::filesystem::path &file, std::error_code &ec)
{
std::ifstream is(file);
if (is.is_open())
parse_config_file(is, 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)
{
auto &buffer = *is.rdbuf();
enum class State
{
NAME_START,
COMMENT,
NAME,
ASSIGN,
VALUE_START,
VALUE
} state = State::NAME_START;
std::string name, value;
for (;;)
{
auto ch = buffer.sbumpc();
switch (state)
{
case State::NAME_START:
if (is_name_char(ch))
{
name = { static_cast<char>(ch) };
value.clear();
state = State::NAME;
}
else if (ch == '#' or ch == ';')
state = State::COMMENT;
else if (ch != ' ' and ch != '\t' and not is_eoln(ch))
ec = make_error_code(config_error::invalid_config_file);
break;
case State::COMMENT:
if (is_eoln(ch))
state = State::NAME_START;
break;
case State::NAME:
if (is_name_char(ch))
name.insert(name.end(), static_cast<char>(ch));
else if (is_eoln(ch))
{
auto opt = m_impl->get_option(name);
if (opt == nullptr)
{
if (not m_ignore_unknown)
ec = make_error_code(config_error::unknown_option);
}
else if (not opt->m_is_flag)
ec = make_error_code(config_error::missing_argument_for_option);
else
++opt->m_seen;
state = State::NAME_START;
}
else
{
buffer.sungetc();
state = State::ASSIGN;
}
break;
case State::ASSIGN:
if (ch == '=')
state = State::VALUE_START;
else if (is_eoln(ch))
{
auto opt = m_impl->get_option(name);
if (opt == nullptr)
{
if (not m_ignore_unknown)
ec = make_error_code(config_error::unknown_option);
}
else if (not opt->m_is_flag)
ec = make_error_code(config_error::missing_argument_for_option);
else
++opt->m_seen;
state = State::NAME_START;
}
else if (ch != ' ' and ch != '\t')
ec = make_error_code(config_error::invalid_config_file);
break;
case State::VALUE_START:
case State::VALUE:
if (is_eoln(ch))
{
auto opt = m_impl->get_option(name);
if (opt == nullptr)
{
if (not m_ignore_unknown)
ec = make_error_code(config_error::unknown_option);
}
else if (opt->m_is_flag)
ec = make_error_code(config_error::option_does_not_accept_argument);
else if (not value.empty() and (opt->m_seen == 0 or opt->m_multi))
{
opt->set_value(value, ec);
++opt->m_seen;
}
state = State::NAME_START;
}
else if (state == State::VALUE)
value.insert(value.end(), static_cast<char>(ch));
else if (ch != ' ' and ch != '\t')
{
value = { static_cast<char>(ch) };
state = State::VALUE;
}
break;
}
if (ec or ch == std::char_traits<char>::eof())
break;
}
}
void parse(int argc, const char *const argv[], std::error_code &ec)
{
using namespace std::literals;
enum class State
{
options,
operands
} state = State::options;
for (int i = 1; i < argc and not ec; ++i)
{
const char *arg = argv[i];
if (arg == nullptr) // should not happen
break;
if (state == State::options)
{
if (*arg != '-') // according to POSIX this is the end of options, start operands
// state = State::operands;
{ // however, people nowadays expect to be able to mix operands and options
m_impl->m_operands.emplace_back(arg);
continue;
}
else if (arg[1] == '-' and arg[2] == 0)
{
state = State::operands;
continue;
}
}
if (state == State::operands)
{
m_impl->m_operands.emplace_back(arg);
continue;
}
option_base *opt = nullptr;
std::string_view opt_arg;
assert(*arg == '-');
++arg;
if (*arg == '-') // double --, start of new argument
{
++arg;
assert(*arg != 0); // this should not happen, as it was checked for before
std::string_view s_arg(arg);
std::string_view::size_type p = s_arg.find('=');
if (p != std::string_view::npos)
{
opt_arg = s_arg.substr(p + 1);
s_arg = s_arg.substr(0, p);
}
opt = m_impl->get_option(s_arg);
if (opt == nullptr)
{
if (not m_ignore_unknown)
ec = make_error_code(config_error::unknown_option);
continue;
}
if (opt->m_is_flag)
{
if (not opt_arg.empty())
ec = make_error_code(config_error::option_does_not_accept_argument);
++opt->m_seen;
continue;
}
++opt->m_seen;
}
else // single character options
{
bool expect_option_argument = false;
while (*arg != 0 and not ec)
{
opt = m_impl->get_option(*arg++);
if (opt == nullptr)
{
if (not m_ignore_unknown)
ec = make_error_code(config_error::unknown_option);
continue;
}
++opt->m_seen;
if (opt->m_is_flag)
continue;
opt_arg = arg;
expect_option_argument = true;
break;
}
if (not expect_option_argument)
continue;
}
if (opt_arg.empty() and i + 1 < argc) // So, the = character was not present, the next arg must be the option argument
{
++i;
opt_arg = argv[i];
}
if (opt_arg.empty())
ec = make_error_code(config_error::missing_argument_for_option);
else
opt->set_value(opt_arg, ec);
}
}
private:
config() = default;
config(const config &) = delete;
config &operator=(const config &) = delete;
struct config_impl_base
{
virtual ~config_impl_base() = default;
virtual option_base *get_option(std::string_view name) = 0;
virtual option_base *get_option(char short_name) = 0;
virtual size_t get_option_width() const = 0;
virtual void write(std::ostream &os, size_t width) const = 0;
std::vector<std::string> m_operands;
};
template <typename... Options>
struct config_impl : public config_impl_base
{
static constexpr size_t N = sizeof...(Options);
config_impl(Options... options)
: m_options(std::forward<Options>(options)...)
{
}
option_base *get_option(std::string_view name) override
{
return get_option<0>(name);
}
template <size_t Ix>
option_base *get_option([[maybe_unused]] std::string_view name)
{
if constexpr (Ix == N)
return nullptr;
else
{
option_base &opt = std::get<Ix>(m_options);
return (opt.m_name == name) ? &opt : get_option<Ix + 1>(name);
}
}
option_base *get_option(char short_name) override
{
return get_option<0>(short_name);
}
template <size_t Ix>
option_base *get_option([[maybe_unused]] char short_name)
{
if constexpr (Ix == N)
return nullptr;
else
{
option_base &opt = std::get<Ix>(m_options);
return (opt.m_short_name == short_name) ? &opt : get_option<Ix + 1>(short_name);
}
}
virtual size_t get_option_width() const override
{
return std::apply([](Options const& ...opts) {
size_t width = 0;
((width = std::max(width, opts.width())), ...);
return width;
}, m_options);
}
virtual void write(std::ostream &os, size_t width) const override
{
std::apply([&os,width](Options const& ...opts) {
(opts.write(os, width), ...);
}, m_options);
}
std::tuple<Options...> m_options;
};
std::unique_ptr<config_impl_base> m_impl;
bool m_ignore_unknown = false;
std::string m_usage;
};
// --------------------------------------------------------------------
template <typename T = void, std::enable_if_t<not detail::is_container_type_v<T>, int> = 0>
auto make_option(std::string_view name, std::string_view description)
{
return detail::option<T>(name, description, false);
}
template <typename T, std::enable_if_t<detail::is_container_type_v<T>, int> = 0>
auto make_option(std::string_view name, std::string_view description)
{
return detail::multiple_option<T>(name, description, false);
}
template <typename T, std::enable_if_t<not detail::is_container_type_v<T>, int> = 0>
auto make_option(std::string_view name, const T &v, std::string_view description)
{
return detail::option<T>(name, v, description, false);
}
template <typename T = void, std::enable_if_t<not detail::is_container_type_v<T>, int> = 0>
auto make_hidden_option(std::string_view name, std::string_view description)
{
return detail::option<T>(name, description, true);
}
template <typename T, std::enable_if_t<detail::is_container_type_v<T>, int> = 0>
auto make_hidden_option(std::string_view name, std::string_view description)
{
return detail::multiple_option<T>(name, description, true);
}
template <typename T, std::enable_if_t<not detail::is_container_type_v<T>, int> = 0>
auto make_hidden_option(std::string_view name, const T &v, std::string_view description)
{
return detail::option<T>(name, v, description, true);
}
} // namespace mcfp
namespace std
{
template <>
struct is_error_condition_enum<mcfp::config_error>
: public true_type
{
};
} // namespace std