/** * @file argparse.hpp * @brief Argument Parser for Modern C++ * @author Pranav Srinivas Kumar * @copyright Copyright (c) 2019-2022 Pranav Srinivas Kumar and other contributors * @license MIT License * * A powerful, flexible, and easy-to-use command-line argument parser for modern C++. * Provides a simple interface for defining, parsing, and validating command-line arguments. * Supports both positional and optional arguments, subcommands, and more. */ #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 #endif #include "src/util/defs.hpp" #include "src/util/error.hpp" #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, modernize-use-nullptr) namespace argparse { using namespace util::types; using util::error::DracError, util::error::DracErrorCode; namespace details { /** * @brief Trait to check if a type has container-like properties * @tparam T The type to check * @tparam void SFINAE parameter */ template struct HasContainerTraits : std::false_type {}; /** * @brief Specialization for std::string - not considered a container */ template <> struct HasContainerTraits : std::false_type {}; /** * @brief Specialization for std::string_view - not considered a container */ template <> struct HasContainerTraits : std::false_type {}; /** * @brief Specialization for types that have container-like properties * @tparam T The type to check */ template struct HasContainerTraits().begin()), decltype(std::declval().end()), decltype(std::declval().size())>> : std::true_type {}; /** * @brief Convenience variable template for checking if a type is a container * @tparam T The type to check */ template inline constexpr bool IsContainer = HasContainerTraits::value; /** * @brief Trait to check if a type can be streamed to std::ostream * @tparam T The type to check * @tparam void SFINAE parameter */ template struct HasStreamableTraits : std::false_type {}; /** * @brief Specialization for types that can be streamed to std::ostream * @tparam T The type to check */ template struct HasStreamableTraits() << std::declval())>> : std::true_type {}; /** * @brief Convenience variable template for checking if a type is streamable * @tparam T The type to check */ template inline constexpr bool IsStreamable = HasStreamableTraits::value; /** * @brief Maximum number of elements to show when representing a container */ constexpr usize repr_max_container_size = 5; /** * @brief Concept to check if a type can be formatted using std::format * @tparam T The type to check * @tparam CharT The character type for formatting */ template concept Formattable = requires(const T& t, std::basic_format_context ctx) { std::formatter, CharT>().format(t, ctx); }; /** * @brief Convert a value to its string representation * @tparam T The type of the value to convert * @param val The value to convert * @return String representation of the value */ 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 ""; } /** * @brief Radix constants for number parsing */ constexpr i32 radix_2 = 2; constexpr i32 radix_8 = 8; constexpr i32 radix_10 = 10; constexpr i32 radix_16 = 16; /** * @brief Helper function to apply a function with an additional argument * @tparam F Function type * @tparam Tuple Tuple type containing the base arguments * @tparam Extra Type of the additional argument * @tparam I... Index sequence * @param f Function to apply * @param t Tuple of base arguments * @param x Additional argument * @return Result of applying the function */ 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)); } /** * @brief Wrapper for apply_plus_one_impl that creates the index sequence * @tparam F Function type * @tparam Tuple Tuple type containing the base arguments * @tparam Extra Type of the additional argument * @param f Function to apply * @param t Tuple of base arguments * @param x Additional argument * @return Result of applying the function */ 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>> {} ); } /** * @brief Get a tuple of pointers to the start and end of a string view * @param s The string view to get pointers for * @return Tuple of (start pointer, end pointer) */ constexpr fn pointer_range(const StringView s) noexcept -> std::tuple { return { s.data(), s.data() + s.size() }; } /** * @brief Check if a string view starts with a given prefix * @tparam CharT Character type * @tparam Traits Character traits type * @param prefix The prefix to check for * @param s The string to check * @return true if s starts with prefix, false otherwise */ template constexpr fn starts_with(std::basic_string_view prefix, std::basic_string_view s) noexcept -> bool { return s.substr(0, prefix.size()) == prefix; } /** * @brief Format flags for number parsing */ enum class chars_format : u8 { scientific = 0xf1, ///< Scientific notation (e.g., 1.23e4) fixed = 0xf2, ///< Fixed point notation (e.g., 123.45) hex = 0xf4, ///< Hexadecimal notation (e.g., 0x1a) binary = 0xf8, ///< Binary notation (e.g., 0b1010) general = fixed | scientific ///< General format (either fixed or scientific) }; /** * @brief Result of checking for binary prefix */ struct ConsumeBinaryPrefixResult { bool is_binary; ///< Whether the string had a binary prefix StringView rest; ///< The string after removing the prefix }; /** * @brief Check if a string starts with a binary prefix and remove it * @param s The string to check * @return Result containing whether a binary prefix was found and the remaining string */ 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 }; } /** * @brief Result of checking for hexadecimal prefix */ struct ConsumeHexPrefixResult { bool is_hexadecimal; ///< Whether the string had a hex prefix StringView rest; ///< The string after removing the prefix }; using namespace std::literals; /** * @brief Check if a string starts with a hexadecimal prefix and remove it * @param s The string to check * @return Result containing whether a hex prefix was found and the remaining string */ 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 }; } /** * @brief Parse a string into a number using std::from_chars * @tparam T The type to parse into * @tparam Param The radix or format to use * @param s The string to parse * @return Result containing the parsed number or an error */ 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(DracError(DracErrorCode::ParseError, std::format("pattern '{}' does not match to the end", String(s)))); } if (ec == std::errc::invalid_argument) return Err(DracError(DracErrorCode::InvalidArgument, std::format("pattern '{}' not found", String(s)))); if (ec == std::errc::result_out_of_range) return Err(DracError(DracErrorCode::ParseError, std::format("'{}' not representable", String(s)))); return Err(DracError(DracErrorCode::InternalError, std::format("Unknown parsing error for '{}'", String(s)))); } /** * @brief Functor for parsing numbers with a specific radix * @tparam T The type to parse into * @tparam Param The radix to use (defaults to 0 for automatic detection) */ template struct parse_number { /** * @brief Parse a string into a number * @param s The string to parse * @return Result containing the parsed number or an error */ static fn operator()(const StringView s)->Result { return do_from_chars(s); } }; /** * @brief Specialization for binary number parsing * @tparam T The type to parse into */ template struct parse_number { /** * @brief Parse a binary string into a number * @param s The string to parse (must start with 0b or 0B) * @return Result containing the parsed number or an error */ static fn operator()(const StringView s)->Result { if (auto [ok, rest] = consume_binary_prefix(s); ok) return do_from_chars(rest); return Err(DracError(DracErrorCode::InvalidArgument, "pattern not found")); } }; /** * @brief Specialization for hexadecimal number parsing * @tparam T The type to parse into */ template struct parse_number { /** * @brief Parse a hexadecimal string into a number * @param s The string to parse (may start with 0x or 0X) * @return Result containing the parsed number or an error */ 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(DracError(DracErrorCode::InternalError, std::format("Inconsistent hex prefix detection for '{}'", String(s)))); } else result = do_from_chars(s); if (!result) return Err(DracError(result.error().code, std::format("Failed to parse '{}' as hexadecimal: {}", String(s), result.error().message))); return result; } }; /** * @brief Specialization for automatic number format detection * @tparam T The type to parse into */ template struct parse_number { /** * @brief Parse a string into a number, automatically detecting the format * @param s The string to parse * @return Result containing the parsed number or an error * * Supports: * - Hexadecimal (0x/0X prefix) * - Binary (0b/0B prefix) * - Octal (0 prefix) * - Decimal (no prefix) */ 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(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(DracError(result.error().code, std::format("Failed to parse '{}' as binary: {}", String(s), result.error().message))); return result; } if (starts_with("0"sv, s)) { Result result = do_from_chars(s); if (!result) return Err(DracError(result.error().code, std::format("Failed to parse '{}' as octal: {}", String(s), result.error().message))); return result; } Result result = do_from_chars(s); if (!result) return Err(DracError(result.error().code, std::format("Failed to parse '{}' as decimal integer: {}", String(s), result.error().message))); return result; } }; /** * @brief Custom string to number conversion functions * @tparam T The type to convert to */ 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; /** * @brief Parse a string into a floating point number * @tparam T The floating point type to parse into * @param s The string to parse * @return Result containing the parsed number or an error */ template fn do_strtod(const String& s) -> Result { if (isspace(static_cast(s[0])) || s[0] == '+') return Err(DracError(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(DracError(DracErrorCode::ParseError, std::format("pattern '{}' does not match to the end", s))); } if (errno == ERANGE) return Err(DracError(DracErrorCode::ParseError, std::format("'{}' not representable", s))); return Err(DracError(std::error_code(errno, std::system_category()))); } /** * @brief Specialization for general floating point format * @tparam T The floating point type to parse into */ template struct parse_number { /** * @brief Parse a string into a floating point number in general format * @param s The string to parse * @return Result containing the parsed number or an error */ fn operator()(const String& s)->Result { if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::general does not parse hexfloat")); if (auto [is_bin, rest] = consume_binary_prefix(s); is_bin) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::general does not parse binfloat")); Result result = do_strtod(s); if (!result) return Err(DracError(result.error().code, std::format("Failed to parse '{}' as number: {}", s, result.error().message))); return result; } }; /** * @brief Specialization for hexadecimal floating point format * @tparam T The floating point type to parse into */ template struct parse_number { /** * @brief Parse a string into a floating point number in hexadecimal format * @param s The string to parse (must start with 0x or 0X) * @return Result containing the parsed number or an error */ fn operator()(const String& s)->Result { if (auto [is_hex, rest] = consume_hex_prefix(s); !is_hex) return Err(DracError(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(DracError(DracErrorCode::InvalidArgument, "chars_format::hex does not parse binfloat")); Result result = do_strtod(s); if (!result) return Err(DracError(result.error().code, std::format("Failed to parse '{}' as hexadecimal float: {}", s, result.error().message))); return result; } }; /** * @brief Specialization for binary floating point format * @tparam T The floating point type to parse into */ template struct parse_number { /** * @brief Parse a string into a floating point number in binary format * @param s The string to parse (must start with 0b or 0B) * @return Result containing the parsed number or an error */ fn operator()(const String& s)->Result { if (auto [is_hex, rest] = consume_hex_prefix(s); is_hex) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::binary does not parse hexfloat")); if (auto [is_bin, rest] = consume_binary_prefix(s); !is_bin) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::binary requires binfloat format (e.g., 0b1.01p2)")); Result result = do_strtod(s); if (!result) return Err(DracError(result.error().code, std::format("Failed to parse '{}' as binary float: {}", s, result.error().message))); return result; } }; /** * @brief Specialization for scientific floating point format * @tparam T The floating point type to parse into */ template struct parse_number { /** * @brief Parse a string into a floating point number in scientific notation * @param s The string to parse (must contain e or E) * @return Result containing the parsed number or an error */ fn operator()(const String& s)->Result { if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::scientific does not parse hexfloat")); if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::scientific does not parse binfloat")); if (s.find_first_of("eE") == String::npos) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::scientific requires exponent part")); Result result = do_strtod(s); if (!result) return Err(DracError(result.error().code, std::format("Failed to parse '{}' as scientific notation: {}", s, result.error().message))); return result; } }; /** * @brief Specialization for fixed point floating point format * @tparam T The floating point type to parse into */ template struct parse_number { /** * @brief Parse a string into a floating point number in fixed point notation * @param s The string to parse (must not contain e or E) * @return Result containing the parsed number or an error */ fn operator()(const String& s)->Result { if (const auto [is_hex, rest] = consume_hex_prefix(s); is_hex) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::fixed does not parse hexfloat")); if (const auto [is_bin, rest] = consume_binary_prefix(s); is_bin) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::fixed does not parse binfloat")); if (s.find_first_of("eE") != String::npos) return Err(DracError(DracErrorCode::InvalidArgument, "chars_format::fixed does not parse exponent part")); Result result = do_strtod(s); if (!result) return Err(DracError(result.error().code, std::format("Failed to parse '{}' as fixed notation: {}", s, result.error().message))); return result; } }; /** * @brief Concept to check if a type can be converted to a string * @tparam T The type to check */ template concept ToStringConvertible = std::convertible_to || std::convertible_to || requires(const T& t) { std::format("{}", t); }; /** * @brief Join a range of strings with a separator * @tparam StrIt Iterator type for the string range * @param first Iterator to the first string * @param last Iterator past the last string * @param separator The separator to use between strings * @return The joined string */ 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(); } /** * @brief Trait to check if a type can be converted using std::to_string * @tparam T The type to check */ template struct can_invoke_to_string { /** * @brief SFINAE test for std::to_string support * @tparam U The type to test */ template // ReSharper disable CppFunctionIsNotImplemented static fn test(int) -> decltype(std::to_string(std::declval()), std::true_type {}); /** * @brief Fallback for types without std::to_string support * @tparam U The type to test */ template static fn test(...) -> std::false_type; // ReSharper restore CppFunctionIsNotImplemented static constexpr bool value = decltype(test(0))::value; }; /** * @brief Trait to check if a type is supported for choice arguments * @tparam T The type to check */ 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; }; /** * @brief Calculate the Levenshtein distance between two strings * @tparam StringType The string type to use * @param s1 First string * @param s2 Second string * @return The Levenshtein distance between s1 and s2 */ 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()]; } /** * @brief Find the most similar string in a map to a given input * @tparam MapType The map-like container type * @tparam ValueType The value type of the map * @param map The map to search in * @param input The input string to find matches for * @return The most similar string from the map */ template fn get_most_similar_string(const MapType& 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; } /** * @brief Trait to check if a type is a specialization of a template * @tparam Test The type to check * @tparam Ref The template to check against */ template class Ref> struct is_specialization : std::false_type {}; /** * @brief Specialization for when Test is a specialization of Ref * @tparam Ref The template * @tparam Args The template arguments */ template