#pragma once /* * __ _ _ __ __ _ _ __ __ _ _ __ ___ ___ * / _` | '__/ _` | '_ \ / _` | '__/ __|/ _ \ Argument Parser for Modern C++ * | (_| | | | (_| | |_) | (_| | | \__ \ __/ http://github.com/p-ranav/argparse * \__,_|_| \__, | .__/ \__,_|_| |___/\___| * |___/|_| * * Licensed under the MIT License . * SPDX-License-Identifier: MIT * Copyright (c) 2019-2022 Pranav Srinivas Kumar * and other contributors. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #ifndef ARGPARSE_MODULE_USE_STD_MODULE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #endif #include "src/util/defs.hpp" #include "src/util/error.hpp" // Added for Result type #include "src/util/types.hpp" #ifndef ARGPARSE_CUSTOM_STRTOF #define ARGPARSE_CUSTOM_STRTOF strtof #endif #ifndef ARGPARSE_CUSTOM_STRTOD #define ARGPARSE_CUSTOM_STRTOD strtod #endif #ifndef ARGPARSE_CUSTOM_STRTOLD #define ARGPARSE_CUSTOM_STRTOLD strtold #endif // ReSharper disable CppTemplateParameterNeverUsed, CppDFATimeOver // NOLINTBEGIN(readability-identifier-naming, readability-identifier-length) namespace argparse { using namespace util::types; namespace details { // namespace for helper methods template struct HasContainerTraits : std::false_type {}; template <> struct HasContainerTraits : std::false_type {}; template <> struct HasContainerTraits : std::false_type {}; template struct HasContainerTraits().begin()), decltype(std::declval().end()), decltype(std::declval().size())>> : std::true_type {}; template inline constexpr bool IsContainer = HasContainerTraits::value; template struct HasStreamableTraits : std::false_type {}; template struct HasStreamableTraits() << std::declval())>> : std::true_type {}; template inline constexpr bool IsStreamable = HasStreamableTraits::value; constexpr usize repr_max_container_size = 5; template concept Formattable = requires(const T& t, std::basic_format_context ctx) { std::formatter, CharT>().format(t, ctx); }; template static auto repr(const T& val) -> String { if constexpr (std::is_same_v) return val ? "true" : "false"; else if constexpr (std::is_convertible_v) return std::format("\"{}\"", String { StringView { val } }); else if constexpr (IsContainer) { String result = "{"; const auto size = val.size(); if (size > 0) { bool first = true; auto transformed_view = val | std::views::transform([](const auto& elem) { return details::repr(elem); }); if (size <= repr_max_container_size) { for (const String& elem_repr : transformed_view) { if (!first) result += " "; result += elem_repr; first = false; } } else { for (const String& elem_repr : transformed_view | std::views::take(repr_max_container_size - 1)) { if (!first) result += " "; result += elem_repr; first = false; } result += "... "; result += details::repr(*std::prev(val.end())); } } result += "}"; return result; } else if constexpr (Formattable) return std::format("{}", val); else if constexpr (IsStreamable) { std::stringstream out; out << val; return out.str(); } else return ""; } constexpr i32 radix_2 = 2; constexpr i32 radix_8 = 8; constexpr i32 radix_10 = 10; constexpr i32 radix_16 = 16; template constexpr fn apply_plus_one_impl(F&& f, Tuple&& t, Extra&& x, std::index_sequence /*unused*/) -> decltype(auto) { return std::invoke(std::forward(f), std::get(std::forward(t))..., std::forward(x)); } template constexpr fn apply_plus_one(F&& f, Tuple&& t, Extra&& x) -> decltype(auto) { return details::apply_plus_one_impl( std::forward(f), std::forward(t), std::forward(x), std::make_index_sequence>> {} ); } constexpr fn pointer_range(const StringView s) noexcept -> std::tuple { return { s.data(), s.data() + s.size() }; } template constexpr fn starts_with(std::basic_string_view prefix, std::basic_string_view s) noexcept -> bool { return s.substr(0, prefix.size()) == prefix; } enum class chars_format : u8 { scientific = 0xf1, fixed = 0xf2, hex = 0xf4, binary = 0xf8, general = fixed | scientific }; struct ConsumeBinaryPrefixResult { bool is_binary; StringView rest; }; constexpr fn consume_binary_prefix(StringView s) -> ConsumeBinaryPrefixResult { if (starts_with(StringView { "0b" }, s) || starts_with(StringView { "0B" }, s)) { s.remove_prefix(2); return { .is_binary = true, .rest = s }; } return { .is_binary = false, .rest = s }; } struct ConsumeHexPrefixResult { bool is_hexadecimal; StringView rest; }; using namespace std::literals; constexpr fn consume_hex_prefix(StringView s) -> ConsumeHexPrefixResult { if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { s.remove_prefix(2); return { .is_hexadecimal = true, .rest = s }; } return { .is_hexadecimal = false, .rest = s }; } template fn do_from_chars(const StringView s) -> Result { T x { 0 }; auto [first, last] = pointer_range(s); auto [ptr, ec] = std::from_chars(first, last, x, Param); if (ec == std::errc()) { if (ptr == last) return x; return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("pattern '{}' does not match to the end", String(s)))); } if (ec == std::errc::invalid_argument) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("pattern '{}' not found", String(s)))); if (ec == std::errc::result_out_of_range) return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("'{}' not representable", String(s)))); // Should be unreachable, but handle potential unknown error codes return Err(util::error::DracError(util::error::DracErrorCode::InternalError, std::format("Unknown parsing error for '{}'", String(s)))); } template struct parse_number { static fn operator()(const StringView s)->Result { return do_from_chars(s); } }; template struct parse_number { static fn operator()(const StringView s)->Result { if (auto [ok, rest] = consume_binary_prefix(s); ok) return do_from_chars(rest); return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "pattern not found")); } }; template struct parse_number { static fn operator()(const StringView s)->Result { Result result; if (starts_with("0x"sv, s) || starts_with("0X"sv, s)) { if (auto [ok, rest] = consume_hex_prefix(s); ok) result = do_from_chars(rest); else return Err(util::error::DracError(util::error::DracErrorCode::InternalError, std::format("Inconsistent hex prefix detection for '{}'", String(s)))); } else result = do_from_chars(s); if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as hexadecimal: {}", String(s), result.error().message))); return result; } }; template struct parse_number { static fn operator()(const StringView s)->Result { if (auto [ok, rest] = consume_hex_prefix(s); ok) { Result result = do_from_chars(rest); if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as hexadecimal: {}", String(s), result.error().message))); return result; } if (auto [ok_binary, rest_binary] = consume_binary_prefix(s); ok_binary) { Result result = do_from_chars(rest_binary); if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as binary: {}", String(s), result.error().message))); return result; } // Note: consume_hex_prefix already removed the prefix if present, so 'rest' is correct here for octal/decimal check. if (starts_with("0"sv, s)) { // Check original string for octal prefix Result result = do_from_chars(s); // Pass original string for octal if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as octal: {}", String(s), result.error().message))); return result; } Result result = do_from_chars(s); // Pass original string for decimal if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as decimal integer: {}", String(s), result.error().message))); return result; } }; template inline constexpr std::nullptr_t generic_strtod = nullptr; template <> inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOF; template <> inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOD; template <> inline const auto generic_strtod = ARGPARSE_CUSTOM_STRTOLD; template fn do_strtod(const String& s) -> Result { if (isspace(static_cast(s[0])) || s[0] == '+') return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("pattern '{}' not found", s))); auto [first, last] = pointer_range(s); char* ptr = nullptr; errno = 0; auto x = generic_strtod(first, &ptr); if (errno == 0) { if (ptr == last) return x; return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("pattern '{}' does not match to the end", s))); } if (errno == ERANGE) return Err(util::error::DracError(util::error::DracErrorCode::ParseError, std::format("'{}' not representable", s))); // Handle other potential errno values return Err(util::error::DracError(std::error_code(errno, std::system_category()))); } template struct parse_number { fn operator()(const String& s)->Result { if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::general does not parse hexfloat")); if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::general does not parse binfloat")); Result result = do_strtod(s); if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as number: {}", s, result.error().message))); return result; } }; template struct parse_number { fn operator()(const String& s)->Result { if (auto [is_hex, rest] = consume_hex_prefix(s); !is_hex) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::hex requires hexfloat format (e.g., 0x1.2p3)")); if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::hex does not parse binfloat")); Result result = do_strtod(s); if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as hexadecimal float: {}", s, result.error().message))); return result; } }; template struct parse_number { fn operator()(const String& s)->Result { if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::binary does not parse hexfloat")); if (auto [is_bin, rest] = consume_binary_prefix(s); !is_bin) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::binary requires binfloat format (e.g., 0b1.01p2)")); Result result = do_strtod(s); if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as binary float: {}", s, result.error().message))); return result; } }; template struct parse_number { fn operator()(const String& s)->Result { if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::scientific does not parse hexfloat")); if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::scientific does not parse binfloat")); if (s.find_first_of("eE") == String::npos) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::scientific requires exponent part")); Result result = do_strtod(s); if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as scientific notation: {}", s, result.error().message))); return result; } }; template struct parse_number { fn operator()(const String& s)->Result { if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::fixed does not parse hexfloat")); if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::fixed does not parse binfloat")); if (s.find_first_of("eE") != String::npos) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, "chars_format::fixed does not parse exponent part")); Result result = do_strtod(s); if (!result) return Err(util::error::DracError(result.error().code, std::format("Failed to parse '{}' as fixed notation: {}", s, result.error().message))); return result; } }; template concept ToStringConvertible = std::convertible_to || std::convertible_to || requires(const T& t) { std::format("{}", t); }; template fn join(StrIt first, StrIt last, const String& separator) -> String { if (first == last) return ""; std::stringstream value; value << *first; ++first; while (first != last) { value << separator << *first; ++first; } return value.str(); } template struct can_invoke_to_string { template // ReSharper disable CppFunctionIsNotImplemented static fn test(int) -> decltype(std::to_string(std::declval()), std::true_type {}); template static fn test(...) -> std::false_type; // ReSharper restore CppFunctionIsNotImplemented static constexpr bool value = decltype(test(0))::value; }; template struct IsChoiceTypeSupported { using CleanType = std::decay_t; static const bool value = std::is_integral_v || std::is_same_v || std::is_same_v || std::is_same_v; }; template fn get_levenshtein_distance(const StringType& s1, const StringType& s2) -> usize { Vec> dp( s1.size() + 1, Vec(s2.size() + 1, 0) ); for (usize i = 0; i <= s1.size(); ++i) { for (usize j = 0; j <= s2.size(); ++j) { if (i == 0) { dp[i][j] = j; } else if (j == 0) { dp[i][j] = i; } else if (s1[i - 1] == s2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + std::min({ dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1] }); } } } return dp[s1.size()][s2.size()]; } template fn get_most_similar_string(const Map& map, const String& input) -> String { String most_similar {}; usize min_distance = (std::numeric_limits::max)(); for (const auto& entry : map) if (const usize distance = get_levenshtein_distance(entry.first, input); distance < min_distance) { min_distance = distance; most_similar = entry.first; } return most_similar; } } // namespace details enum class nargs_pattern : u8 { optional, any, at_least_one, }; enum class default_arguments : u8 { none = 0, help = 1, version = 2, all = help | version, }; inline fn operator&(const default_arguments& a, const default_arguments& b)->default_arguments { return static_cast( std::to_underlying(a) & std::to_underlying(b) ); } class ArgumentParser; class Argument { friend class ArgumentParser; friend fn operator<<(std::ostream& stream, const ArgumentParser& parser) ->std::ostream&; template explicit Argument(const StringView prefix_chars, std::array&& a, std::index_sequence /*unused*/) // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved) : m_accepts_optional_like_value(false), m_is_optional((is_optional(a[I], prefix_chars) || ...)), m_is_required(false), m_is_repeatable(false), m_is_used(false), m_is_hidden(false), m_prefix_chars(prefix_chars) { ((void)m_names.emplace_back(a[I]), ...); std::sort( m_names.begin(), m_names.end(), [](const auto& lhs, const auto& rhs) { return lhs.size() == rhs.size() ? lhs < rhs : lhs.size() < rhs.size(); } ); } public: template explicit Argument(StringView prefix_chars, std::array&& a) : Argument(prefix_chars, std::move(a), std::make_index_sequence {}) {} fn help(String help_text) -> Argument& { m_help = std::move(help_text); return *this; } fn metavar(String metavar) -> Argument& { m_metavar = std::move(metavar); return *this; } template fn default_value(T&& value) -> Argument& { m_num_args_range = NArgsRange { 0, m_num_args_range.get_max() }; m_default_value_repr = details::repr(value); if constexpr (std::is_convertible_v) m_default_value_str = String { StringView { value } }; else if constexpr (details::can_invoke_to_string::value) m_default_value_str = std::to_string(value); m_default_value = std::forward(value); return *this; } fn default_value(const char* value) -> Argument& { return default_value(String(value)); } fn required() -> Argument& { m_is_required = true; return *this; } fn implicit_value(std::any value) -> Argument& { m_implicit_value = std::move(value); m_num_args_range = NArgsRange { 0, 0 }; return *this; } // This is shorthand for: // program.add_argument("foo") // .default_value(false) // .implicit_value(true) fn flag() -> Argument& { default_value(false); implicit_value(true); return *this; } template fn action(F&& callable, Args&&... bound_args) -> Argument& requires(std::is_invocable_v) { using action_type = std::conditional_t>, void_action, valued_action>; if constexpr (sizeof...(Args) == 0) m_actions.emplace_back(std::forward(callable)); else m_actions.emplace_back( [f = std::forward(callable), tup = std::make_tuple(std::forward(bound_args)...)](const String& opt) mutable { return details::apply_plus_one(f, tup, opt); } ); return *this; } fn store_into(bool& var) ->Argument& { if ((!m_default_value.has_value()) && (!m_implicit_value.has_value())) flag(); if (m_default_value.has_value()) var = std::any_cast(m_default_value); action([&var](const String& /*unused*/) { var = true; return var; }); return *this; } template fn store_into(T& var) -> Argument& requires(std::is_integral_v) { if (m_default_value.has_value()) var = std::any_cast(m_default_value); action([&var](const auto& s) { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as decimal integer: {}", s, result.error().message)); var = *result; return var; }); return *this; } template fn store_into(T& var)->Argument& requires(std::is_floating_point_v) { if (m_default_value.has_value()) var = std::any_cast(m_default_value); action([&var](const auto& s) { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as number: {}", s, result.error().message)); var = *result; return var; }); return *this; } fn store_into(String& var) ->Argument& { if (m_default_value.has_value()) var = std::any_cast(m_default_value); action([&var](const String& s) { var = s; return var; }); return *this; } fn store_into(std::filesystem::path& var) -> Argument& { if (m_default_value.has_value()) var = std::any_cast(m_default_value); action([&var](const String& s) { var = s; }); return *this; } fn store_into(Vec& var) -> Argument& { if (m_default_value.has_value()) var = std::any_cast>(m_default_value); action([this, &var](const String& s) { if (!m_is_used) var.clear(); m_is_used = true; var.push_back(s); return var; }); return *this; } fn store_into(Vec& var) -> Argument& { if (m_default_value.has_value()) var = std::any_cast>(m_default_value); action([this, &var](const String& s) { if (!m_is_used) var.clear(); m_is_used = true; Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as decimal integer for vector: {}", s, result.error().message)); var.push_back(*result); return var; }); return *this; } fn store_into(std::set& var) -> Argument& { if (m_default_value.has_value()) var = std::any_cast>(m_default_value); action([this, &var](const String& s) { if (!m_is_used) var.clear(); m_is_used = true; var.insert(s); return var; }); return *this; } fn store_into(std::set& var) -> Argument& { if (m_default_value.has_value()) var = std::any_cast>(m_default_value); action([this, &var](const String& s) { if (!m_is_used) var.clear(); m_is_used = true; Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as decimal integer for set: {}", s, result.error().message)); var.insert(*result); return var; }); return *this; } fn append() -> Argument& { m_is_repeatable = true; return *this; } // Cause the argument to be invisible in usage and help fn hidden() -> Argument& { m_is_hidden = true; return *this; } template fn scan() -> Argument& requires(std::is_arithmetic_v) { static_assert(!(std::is_const_v || std::is_volatile_v), "T should not be cv-qualified"); fn is_one_of = [](char c, auto... x) constexpr { return ((c == x) || ...); }; if constexpr (Shape == 'd' && std::is_integral_v) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as decimal integer (scan 'd'): {}", s, result.error().message)); return *result; }); else if constexpr (Shape == 'i' && std::is_integral_v) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as integer (scan 'i'): {}", s, result.error().message)); return *result; }); else if constexpr (Shape == 'u' && (std::is_integral_v && std::is_unsigned_v)) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as unsigned decimal integer (scan 'u'): {}", s, result.error().message)); return *result; }); else if constexpr (Shape == 'b' && (std::is_integral_v && std::is_unsigned_v)) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as binary integer (scan 'b'): {}", s, result.error().message)); return *result; }); else if constexpr (Shape == 'o' && (std::is_integral_v && std::is_unsigned_v)) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as octal integer (scan 'o'): {}", s, result.error().message)); return *result; }); else if constexpr (is_one_of(Shape, 'x', 'X') && (std::is_integral_v && std::is_unsigned_v)) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as hexadecimal integer (scan '{}'): {}", s, Shape, result.error().message)); return *result; }); else if constexpr (is_one_of(Shape, 'a', 'A') && std::is_floating_point_v) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as hexadecimal float (scan '{}'): {}", s, Shape, result.error().message)); return *result; }); else if constexpr (is_one_of(Shape, 'e', 'E') && std::is_floating_point_v) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as scientific float (scan '{}'): {}", s, Shape, result.error().message)); return *result; }); else if constexpr (is_one_of(Shape, 'f', 'F') && std::is_floating_point_v) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as fixed float (scan '{}'): {}", s, Shape, result.error().message)); return *result; }); else if constexpr (is_one_of(Shape, 'g', 'G') && std::is_floating_point_v) action([](const String& s) -> T { Result result = details::parse_number()(s); if (!result) throw std::runtime_error(std::format("Failed to parse '{}' as general float (scan '{}'): {}", s, Shape, result.error().message)); return *result; }); else static_assert(false, "No scan specification for T"); return *this; } fn nargs(const usize num_args) ->Argument& { m_num_args_range = NArgsRange { num_args, num_args }; return *this; } fn nargs(const usize num_args_min, const usize num_args_max) -> Argument& { m_num_args_range = NArgsRange { num_args_min, num_args_max }; return *this; } fn nargs(const nargs_pattern pattern) -> Argument& { switch (pattern) { case nargs_pattern::optional: m_num_args_range = NArgsRange { 0, 1 }; break; case nargs_pattern::any: m_num_args_range = NArgsRange { 0, (std::numeric_limits::max)() }; break; case nargs_pattern::at_least_one: m_num_args_range = NArgsRange { 1, (std::numeric_limits::max)() }; break; } return *this; } fn remaining() -> Argument& { m_accepts_optional_like_value = true; return nargs(nargs_pattern::any); } template fn add_choice(T&& choice) -> void { static_assert(details::IsChoiceTypeSupported::value, "Only string or integer type supported for choice"); static_assert(std::is_convertible_v || details::can_invoke_to_string::value, "Choice is not convertible to string_type"); if (!m_choices.has_value()) m_choices = Vec {}; if constexpr (std::is_convertible_v) m_choices.value().emplace_back(StringView { std::forward(choice) }); else if constexpr (details::can_invoke_to_string::value) m_choices.value().push_back(std::to_string(std::forward(choice))); } fn choices() -> Argument& { if (!m_choices.has_value()) throw std::runtime_error("Zero choices provided"); return *this; } template fn choices(T&& first, U&&... rest) -> Argument& { add_choice(std::forward(first)); choices(std::forward(rest)...); return *this; } fn find_default_value_in_choices_or_throw() const -> void { assert(m_choices.has_value()); const Vec& choices = m_choices.value(); if (m_default_value.has_value()) { if (std::ranges::find(choices, m_default_value_str) == choices.end()) { // provided arg not in list of allowed choices // report error const String choices_as_csv = std::accumulate(choices.begin(), choices.end(), String(), [](const String& a, const String& b) { return a + (a.empty() ? "" : ", ") + b; }); throw std::runtime_error( String { "Invalid default value " } + m_default_value_repr + " - allowed options: {" + choices_as_csv + "}" ); } } } template [[nodiscard]] fn is_value_in_choices(Iterator option_it) const -> bool { assert(m_choices.has_value()); const Vec& choices = m_choices.value(); return (std::find(choices.begin(), choices.end(), *option_it) != choices.end()); } template fn throw_invalid_arguments_error(Iterator option_it) const -> void { assert(m_choices.has_value()); const Vec& choices = m_choices.value(); const String choices_as_csv = std::accumulate( choices.begin(), choices.end(), String(), [](const String& option_a, const String& option_b) { return option_a + (option_a.empty() ? "" : ", ") + option_b; } ); throw std::runtime_error(String { "Invalid argument " } + details::repr(*option_it) + " - allowed options: {" + choices_as_csv + "}"); } /* The dry_run parameter can be set to true to avoid running the actions, * and setting m_is_used. This may be used by a pre-processing step to do * a first iteration over arguments. */ template fn consume(Iterator start, Iterator end, const StringView used_name = {}, const bool dry_run = false) -> Iterator { if (!m_is_repeatable && m_is_used) throw std::runtime_error( String("Duplicate argument ").append(used_name) ); m_used_name = used_name; usize passed_options = 0; if (m_choices.has_value()) { // Check each value in (start, end) and make sure // it is in the list of allowed choices/options const auto max_number_of_args = m_num_args_range.get_max(); const auto min_number_of_args = m_num_args_range.get_min(); for (auto it = start; it != end; ++it) { if (is_value_in_choices(it)) { passed_options += 1; continue; } if ((passed_options >= min_number_of_args) && (passed_options <= max_number_of_args)) { break; } throw_invalid_arguments_error(it); } } const usize num_args_max = (m_choices.has_value()) ? passed_options : m_num_args_range.get_max(); const usize num_args_min = m_num_args_range.get_min(); if (num_args_max == 0) { if (!dry_run) { m_values.emplace_back(m_implicit_value); for (auto& action : m_actions) std::visit([&](const auto& f) { f({}); }, action); if (m_actions.empty()) std::visit([&](const auto& f) { f({}); }, m_default_action); m_is_used = true; } return start; } if (auto dist = static_cast(std::distance(start, end)); dist >= num_args_min) { if (num_args_max < dist) end = std::next(start, static_cast(num_args_max)); if (!m_accepts_optional_like_value) { end = std::find_if( start, end, [this](T&& PH1) { return is_optional(std::forward(PH1), m_prefix_chars); } ); dist = static_cast(std::distance(start, end)); if (dist < num_args_min) throw std::runtime_error("Too few arguments for '" + String(m_used_name) + "'."); } struct ActionApply { fn operator()(valued_action& f)->void { std::transform(first, last, std::back_inserter(self.m_values), f); } fn operator()(void_action& f)->void { std::for_each(first, last, f); if (!self.m_default_value.has_value()) if (!self.m_accepts_optional_like_value) self.m_values.resize( static_cast(std::distance(first, last)) ); } Iterator first, last; Argument self; }; if (!dry_run) { for (std::variant& action : m_actions) std::visit(ActionApply { start, end, *this }, action); if (m_actions.empty()) std::visit(ActionApply { start, end, *this }, m_default_action); m_is_used = true; } return end; } if (m_default_value.has_value()) { if (!dry_run) m_is_used = true; return start; } throw std::runtime_error("Too few arguments for '" + String(m_used_name) + "'."); } /* * @returns Result indicating success or failure */ [[nodiscard]] fn validate() const -> Result { if (m_is_optional) { // TODO: check if an implicit value was programmed for this argument if (!m_is_used && !m_default_value.has_value() && m_is_required) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("Required argument '{}' was not provided", m_names[0]))); if (m_is_used && m_is_required && m_values.empty()) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("Required argument '{}' requires a value, but none was provided", m_names[0]))); if (m_is_used && m_num_args_range.get_min() > m_values.size()) return Err(util::error::DracError(util::error::DracErrorCode::InvalidArgument, std::format("Too few arguments for optional argument '{}'. Expected at least {}, got {}.", m_names[0], m_num_args_range.get_min(), m_values.size()))); } else { if (!m_num_args_range.contains(m_values.size()) && !m_default_value.has_value()) { String expected_str; if (m_num_args_range.is_exact()) expected_str = std::to_string(m_num_args_range.get_min()); else if (!m_num_args_range.is_right_bounded()) expected_str = std::format("at least {}", m_num_args_range.get_min()); else expected_str = std::format("{} to {}", m_num_args_range.get_min(), m_num_args_range.get_max()); return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Incorrect number of arguments for positional argument '{}'. Expected {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), expected_str, m_values.size()))); } if (m_num_args_range.get_min() > m_values.size()) return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Too few arguments for positional argument '{}'. Expected at least {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), m_num_args_range.get_min(), m_values.size()))); } if (m_num_args_range.get_max() < m_values.size()) { if (m_is_optional) return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Too many arguments for optional argument '{}'. Expected at most {}, got {}.", m_names[0], m_num_args_range.get_max(), m_values.size()))); return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Too many arguments for positional argument '{}'. Expected at most {}, got {}.", (m_metavar.empty() ? m_names[0] : m_metavar), m_num_args_range.get_max(), m_values.size()))); } if (m_choices.has_value()) { const Vec& choices = m_choices.value(); // Check default value if (m_default_value.has_value()) if (const String& default_val_str = m_default_value_str.value(); std::ranges::find(choices, default_val_str) == choices.end()) { const String choices_as_csv = std::accumulate( choices.begin(), choices.end(), String(), [](const String& option_a, const String& option_b) -> String { return option_a + (option_a.empty() ? "" : ", ") + option_b; } ); return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Default value '{}' is not in the allowed choices: {{{}}}", default_val_str, choices_as_csv))); } // Check provided values for (const auto& value_any : m_values) { if (value_any.type() != typeid(String)) return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Invalid argument type for choice validation - expected string, got '{}'", value_any.type().name()))); if (const String& value = std::any_cast(value_any); std::ranges::find(choices, value) == choices.end()) { const String choices_as_csv = std::accumulate( choices.begin(), choices.end(), String(), [](const String& option_a, const String& option_b) -> String { return std::format("{}{}{}", option_a, option_a.empty() ? "" : ", ", option_b); } ); return Err(DracError(util::error::DracErrorCode::InvalidArgument, std::format("Invalid argument '{}' - allowed options: {{{}}}", value, choices_as_csv))); } } } return {}; } [[nodiscard]] fn get_names_csv(const char separator = ',') const -> String { return std::accumulate( m_names.begin(), m_names.end(), String { "" }, [&](const String& result, const String& name) { return result.empty() ? name : result + separator + name; } ); } [[nodiscard]] fn get_usage_full() const -> String { std::stringstream usage; usage << get_names_csv('/'); const String metavar = !m_metavar.empty() ? m_metavar : "VAR"; if (m_num_args_range.get_max() > 0) { usage << " " << metavar; if (m_num_args_range.get_max() > 1) usage << "..."; } return usage.str(); } [[nodiscard]] fn get_inline_usage() const -> String { std::stringstream usage; // Find the longest variant to show in the usage string String longest_name = m_names.front(); for (const String& s : m_names) if (s.size() > longest_name.size()) longest_name = s; if (!m_is_required) usage << "["; usage << longest_name; const String metavar = !m_metavar.empty() ? m_metavar : "VAR"; if (m_num_args_range.get_max() > 0) { usage << " " << metavar; if (m_num_args_range.get_max() > 1 && m_metavar.contains("> <")) usage << "..."; } if (!m_is_required) usage << "]"; if (m_is_repeatable) usage << "..."; return usage.str(); } [[nodiscard]] fn get_arguments_length() const -> usize { const usize names_size = std::accumulate( std::begin(m_names), std::end(m_names), static_cast(0), [](const u32& sum, const String& s) { return sum + s.size(); } ); if (is_positional(m_names.front(), m_prefix_chars)) { // A set metavar means this replaces the names if (!m_metavar.empty()) // Indent and metavar return 2 + m_metavar.size(); // Indent and space-separated return 2 + names_size + (m_names.size() - 1); } // Is an option - include both names _and_ metavar // size = text + (", " between names) usize size = names_size + (2 * (m_names.size() - 1)); if (!m_metavar.empty() && m_num_args_range == NArgsRange { 1, 1 }) size += m_metavar.size() + 1; return size + 2; // indent } friend fn operator<<(std::ostream& stream, const Argument& argument)->std::ostream& { std::stringstream name_stream; name_stream << " "; // indent // if (argparse::Argument::is_positional(argument.m_names.front(), argument.m_prefix_chars)) { if (!argument.m_metavar.empty()) { name_stream << argument.m_metavar; } else { name_stream << details::join(argument.m_names.begin(), argument.m_names.end(), " "); } } else { name_stream << details::join(argument.m_names.begin(), argument.m_names.end(), ", "); // If we have a metavar, and one narg - print the metavar if (!argument.m_metavar.empty() && ((argument.m_num_args_range == NArgsRange { 1, 1 }) || (argument.m_num_args_range.get_min() == argument.m_num_args_range.get_max() && argument.m_metavar.contains("> <")))) { name_stream << " " << argument.m_metavar; } } // align multiline help message const std::streamsize stream_width = stream.width(); const String name_padding = String(name_stream.str().size(), ' '); auto pos = String::size_type {}; auto prev = String::size_type {}; bool first_line = true; const char* hspace = " "; // minimal space between name and help message stream << name_stream.str(); const StringView help_view(argument.m_help); while ((pos = argument.m_help.find('\n', prev)) != String::npos) { const StringView line = help_view.substr(prev, pos - prev + 1); if (first_line) { stream << hspace << line; first_line = false; } else { stream.width(stream_width); stream << name_padding << hspace << line; } prev += pos - prev + 1; } if (first_line) stream << hspace << argument.m_help; else if (const StringView leftover = help_view.substr(prev, argument.m_help.size() - prev); !leftover.empty()) { stream.width(stream_width); stream << name_padding << hspace << leftover; } // print nargs spec if (!argument.m_help.empty()) stream << " "; stream << argument.m_num_args_range; bool add_space = false; if (argument.m_default_value.has_value() && argument.m_num_args_range != NArgsRange { 0, 0 }) { stream << "[default: " << argument.m_default_value_repr << "]"; add_space = true; } else if (argument.m_is_required) { stream << "[required]"; add_space = true; } if (argument.m_is_repeatable) { if (add_space) stream << " "; stream << "[may be repeated]"; } stream << "\n"; return stream; } template fn operator!=(const T& rhs) const->bool { return !(*this == rhs); } /* * Compare to an argument value of known type * @throws std::logic_error in case of incompatible types */ template fn operator==(const T& rhs) const->bool { if constexpr (!details::IsContainer) return get() == rhs; else { using ValueType = typename T::value_type; auto lhs = get(); return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs), std::end(rhs), [](const auto& a, const auto& b) { return std::any_cast(a) == b; }); } } /* * positional: * _empty_ * '-' * '-' decimal-literal * !'-' anything */ static fn is_positional(StringView name, const StringView prefix_chars) -> bool { const int first = lookahead(name); if (first == eof) return true; if (prefix_chars.contains(static_cast(first))) { name.remove_prefix(1); if (name.empty()) return true; return is_decimal_literal(name); } return true; } private: class NArgsRange { usize m_min; usize m_max; public: NArgsRange(const usize minimum, const usize maximum) : m_min(minimum), m_max(maximum) { if (minimum > maximum) throw std::logic_error("Range of number of arguments is invalid"); } [[nodiscard]] fn contains(const usize value) const -> bool { return value >= m_min && value <= m_max; } [[nodiscard]] fn is_exact() const -> bool { return m_min == m_max; } [[nodiscard]] fn is_right_bounded() const -> bool { return m_max < (std::numeric_limits::max)(); } [[nodiscard]] fn get_min() const -> usize { return m_min; } [[nodiscard]] fn get_max() const -> usize { return m_max; } // Print help message friend fn operator<<(std::ostream& stream, const NArgsRange& range) ->std::ostream& { if (range.m_min == range.m_max) { if (range.m_min != 0 && range.m_min != 1) stream << "[nargs: " << range.m_min << "] "; } else if (range.m_max == (std::numeric_limits::max)()) stream << "[nargs: " << range.m_min << " or more] "; else stream << "[nargs=" << range.m_min << ".." << range.m_max << "] "; return stream; } fn operator==(const NArgsRange& rhs) const->bool { return rhs.m_min == m_min && rhs.m_max == m_max; } fn operator!=(const NArgsRange& rhs) const->bool { return !(*this == rhs); } }; fn throw_nargs_range_validation_error() const -> void { std::stringstream stream; if (!m_used_name.empty()) stream << m_used_name << ": "; else stream << m_names.front() << ": "; if (m_num_args_range.is_exact()) stream << m_num_args_range.get_min(); else if (m_num_args_range.is_right_bounded()) stream << m_num_args_range.get_min() << " to " << m_num_args_range.get_max(); else stream << m_num_args_range.get_min() << " or more"; stream << " argument(s) expected. " << m_values.size() << " provided."; throw std::runtime_error(stream.str()); } fn throw_required_arg_not_used_error() const -> void { std::stringstream stream; stream << m_names.front() << ": required."; throw std::runtime_error(stream.str()); } fn throw_required_arg_no_value_provided_error() const -> void { std::stringstream stream; stream << m_used_name << ": no value provided."; throw std::runtime_error(stream.str()); } static constexpr int eof = std::char_traits::eof(); static fn lookahead(const StringView sview) -> int { if (sview.empty()) return eof; return static_cast(sview[0]); } /* * decimal-literal: * '0' * nonzero-digit digit-sequence_opt * integer-part fractional-part * fractional-part * integer-part '.' exponent-part_opt * integer-part exponent-part * * integer-part: * digit-sequence * * fractional-part: * '.' post-decimal-point * * post-decimal-point: * digit-sequence exponent-part_opt * * exponent-part: * 'e' post-e * 'E' post-e * * post-e: * sign_opt digit-sequence * * sign: one of * '+' '-' */ // NOLINTBEGIN(cppcoreguidelines-avoid-goto) static fn is_decimal_literal(StringView s) -> bool { fn is_digit = [](auto c) constexpr -> bool { return c >= '0' && c <= '9'; }; // precondition: we have consumed or will consume at least one digit fn consume_digits = [=](StringView sd) -> StringView { const auto it = std::ranges::find_if_not(sd, is_digit); return sd.substr(static_cast(it - std::begin(sd))); }; switch (lookahead(s)) { case '0': { s.remove_prefix(1); if (s.empty()) return true; goto integer_part; } case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { s = consume_digits(s); if (s.empty()) return true; goto integer_part_consumed; } case '.': { s.remove_prefix(1); goto post_decimal_point; } default: return false; } integer_part: s = consume_digits(s); integer_part_consumed: switch (lookahead(s)) { case '.': { s.remove_prefix(1); if (is_digit(lookahead(s))) goto post_decimal_point; goto exponent_part_opt; } case 'e': case 'E': { s.remove_prefix(1); goto post_e; } default: return false; } post_decimal_point: if (is_digit(lookahead(s))) { s = consume_digits(s); goto exponent_part_opt; } return false; exponent_part_opt: switch (lookahead(s)) { case eof: return true; case 'e': case 'E': { s.remove_prefix(1); goto post_e; } default: return false; } post_e: switch (lookahead(s)) { case '-': case '+': s.remove_prefix(1); default: break; } if (is_digit(lookahead(s))) { s = consume_digits(s); return s.empty(); } return false; } // NOLINTEND(cppcoreguidelines-avoid-goto) static fn is_optional(const StringView name, const StringView prefix_chars) -> bool { return !is_positional(name, prefix_chars); } /* * Get argument value given a type * @throws std::logic_error in case of incompatible types */ template fn get() const -> T { if (!m_values.empty()) { if constexpr (details::IsContainer) return any_cast_container(m_values); else return std::any_cast(m_values.front()); } if (m_default_value.has_value()) return std::any_cast(m_default_value); if constexpr (details::IsContainer) if (!m_accepts_optional_like_value) return any_cast_container(m_values); throw std::logic_error("No value provided for '" + m_names.back() + "'."); } /* * Get argument value given a type. * @pre The object has no default value. * @returns The stored value if any, std::nullopt otherwise. */ template fn present() const -> Option { if (m_default_value.has_value()) throw std::logic_error("Argument with default value always presents"); if (m_values.empty()) return std::nullopt; if constexpr (details::IsContainer) return any_cast_container(m_values); return std::any_cast(m_values.front()); } template static fn any_cast_container(const Vec& operand) -> T { using ValueType = typename T::value_type; T result; std::transform( std::begin(operand), std::end(operand), std::back_inserter(result), [](const auto& value) { return std::any_cast(value); } ); return result; } fn set_usage_newline_counter(const int i) -> void { m_usage_newline_counter = i; } fn set_group_idx(const usize i) -> void { m_group_idx = i; } Vec m_names; StringView m_used_name; String m_help; String m_metavar; std::any m_default_value; String m_default_value_repr; Option m_default_value_str; // used for checking default_value against choices std::any m_implicit_value; Option> m_choices { std::nullopt }; using valued_action = std::function; using void_action = std::function; Vec> m_actions; std::variant m_default_action { std::in_place_type, [](const String& value) { return value; } }; Vec m_values; NArgsRange m_num_args_range { 1, 1 }; // Bit field of bool values. Set default value in ctor. bool m_accepts_optional_like_value : 1; bool m_is_optional : 1; bool m_is_required : 1; bool m_is_repeatable : 1; bool m_is_used : 1; bool m_is_hidden : 1; // if set, does not appear in usage or help StringView m_prefix_chars; // ArgumentParser has the prefix_chars int m_usage_newline_counter = 0; usize m_group_idx = 0; }; class ArgumentParser { public: explicit ArgumentParser(String program_name = {}, String version = "1.0", const default_arguments add_args = default_arguments::all, const bool exit_on_default_arguments = true, std::ostream& os = std::cout) : m_program_name(std::move(program_name)), m_version(std::move(version)), m_exit_on_default_arguments(exit_on_default_arguments), m_parser_path(m_program_name) { if ((add_args & default_arguments::help) == default_arguments::help) add_argument("-h", "--help") .action([&](const String& /*unused*/) { os << help().str(); if (m_exit_on_default_arguments) std::exit(0); }) .default_value(false) .help("shows help message and exits") .implicit_value(true) .nargs(0); if ((add_args & default_arguments::version) == default_arguments::version) add_argument("-v", "--version") .action([&](const String& /*unused*/) { os << m_version << '\n'; if (m_exit_on_default_arguments) std::exit(0); }) .default_value(false) .help("prints version information and exits") .implicit_value(true) .nargs(0); } ~ArgumentParser() = default; // ArgumentParser is meant to be used in a single function. // Setup everything and parse arguments in one place. // // ArgumentParser internally uses StringViews, // references, iterators, etc. // Many of these elements become invalidated after a copy or move. ArgumentParser(const ArgumentParser& other) = delete; fn operator=(const ArgumentParser& other)->ArgumentParser& = delete; ArgumentParser(ArgumentParser&&) noexcept = delete; fn operator=(ArgumentParser&&)->ArgumentParser& = delete; explicit operator bool() const { const bool arg_used = std::ranges::any_of(m_argument_map, [](auto& it) { return it.second->m_is_used; }); const bool subparser_used = std::ranges::any_of(m_subparser_used, [](auto& it) { return it.second; }); return m_is_parsed && (arg_used || subparser_used); } // Parameter packing // Call add_argument with variadic number of string arguments template fn add_argument(Targs... f_args) -> Argument& { using array_of_sv = std::array; auto argument = m_optional_arguments.emplace(std::cend(m_optional_arguments), m_prefix_chars, array_of_sv { f_args... }); if (!argument->m_is_optional) m_positional_arguments.splice(std::cend(m_positional_arguments), m_optional_arguments, argument); argument->set_usage_newline_counter(m_usage_newline_counter); argument->set_group_idx(m_group_names.size()); index_argument(argument); return *argument; } class MutuallyExclusiveGroup { friend class ArgumentParser; public: MutuallyExclusiveGroup() = delete; ~MutuallyExclusiveGroup() = default; fn operator=(MutuallyExclusiveGroup&&)->MutuallyExclusiveGroup& = delete; explicit MutuallyExclusiveGroup(ArgumentParser& parent, const bool required = false) : m_parent(parent), m_required(required), m_elements({}) {} MutuallyExclusiveGroup(const MutuallyExclusiveGroup& other) = delete; fn operator=(const MutuallyExclusiveGroup& other)->MutuallyExclusiveGroup& = delete; MutuallyExclusiveGroup(MutuallyExclusiveGroup&& other) noexcept : m_parent(other.m_parent), m_required(other.m_required), m_elements(std::move(other.m_elements)) { other.m_elements.clear(); } template fn add_argument(Targs... f_args) -> Argument& { Argument& argument = m_parent.add_argument(std::forward(f_args)...); m_elements.push_back(&argument); argument.set_usage_newline_counter(m_parent.m_usage_newline_counter); argument.set_group_idx(m_parent.m_group_names.size()); return argument; } private: ArgumentParser& m_parent; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members) bool m_required = false; Vec m_elements; }; fn add_mutually_exclusive_group(bool required = false) -> MutuallyExclusiveGroup& { m_mutually_exclusive_groups.emplace_back(*this, required); return m_mutually_exclusive_groups.back(); } // Parameter packed add_parents method // Accepts a variadic number of ArgumentParser objects template fn add_parents(const Targs&... f_args) -> ArgumentParser& { for (const ArgumentParser& parent_parser : { std::ref(f_args)... }) { for (const Argument& argument : parent_parser.m_positional_arguments) { const auto it = m_positional_arguments.insert( std::cend(m_positional_arguments), argument ); index_argument(it); } for (const Argument& argument : parent_parser.m_optional_arguments) { const auto it = m_optional_arguments.insert(std::cend(m_optional_arguments), argument); index_argument(it); } } return *this; } // Ask for the next optional arguments to be displayed on a separate // line in usage() output. Only effective if set_usage_max_line_width() is // also used. fn add_usage_newline() -> ArgumentParser& { ++m_usage_newline_counter; return *this; } // Ask for the next optional arguments to be displayed in a separate section // in usage() and help (<< *this) output. // For usage(), this is only effective if set_usage_max_line_width() is // also used. fn add_group(String group_name) -> ArgumentParser& { m_group_names.emplace_back(std::move(group_name)); return *this; } fn add_description(String description) -> ArgumentParser& { m_description = std::move(description); return *this; } fn add_epilog(String epilog) -> ArgumentParser& { m_epilog = std::move(epilog); return *this; } // Add a un-documented/hidden alias for an argument. // Ideally we'd want this to be a method of Argument, but Argument // does not own its owing ArgumentParser. fn add_hidden_alias_for(const Argument& arg, const StringView alias) -> ArgumentParser& { for (auto it = m_optional_arguments.begin(); it != m_optional_arguments.end(); ++it) if (&(*it) == &arg) { m_argument_map.insert_or_assign(String(alias), it); return *this; } throw std::logic_error( "Argument is not an optional argument of this parser" ); } /* Getter for arguments and subparsers. * @throws std::logic_error in case of an invalid argument or subparser name */ template fn at(const StringView name) -> T& { if constexpr (std::is_same_v) return (*this)[name]; else { const String str_name(name); if (const auto subparser_it = m_subparser_map.find(str_name); subparser_it != m_subparser_map.end()) return subparser_it->second->get(); throw std::logic_error("No such subparser: " + str_name); } } fn set_prefix_chars(String prefix_chars) -> ArgumentParser& { m_prefix_chars = std::move(prefix_chars); return *this; } fn set_assign_chars(String assign_chars) -> ArgumentParser& { m_assign_chars = std::move(assign_chars); return *this; } /* Call parse_args_internal - which does all the work * Then, validate the parsed arguments * This variant is used mainly for testing * @throws std::runtime_error in case of any invalid argument */ // NOLINTNEXTLINE(misc-no-recursion) fn parse_args(const Vec& arguments) -> void { parse_args_internal(arguments); // Check if all arguments are parsed for (const auto& argument : m_argument_map | std::views::values) if (Result<> validation_result = argument->validate(); !validation_result) throw std::runtime_error(validation_result.error().message); // Check each mutually exclusive group and make sure // there are no constraint violations for (const MutuallyExclusiveGroup& group : m_mutually_exclusive_groups) { bool mutex_argument_used = false; const Argument* mutex_argument_it { nullptr }; for (const Argument* arg : group.m_elements) { if (!mutex_argument_used && arg->m_is_used) { mutex_argument_used = true; mutex_argument_it = arg; } else if (mutex_argument_used && arg->m_is_used) // Violation throw std::runtime_error("Argument '" + arg->get_usage_full() + "' not allowed with '" + mutex_argument_it->get_usage_full() + "'"); } if (!mutex_argument_used && group.m_required) { // at least one argument from the group is // required String argument_names {}; usize i = 0; const usize size = group.m_elements.size(); for (const Argument* arg : group.m_elements) { if (i + 1 == size) // last argument_names += String("'") + arg->get_usage_full() + String("' "); else argument_names += String("'") + arg->get_usage_full() + String("' or "); i += 1; } throw std::runtime_error("One of the arguments " + argument_names + "is required"); } } } /* Call parse_known_args_internal - which does all the work * Then, validate the parsed arguments * This variant is used mainly for testing * @throws std::runtime_error in case of any invalid argument */ fn parse_known_args(const Vec& arguments) -> Vec { Vec unknown_arguments = parse_known_args_internal(arguments); for (const auto& argument : m_argument_map | std::views::values) { if (Result<> validation_result = argument->validate(); !validation_result) throw std::runtime_error(validation_result.error().message); } return unknown_arguments; } /* Main entry point for parsing command-line arguments using this * ArgumentParser * @throws std::runtime_error in case of any invalid argument */ // NOLINTNEXTLINE(*-avoid-c-arrays) fn parse_args(const int argc, const char* const argv[]) -> void { parse_args({ argv, argv + argc }); } /* Main entry point for parsing command-line arguments using this * ArgumentParser * @throws std::runtime_error in case of any invalid argument */ // NOLINTNEXTLINE(*-avoid-c-arrays) fn parse_known_args(const int argc, const char* const argv[]) { return parse_known_args({ argv, argv + argc }); } /* Getter for options with default values. * @throws std::logic_error if parse_args() has not been previously called * @throws std::logic_error if there is no such option * @throws std::logic_error if the option has no value * @throws std::bad_any_cast if the option is not of type T */ template fn get(const StringView arg_name) const -> T { if (!m_is_parsed) throw std::logic_error("Nothing parsed, no arguments are available."); return (*this)[arg_name].get(); } /* Getter for options without default values. * @pre The option has no default value. * @throws std::logic_error if there is no such option * @throws std::bad_any_cast if the option is not of type T */ template fn present(const StringView arg_name) const -> Option { return (*this)[arg_name].present(); } /* Getter that returns true for user-supplied options. Returns false if not * user-supplied, even with a default value. */ [[nodiscard]] fn is_used(const StringView arg_name) const -> bool { return (*this)[arg_name].m_is_used; } /* Getter that returns true if a subcommand is used. */ [[nodiscard]] fn is_subcommand_used(const StringView subcommand_name) const -> bool { return m_subparser_used.at(String(subcommand_name)); } /* Getter that returns true if a subcommand is used. */ [[nodiscard]] fn is_subcommand_used(const ArgumentParser& subparser) const -> bool { return is_subcommand_used(subparser.m_program_name); } /* Indexing operator. Return a reference to an Argument object * Used in conjunction with Argument.operator== e.g., parser["foo"] == true * @throws std::logic_error in case of an invalid argument name */ fn operator[](const StringView arg_name) const->Argument& { String name(arg_name); auto it = m_argument_map.find(name); if (it != m_argument_map.end()) return *(it->second); if (!is_valid_prefix_char(arg_name.front())) { const char legal_prefix_char = get_any_valid_prefix_char(); const String prefix = String(1, legal_prefix_char); // "-" + arg_name name = prefix + name; it = m_argument_map.find(name); if (it != m_argument_map.end()) return *(it->second); // "--" + arg_name name = prefix + name; it = m_argument_map.find(name); if (it != m_argument_map.end()) return *(it->second); } throw std::logic_error("No such argument: " + String(arg_name)); } // Print help message friend fn operator<<(std::ostream& stream, const ArgumentParser& parser)->std::ostream& { stream.setf(std::ios_base::left); const usize longest_arg_length = parser.get_length_of_longest_argument(); stream << parser.usage() << "\n\n"; if (!parser.m_description.empty()) stream << parser.m_description << "\n\n"; const bool has_visible_positional_args = std::ranges::find_if(parser.m_positional_arguments, [](const Argument& argument) { return !argument.m_is_hidden; }) != parser.m_positional_arguments.end(); if (has_visible_positional_args) stream << "Positional arguments:\n"; for (const Argument& argument : parser.m_positional_arguments) if (!argument.m_is_hidden) { stream.width(static_cast(longest_arg_length)); stream << argument; } if (!parser.m_optional_arguments.empty()) stream << (!has_visible_positional_args ? "" : "\n") << "Optional arguments:\n"; for (const Argument& argument : parser.m_optional_arguments) if (argument.m_group_idx == 0 && !argument.m_is_hidden) { stream.width(static_cast(longest_arg_length)); stream << argument; } for (usize i_group = 0; i_group < parser.m_group_names.size(); ++i_group) { stream << "\n" << parser.m_group_names[i_group] << " (detailed usage):\n"; for (const Argument& argument : parser.m_optional_arguments) if (argument.m_group_idx == i_group + 1 && !argument.m_is_hidden) { stream.width(static_cast(longest_arg_length)); stream << argument; } } if (std::ranges::any_of(parser.m_subparser_map, [](auto& p) { return !p.second->get().m_suppress; })) { stream << (parser.m_positional_arguments.empty() ? (parser.m_optional_arguments.empty() ? "" : "\n") : "\n") << "Subcommands:\n"; for (const auto& [command, subparser] : parser.m_subparser_map) { if (subparser->get().m_suppress) continue; stream << std::setw(2) << " "; stream << std::setw(static_cast(longest_arg_length - 2)) << command; stream << " " << subparser->get().m_description << "\n"; } } if (!parser.m_epilog.empty()) { stream << '\n'; stream << parser.m_epilog << "\n\n"; } return stream; } // Format help message [[nodiscard]] fn help() const -> std::stringstream { std::stringstream out; out << *this; return out; } // Sets the maximum width for a line of the Usage message fn set_usage_max_line_width(const usize w) -> ArgumentParser& { this->m_usage_max_line_width = w; return *this; } // Asks to display arguments of mutually exclusive group on separate lines in // the Usage message fn set_usage_break_on_mutex() -> ArgumentParser& { this->m_usage_break_on_mutex = true; return *this; } // Format usage part of help only [[nodiscard]] fn usage() const -> String { std::stringstream stream; String curline("Usage: "); curline += this->m_parser_path; const bool multiline_usage = this->m_usage_max_line_width < (std::numeric_limits::max)(); const usize indent_size = curline.size(); const fn deal_with_options_of_group = [&](const usize group_idx) { bool found_options = false; // Add any options inline here const MutuallyExclusiveGroup* cur_mutex = nullptr; int usage_newline_counter = -1; for (const Argument& argument : this->m_optional_arguments) { if (argument.m_is_hidden) { continue; } if (multiline_usage) { if (argument.m_group_idx != group_idx) { continue; } if (usage_newline_counter != argument.m_usage_newline_counter) { if (usage_newline_counter >= 0) { if (curline.size() > indent_size) { stream << curline << '\n'; curline = String(indent_size, ' '); } } usage_newline_counter = argument.m_usage_newline_counter; } } found_options = true; const String arg_inline_usage = argument.get_inline_usage(); const MutuallyExclusiveGroup* arg_mutex = get_belonging_mutex(&argument); if ((cur_mutex != nullptr) && (arg_mutex == nullptr)) { curline += ']'; if (this->m_usage_break_on_mutex) { stream << curline << '\n'; curline = String(indent_size, ' '); } } else if ((cur_mutex == nullptr) && (arg_mutex != nullptr)) { if ((this->m_usage_break_on_mutex && curline.size() > indent_size) || curline.size() + 3 + arg_inline_usage.size() > this->m_usage_max_line_width) { stream << curline << '\n'; curline = String(indent_size, ' '); } curline += " ["; } else if ((cur_mutex != nullptr) && (arg_mutex != nullptr)) { if (cur_mutex != arg_mutex) { curline += ']'; if (this->m_usage_break_on_mutex || curline.size() + 3 + arg_inline_usage.size() > this->m_usage_max_line_width) { stream << curline << '\n'; curline = String(indent_size, ' '); } curline += " ["; } else { curline += '|'; } } cur_mutex = arg_mutex; if (curline.size() != indent_size && curline.size() + 1 + arg_inline_usage.size() > this->m_usage_max_line_width) { stream << curline << '\n'; curline = String(indent_size, ' '); curline += " "; } else if (cur_mutex == nullptr) { curline += " "; } curline += arg_inline_usage; } if (cur_mutex != nullptr) { curline += ']'; } return found_options; }; if (const bool found_options = deal_with_options_of_group(0); found_options && multiline_usage && !this->m_positional_arguments.empty()) { stream << curline << '\n'; curline = String(indent_size, ' '); } // Put positional arguments after the optionals for (const Argument& argument : this->m_positional_arguments) { if (argument.m_is_hidden) continue; const String pos_arg = !argument.m_metavar.empty() ? argument.m_metavar : argument.m_names.front(); if (curline.size() + 1 + pos_arg.size() > this->m_usage_max_line_width) { stream << curline << '\n'; curline = String(indent_size, ' '); } curline += " "; if (argument.m_num_args_range.get_min() == 0 && !argument.m_num_args_range.is_right_bounded()) { curline += "["; curline += pos_arg; curline += "]..."; } else if (argument.m_num_args_range.get_min() == 1 && !argument.m_num_args_range.is_right_bounded()) { curline += pos_arg; curline += "..."; } else curline += pos_arg; } if (multiline_usage) // Display options of other groups for (usize i = 0; i < m_group_names.size(); ++i) { stream << curline << '\n' << '\n'; stream << m_group_names[i] << ":" << '\n'; curline = String(indent_size, ' '); deal_with_options_of_group(i + 1); } stream << curline; // Put subcommands after positional arguments if (!m_subparser_map.empty()) { stream << " {"; usize i { 0 }; for (const auto& [command, subparser] : m_subparser_map) { if (subparser->get().m_suppress) continue; if (i == 0) stream << command; else stream << "," << command; ++i; } stream << "}"; } return stream.str(); } fn add_subparser(ArgumentParser& parser) -> void { parser.m_parser_path = m_program_name + " " + parser.m_program_name; auto it = m_subparsers.emplace(std::cend(m_subparsers), parser); m_subparser_map.insert_or_assign(parser.m_program_name, it); m_subparser_used.insert_or_assign(parser.m_program_name, false); } fn set_suppress(const bool suppress) -> void { m_suppress = suppress; } protected: fn get_belonging_mutex(const Argument* arg) const -> const MutuallyExclusiveGroup* { for (const MutuallyExclusiveGroup& mutex : m_mutually_exclusive_groups) if (std::ranges::find(mutex.m_elements, arg) != mutex.m_elements.end()) return &mutex; return nullptr; } [[nodiscard]] fn is_valid_prefix_char(const char c) const -> bool { return m_prefix_chars.contains(c); } [[nodiscard]] fn get_any_valid_prefix_char() const -> char { return m_prefix_chars[0]; } /* * Pre-process this argument list. Anything starting with "--", that * contains an =, where the prefix before the = has an entry in the * options table, should be split. */ [[nodiscard]] fn preprocess_arguments(const Vec& raw_arguments) const -> Vec { Vec arguments {}; for (const String& arg : raw_arguments) { const auto argument_starts_with_prefix_chars = [this](const String& a) -> bool { if (!a.empty()) { // Windows-style // if '/' is a legal prefix char // then allow single '/' followed by argument name, followed by an // assign char, e.g., ':' e.g., 'test.exe /A:Foo' if (is_valid_prefix_char('/')) { if (is_valid_prefix_char(a[0])) return true; } else // Slash '/' is not a legal prefix char // For all other characters, only support long arguments // i.e., the argument must start with 2 prefix chars, e.g, // '--foo' e,g, './test --foo=Bar -DARG=yes' if (a.size() > 1) return (is_valid_prefix_char(a[0]) && is_valid_prefix_char(a[1])); } return false; }; // Check that: // - We don't have an argument named exactly this // - The argument starts with a prefix char, e.g., "--" // - The argument contains an assign char, e.g., "=" if (const usize assign_char_pos = arg.find_first_of(m_assign_chars); !m_argument_map.contains(arg) && argument_starts_with_prefix_chars(arg) && assign_char_pos != String::npos) // Get the name of the potential option, and check it exists if (String opt_name = arg.substr(0, assign_char_pos); m_argument_map.contains(opt_name)) { // This is the name of an option! Split it into two parts arguments.push_back(std::move(opt_name)); arguments.push_back(arg.substr(assign_char_pos + 1)); continue; } // If we've fallen through to here, then it's a standard argument arguments.push_back(arg); } return arguments; } /* * @throws std::runtime_error in case of any invalid argument */ // NOLINTNEXTLINE(misc-no-recursion) fn parse_args_internal(const Vec& raw_arguments) -> void { Vec arguments = preprocess_arguments(raw_arguments); if (m_program_name.empty() && !arguments.empty()) m_program_name = arguments.front(); auto end = std::end(arguments); auto positional_argument_it = std::begin(m_positional_arguments); for (auto it = std::next(std::begin(arguments)); it != end;) { const String& current_argument = *it; if (Argument::is_positional(current_argument, m_prefix_chars)) { if (positional_argument_it == std::end(m_positional_arguments)) { // Check sub-parsers if (const auto subparser_it = m_subparser_map.find(current_argument); subparser_it != m_subparser_map.end()) { // build list of remaining args const Vec unprocessed_arguments = Vec(it, end); // invoke subparser m_is_parsed = true; m_subparser_used[current_argument] = true; subparser_it->second->get().parse_args( unprocessed_arguments ); return; } if (m_positional_arguments.empty()) { // Ask the user if they argument they provided was a typo // for some sub-parser, // e.g., user provided `git totes` instead of `git notes` if (!m_subparser_map.empty()) throw std::runtime_error( "Failed to parse '" + current_argument + "', did you mean '" + String { details::get_most_similar_string( m_subparser_map, current_argument ) } + "'" ); // Ask the user if they meant to use a specific optional argument if (!m_optional_arguments.empty()) { for (const Argument& opt : m_optional_arguments) { if (!opt.m_implicit_value.has_value()) { // not a flag, requires a value if (!opt.m_is_used) { throw std::runtime_error( "Zero positional arguments expected, did you mean " + opt.get_usage_full() ); } } } throw std::runtime_error("Zero positional arguments expected"); } throw std::runtime_error("Zero positional arguments expected"); } throw std::runtime_error( "Maximum number of positional arguments " "exceeded, failed to parse '" + current_argument + "'" ); } const auto argument = positional_argument_it++; // Deal with the situation of ... if (argument->m_num_args_range.get_min() == 1 && argument->m_num_args_range.get_max() == (std::numeric_limits::max)() && positional_argument_it != std::end(m_positional_arguments) && std::next(positional_argument_it) == std::end(m_positional_arguments) && positional_argument_it->m_num_args_range.get_min() == 1 && positional_argument_it->m_num_args_range.get_max() == 1) { if (std::next(it) != end) { positional_argument_it->consume(std::prev(end), end); end = std::prev(end); } else throw std::runtime_error("Missing " + positional_argument_it->m_names.front()); } it = argument->consume(it, end); continue; } auto arg_map_it = m_argument_map.find(current_argument); if (arg_map_it != m_argument_map.end()) { const auto argument = arg_map_it->second; it = argument->consume(std::next(it), end, arg_map_it->first); } else if (const String& compound_arg = current_argument; compound_arg.size() > 1 && is_valid_prefix_char(compound_arg[0]) && !is_valid_prefix_char(compound_arg[1])) { ++it; for (usize j = 1; j < compound_arg.size(); j++) { const String hypothetical_arg = { '-', compound_arg[j] }; auto arg_map_it2 = m_argument_map.find(hypothetical_arg); if (arg_map_it2 != m_argument_map.end()) { const auto argument = arg_map_it2->second; it = argument->consume(it, end, arg_map_it2->first); } else throw std::runtime_error("Unknown argument: " + current_argument); } } else throw std::runtime_error("Unknown argument: " + current_argument); } m_is_parsed = true; } /* * Like parse_args_internal but collects unused args into a vector */ // NOLINTNEXTLINE(misc-no-recursion) fn parse_known_args_internal(const Vec& raw_arguments) -> Vec { Vec arguments = preprocess_arguments(raw_arguments); Vec unknown_arguments {}; if (m_program_name.empty() && !arguments.empty()) m_program_name = arguments.front(); const auto end = std::end(arguments); auto positional_argument_it = std::begin(m_positional_arguments); for (auto it = std::next(std::begin(arguments)); it != end;) { const String& current_argument = *it; if (Argument::is_positional(current_argument, m_prefix_chars)) { if (positional_argument_it == std::end(m_positional_arguments)) { // Check sub-parsers if (auto subparser_it = m_subparser_map.find(current_argument); subparser_it != m_subparser_map.end()) { // build list of remaining args const Vec unprocessed_arguments = Vec(it, end); // invoke subparser m_is_parsed = true; m_subparser_used[current_argument] = true; return subparser_it->second->get().parse_known_args_internal( unprocessed_arguments ); } // save current argument as unknown and go to next argument unknown_arguments.push_back(current_argument); ++it; } else { // current argument is the value of a positional argument // consume it const auto argument = positional_argument_it++; it = argument->consume(it, end); } continue; } auto arg_map_it = m_argument_map.find(current_argument); if (arg_map_it != m_argument_map.end()) { const auto argument = arg_map_it->second; it = argument->consume(std::next(it), end, arg_map_it->first); } else if (const String& compound_arg = current_argument; compound_arg.size() > 1 && is_valid_prefix_char(compound_arg[0]) && !is_valid_prefix_char(compound_arg[1])) { ++it; for (usize j = 1; j < compound_arg.size(); j++) { const String hypothetical_arg = { '-', compound_arg[j] }; auto arg_map_it2 = m_argument_map.find(hypothetical_arg); if (arg_map_it2 != m_argument_map.end()) { const auto argument = arg_map_it2->second; it = argument->consume(it, end, arg_map_it2->first); } else { unknown_arguments.push_back(current_argument); break; } } } else { // current argument is an optional-like argument that is unknown // save it and move to next argument unknown_arguments.push_back(current_argument); ++it; } } m_is_parsed = true; return unknown_arguments; } // Used by print_help. [[nodiscard]] fn get_length_of_longest_argument() const -> usize { if (m_argument_map.empty()) return 0; usize max_size = 0; for (const auto& argument : m_argument_map | std::views::values) max_size = std::max(max_size, argument->get_arguments_length()); for (const String& command : m_subparser_map | std::views::keys) max_size = std::max(max_size, command.size()); return max_size; } using argument_it = std::list::iterator; using mutex_group_it = Vec::iterator; using argument_parser_it = std::list>::iterator; fn index_argument(argument_it it) -> void { for (const String& name : std::as_const(it->m_names)) m_argument_map.insert_or_assign(name, it); } private: String m_program_name; String m_version; String m_description; String m_epilog; bool m_exit_on_default_arguments = true; String m_prefix_chars { "-" }; String m_assign_chars { "=" }; bool m_is_parsed = false; std::list m_positional_arguments; std::list m_optional_arguments; Map m_argument_map; String m_parser_path; std::list> m_subparsers; Map m_subparser_map; Map m_subparser_used; Vec m_mutually_exclusive_groups; bool m_suppress = false; usize m_usage_max_line_width = (std::numeric_limits::max)(); bool m_usage_break_on_mutex = false; int m_usage_newline_counter = 0; Vec m_group_names; }; } // namespace argparse // NOLINTEND(readability-identifier-naming, readability-identifier-length)