Program Listing for File options.hpp
↰ Return to documentation for file (mcfp/options.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
#include "mcfp/charconv.hpp"
#include "mcfp/error.hpp"
#include "mcfp/text.hpp"
#include <cassert>
#include <charconv>
#include <cstdio>
#include <filesystem>
#include <optional>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
namespace mcfp
{
// --------------------------------------------------------------------
// Some template wizardry to detect containers, needed to have special
// handling of options that can be repeated.
template <typename T>
using iterator_t = typename T::iterator;
template <typename T>
using value_type_t = typename T::value_type;
template <typename T>
using std_string_npos_t = decltype(T::npos);
template <typename T, typename = void>
struct is_container_type : std::false_type
{
};
template <typename T>
requires(is_detected_v<value_type_t, T> and
is_detected_v<iterator_t, T> and
not is_detected_v<std_string_npos_t, T>)
struct is_container_type<T>
: std::true_type
{
};
template <typename T>
inline constexpr bool is_container_type_v = is_container_type<T>::value;
static_assert(is_container_type_v<std::vector<int>>);
static_assert(is_container_type_v<std::vector<std::string>>);
// --------------------------------------------------------------------
// Some helper classes, to allow compile time checking of options strings
// This error reporting function is not constexpr and thus when it is
// called by the checking the format of options strings, it will cause
// a compile time error.
[[noreturn]] inline void report_error(const char *msg)
{
(void)fputs(msg, stderr);
exit(1);
}
// --------------------------------------------------------------------
template <typename CharT>
class string_view_base
{
public:
using char_type = CharT;
using value_type = char_type;
using iterator = const char_type *;
constexpr explicit string_view_base(const char *s) noexcept
: m_data(s)
{
while (m_data[m_size] != 0)
++m_size;
}
constexpr string_view_base(const char *s, size_t N) noexcept
: m_data(s)
, m_size(N)
{
}
template <size_t N>
constexpr explicit string_view_base(const char (&s)[N]) noexcept
: m_data(s)
, m_size(N - 1)
{
}
constexpr string_view_base() noexcept = default;
constexpr string_view_base(const string_view_base &) noexcept = default;
constexpr string_view_base &
operator=(const string_view_base &) noexcept = default;
// constexpr string_view_base(nullptr_t) = delete;
template <typename StringType>
// requires(std::is_same_v<typename StringType::value_type, value_type>)
constexpr explicit string_view_base(const StringType &s) noexcept
: m_data(s.data())
, m_size(s.size())
{
}
[[nodiscard]] constexpr const char_type *data() const noexcept { return m_data; }
[[nodiscard]] constexpr size_t size() const noexcept { return m_size; }
[[nodiscard]] constexpr iterator begin() const noexcept { return m_data; }
[[nodiscard]] constexpr iterator end() const noexcept { return m_data + m_size; }
[[nodiscard]] constexpr char_type operator[](size_t ix) const noexcept
{
return m_data[ix];
}
[[nodiscard]] constexpr char_type front() const noexcept { return m_data[0]; }
[[nodiscard]] constexpr char_type back() const noexcept { return m_data[m_size - 1]; }
[[nodiscard]] constexpr string_view_base substr(size_t pos, size_t len) const noexcept
{
return { m_data + pos, len };
}
private:
const char_type *m_data = nullptr;
size_t m_size = 0;
};
using string_view = string_view_base<char>;
// --------------------------------------------------------------------
// A parsed options string, that is, split out the short and long names
struct ostring
{
string_view m_str;
string_view m_long;
string_view m_short;
template <size_t N>
consteval inline ostring(const char (&s)[N]) // NOLINT(hicpp-explicit-conversions)
: m_str(s, N - 1)
{
parse();
}
constexpr void parse();
};
constexpr inline bool is_alnum(int ch) noexcept
{
return (ch >= '0' and ch <= '9') or (ch >= 'a' and ch <= 'z') or
(ch >= 'A' and ch <= 'Z');
}
constexpr inline bool is_valid_option_char(char ch) noexcept
{
return ch == '-' or ch == '_' or is_alnum(ch);
}
constexpr void ostring::parse()
{
if (m_str.size() < 1)
report_error("Empty string is not allowed for an option");
if (m_str.front() == '-')
report_error("Option strings should not start with a hyphen");
auto len = m_str.size();
if (m_str.size() == 1)
{
if (not is_alnum(m_str.front()))
report_error("Single character options should be alnum");
m_long = m_short = m_str;
}
else
{
if (m_str.size() > 2 and m_str[m_str.size() - 2] == ',')
{
if (not is_alnum(m_str.back()))
report_error("Short variant of option should be alnum");
m_short = m_str.substr(m_str.size() - 1, 1);
len -= 2;
}
for (size_t ix = 0; auto ch : m_str)
{
if (ix++ == len)
break;
if (not is_valid_option_char(ch))
report_error("Short variant of option should be alnum");
}
m_long = m_str.substr(0, len);
}
}
// --------------------------------------------------------------------
// The options classes
// The option traits classes are used to convert from the string-based
// command line argument to the type that should be stored.
// In fact, here is where the command line arguments are checked for
// proper formatting.
template <typename T, typename = void>
struct option_traits;
template <typename T>
requires(std::is_arithmetic_v<T>)
struct option_traits<T>
{
using value_type = T;
static value_type set_value(std::string_view argument, std::error_code &ec)
{
value_type value{};
auto r =
from_chars(argument.data(), argument.data() + argument.length(), value);
if (r.ec != std::errc())
ec = std::make_error_code(r.ec);
else if (*r.ptr != 0)
ec = std::make_error_code(std::errc::invalid_argument);
return value;
}
static std::string to_string(const T &value)
{
char b[32];
auto r = std::to_chars(b, b + sizeof(b), value);
if (r.ec != std::errc())
throw std::system_error(std::make_error_code(r.ec));
return { b, r.ptr };
}
};
template <>
struct option_traits<std::filesystem::path>
{
using value_type = std::filesystem::path;
static value_type set_value(std::string_view argument,
std::error_code & /*ec*/)
{
return value_type{ argument };
}
static std::string to_string(const std::filesystem::path &value)
{
return value.string();
}
};
template <typename T>
requires(not std::is_arithmetic_v<T> and std::is_assignable_v<std::string, T>)
struct option_traits<T>
{
using value_type = std::string;
static value_type set_value(std::string_view argument,
std::error_code & /*ec*/)
{
return value_type{ argument };
}
static std::string to_string(const T &value) { return { value }; }
};
// The Options. The reason to have this weird constructing of
// polymorphic options based on templates is to have a very
// simple interface. The disadvantage is that the options have
// to be copied during the construction of the config object.
struct option_base
{
std::string m_name;
std::string m_desc;
char m_short_name;
bool m_is_flag = true,
m_multi = false,
m_hidden;
int m_seen = 0;
// We store the actual data in the argument list, i.e. strings
std::vector<std::string> m_value;
std::optional<std::string> m_default_value;
option_base(const option_base &rhs) = default;
constexpr option_base(string_view name_long, string_view name_short,
std::string desc, bool hidden)
: m_name(name_long.begin(), name_long.end())
, m_desc(std::move(desc))
, m_short_name(name_short.size() > 0 ? name_short.front() : 0)
, m_hidden(hidden)
{
}
virtual ~option_base() = default;
virtual void set_value(std::string_view /*value*/,
std::error_code & /*ec*/) = 0;
template <typename T>
T get_value(std::error_code &ec) const
{
T result{};
if (m_value.empty())
{
if (m_default_value)
{
if constexpr (is_container_type_v<T>)
result.emplace_back(option_traits<typename T::value_type>::set_value(
*m_default_value, ec));
else
result = option_traits<T>::set_value(*m_default_value, ec);
}
else if constexpr (not is_container_type_v<T>) // Return an empty list if not specified
ec = make_error_code(config_error::option_not_specified);
}
else
{
if constexpr (is_container_type_v<T>)
{
for (auto &a : m_value)
{
result.emplace_back(
option_traits<typename T::value_type>::set_value(a, ec));
if (ec)
{
result.clear();
break;
}
}
}
else
result = option_traits<T>::set_value(m_value.front(), ec);
}
return result;
}
[[nodiscard]] size_t width(std::string_view section_name) const;
void write(std::ostream &os, std::string_view section_name, size_t indent, size_t output_width) const;
};
template <typename T>
struct option : public option_base
{
using traits_type = option_traits<T>;
using value_type = typename option_traits<T>::value_type;
option(const option &rhs) = default;
option(string_view name_long, string_view name_short, std::string desc,
bool hidden)
: option_base(name_long, name_short, std::move(desc), hidden)
{
m_is_flag = false;
}
option(string_view name_long, string_view name_short,
const value_type &default_value, std::string desc, bool hidden)
: option(name_long, name_short, std::move(desc), hidden)
{
if constexpr (std::is_same_v<value_type, std::string>)
m_default_value = default_value;
else
m_default_value = traits_type::to_string(default_value);
}
void set_value(std::string_view argument, std::error_code &ec) override
{
traits_type::set_value(argument, ec);
if (not ec)
{
m_value.clear();
m_value.emplace_back(argument);
}
}
};
template <typename T>
struct multiple_option : public option_base
{
using value_type = typename T::value_type;
using traits_type = option_traits<value_type>;
multiple_option(const multiple_option &rhs) = default;
multiple_option(string_view name_long, string_view name_short,
std::string desc, bool hidden)
: option_base(name_long, name_short, std::move(desc), hidden)
{
m_is_flag = false;
m_multi = true;
}
void set_value(std::string_view argument, std::error_code &ec) override
{
traits_type::set_value(argument, ec);
if (not ec)
m_value.emplace_back(argument);
}
};
template <>
struct option<void> : public option_base
{
option(const option &rhs) = default;
option(string_view name_long, string_view name_short, std::string desc,
bool hidden)
: option_base(name_long, name_short, std::move(desc), hidden)
{
}
void set_value(std::string_view value, std::error_code &ec) override
{
if (value == "true")
m_seen = 1;
else if (value == "false")
m_seen = 0;
else if (auto [ptr, ec2] = mcfp::from_chars(
value.data(), value.data() + value.length(), m_seen);
ec2 != std::errc{} or ptr != value.data() + value.length())
ec = make_error_code(config_error::wrong_type_cast_flag);
}
};
} // namespace mcfp