diff --git a/include/argon/argument.hpp b/include/argon/argument.hpp index fd0e67a6..8bc60f32 100644 --- a/include/argon/argument.hpp +++ b/include/argon/argument.hpp @@ -703,10 +703,40 @@ class argument : public detail::typed_argument_base { )); } - if (not this->_is_valid_choice(value)) - throw parsing_failure(std::format( - "Value `{}` is not a valid choice for argument [{}].", str_value, this->_name.str() - )); + if (not this->_is_valid_choice(value)) { + if constexpr (traits::c_writable) { + constexpr std::size_t max_display_choices = 5ull; + + std::string choices_str = util::join( + this->_choices | std::views::take(max_display_choices) + | std::views::transform([](const auto& c) { + return std::format("'{}'", util::as_string(c)); + }) + ); + + if (this->_choices.size() > max_display_choices) { + choices_str = std::format( + "{}, ... and {} more", + choices_str, + this->_choices.size() - max_display_choices + ); + } + + throw parsing_failure(std::format( + "Value `{}` is not a valid choice for argument [{}]. Valid choices are: {}", + str_value, + this->_name.str(), + choices_str + )); + } + else { + throw parsing_failure(std::format( + "Value `{}` is not a valid choice for argument [{}].", + str_value, + this->_name.str() + )); + } + } const auto apply_visitor = action::util::apply_visitor{value}; for (const auto& action : this->_value_actions) diff --git a/include/argon/argument_parser.hpp b/include/argon/argument_parser.hpp index 0045737e..f1d556f2 100644 --- a/include/argon/argument_parser.hpp +++ b/include/argon/argument_parser.hpp @@ -1338,7 +1338,7 @@ class argument_parser { * @param value The value to be set for the current argument. * @param state The current parsing state. */ - void _set_argument_value(const std::string_view value, parsing_state& state) noexcept { + void _set_argument_value(const std::string_view value, parsing_state& state) { if (state.curr_arg->set_value(std::string(value))) return; // argument still accepts values diff --git a/include/argon/util/string.hpp b/include/argon/util/string.hpp index eb249591..68f55ade 100644 --- a/include/argon/util/string.hpp +++ b/include/argon/util/string.hpp @@ -24,7 +24,7 @@ namespace argon::util { * @ingroup util */ template -[[nodiscard]] std::string as_string(const T& value) noexcept { +[[nodiscard]] std::string as_string(T&& value) noexcept { std::ostringstream oss; oss << value; return oss.str(); @@ -45,7 +45,7 @@ template */ template requires(traits::c_writable>) -[[nodiscard]] std::string join(const R& range, const std::string_view delimiter = ", ") { +[[nodiscard]] std::string join(R&& range, const std::string_view delimiter = ", ") { std::ostringstream oss; auto it = std::ranges::begin(range); diff --git a/tests/source/test_optional_argument.cpp b/tests/source/test_optional_argument.cpp index d779f995..321d10a6 100644 --- a/tests/source/test_optional_argument.cpp +++ b/tests/source/test_optional_argument.cpp @@ -526,9 +526,10 @@ TEST_CASE_FIXTURE( REQUIRE_THROWS_WITH_AS( set_value(sut, invalid_choice), - invalid_choice_msg(arg_name_primary, as_string(invalid_choice)).c_str(), + doctest::Contains(invalid_choice_msg(arg_name_primary, as_string(invalid_choice)).c_str()), parsing_failure ); + CHECK_FALSE(has_value(sut)); } diff --git a/tests/source/test_positional_argument.cpp b/tests/source/test_positional_argument.cpp index c81d1353..9c8a6af9 100644 --- a/tests/source/test_positional_argument.cpp +++ b/tests/source/test_positional_argument.cpp @@ -401,7 +401,7 @@ TEST_CASE_FIXTURE( REQUIRE_THROWS_WITH_AS( set_value(sut, invalid_choice), - invalid_choice_msg(arg_name, std::to_string(invalid_choice)).c_str(), + doctest::Contains(invalid_choice_msg(arg_name, as_string(invalid_choice)).c_str()), parsing_failure ); CHECK_FALSE(has_value(sut));