#277 Added in-built support for string_type choices

This commit is contained in:
Pranav Srinivas Kumar 2023-10-27 09:16:25 -05:00
parent 9fe48c74e4
commit 9bb553b882
13 changed files with 204 additions and 50 deletions

View File

@ -509,10 +509,12 @@ public:
m_num_args_range = NArgsRange{0, 1};
break;
case nargs_pattern::any:
m_num_args_range = NArgsRange{0, (std::numeric_limits<std::size_t>::max)()};
m_num_args_range =
NArgsRange{0, (std::numeric_limits<std::size_t>::max)()};
break;
case nargs_pattern::at_least_one:
m_num_args_range = NArgsRange{1, (std::numeric_limits<std::size_t>::max)()};
m_num_args_range =
NArgsRange{1, (std::numeric_limits<std::size_t>::max)()};
break;
}
return *this;
@ -523,6 +525,80 @@ public:
return nargs(nargs_pattern::any);
}
void add_choice(const std::string &choice) {
if (!m_choices.has_value()) {
/// create it
m_choices = std::vector<std::string>{};
}
m_choices.value().push_back(choice);
}
Argument &choices() {
if (!m_choices.has_value()) {
throw std::runtime_error("Zero choices provided");
}
return *this;
}
template <typename... T>
Argument &choices(const std::string &first, T &...rest) {
add_choice(first);
choices(rest...);
return *this;
}
template <typename... T> Argument &choices(const char *first, T &...rest) {
add_choice(first);
choices(rest...);
return *this;
}
void find_default_value_in_choices_or_throw() const {
const auto &choices = m_choices.value();
if (m_default_value.has_value()) {
if (std::find(choices.begin(), choices.end(), m_default_value_repr) ==
choices.end()) {
// provided arg not in list of allowed choices
// report error
std::string choices_as_csv =
std::accumulate(choices.begin(), choices.end(), std::string(),
[](const std::string &a, const std::string &b) {
return a + (a.empty() ? "" : ", ") + b;
});
throw std::runtime_error(
std::string{"Invalid default value "} + m_default_value_repr +
" - allowed options: {" + choices_as_csv + "}");
}
}
}
template <typename Iterator>
void find_value_in_choices_or_throw(Iterator it) const {
const auto &choices = m_choices.value();
if (std::find(choices.begin(), choices.end(), *it) == choices.end()) {
// provided arg not in list of allowed choices
// report error
std::string choices_as_csv =
std::accumulate(choices.begin(), choices.end(), std::string(),
[](const std::string &a, const std::string &b) {
return a + (a.empty() ? "" : ", ") + b;
});
throw std::runtime_error(std::string{"Invalid argument "} +
details::repr(*it) + " - allowed options: {" +
choices_as_csv + "}");
}
}
template <typename Iterator>
Iterator consume(Iterator start, Iterator end,
std::string_view used_name = {}) {
@ -532,6 +608,14 @@ public:
m_is_used = true;
m_used_name = used_name;
if (m_choices.has_value()) {
// Check each value in (start, end) and make sure
// it is in the list of allowed choices/options
for (auto it = start; it != end; ++it) {
find_value_in_choices_or_throw(it);
}
}
const auto num_args_max = m_num_args_range.get_max();
const auto num_args_min = m_num_args_range.get_min();
std::size_t dist = 0;
@ -602,6 +686,12 @@ public:
throw_nargs_range_validation_error();
}
}
if (m_choices.has_value()) {
// Make sure the default value (if provided)
// is in the list of choices
find_default_value_in_choices_or_throw();
}
}
std::string get_inline_usage() const {
@ -738,8 +828,7 @@ public:
using ValueType = typename T::value_type;
auto lhs = get<T>();
return std::equal(std::begin(lhs), std::end(lhs), std::begin(rhs),
std::end(rhs),
[](const auto &a, const auto &b) {
std::end(rhs), [](const auto &a, const auto &b) {
return std::any_cast<const ValueType &>(a) == b;
});
}
@ -1064,6 +1153,7 @@ private:
std::any m_default_value;
std::string m_default_value_repr;
std::any m_implicit_value;
std::optional<std::vector<std::string>> m_choices{std::nullopt};
using valued_action = std::function<std::any(const std::string &)>;
using void_action = std::function<void(const std::string &)>;
std::variant<valued_action, void_action> m_action{
@ -1152,16 +1242,11 @@ public:
}
explicit operator bool() const {
auto arg_used = std::any_of(m_argument_map.cbegin(),
m_argument_map.cend(),
[](auto &it) {
return it.second->m_is_used;
});
auto subparser_used = std::any_of(m_subparser_used.cbegin(),
m_subparser_used.cend(),
[](auto &it) {
return it.second;
});
auto arg_used = std::any_of(m_argument_map.cbegin(), m_argument_map.cend(),
[](auto &it) { return it.second->m_is_used; });
auto subparser_used =
std::any_of(m_subparser_used.cbegin(), m_subparser_used.cend(),
[](auto &it) { return it.second; });
return m_is_parsed && (arg_used || subparser_used);
}
@ -1215,8 +1300,7 @@ public:
/* Getter for arguments and subparsers.
* @throws std::logic_error in case of an invalid argument or subparser name
*/
template <typename T = Argument>
T& at(std::string_view name) {
template <typename T = Argument> T &at(std::string_view name) {
if constexpr (std::is_same_v<T, Argument>) {
return (*this)[name];
} else {
@ -1692,7 +1776,8 @@ private:
}
std::size_t max_size = 0;
for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) {
max_size = std::max<std::size_t>(max_size, argument->get_arguments_length());
max_size =
std::max<std::size_t>(max_size, argument->get_arguments_length());
}
for ([[maybe_unused]] const auto &[command, unused] : m_subparser_map) {
max_size = std::max<std::size_t>(max_size, command.size());

View File

@ -29,6 +29,7 @@ file(GLOB ARGPARSE_TEST_SOURCES
test_append.cpp
test_as_container.cpp
test_bool_operator.cpp
test_choices.cpp
test_compound_arguments.cpp
test_container_arguments.cpp
test_const_correct.cpp

View File

@ -5,8 +5,8 @@ import argparse;
#endif
#include <doctest.hpp>
#include <vector>
#include <string>
#include <vector>
using doctest::test_suite;

View File

@ -28,8 +28,8 @@ TEST_CASE("Get argument with .at()" * test_suite("as_container")) {
SUBCASE("with unknown argument") {
program.parse_args({"test"});
REQUIRE_THROWS_WITH_AS(program.at("--folder"),
"No such argument: --folder", std::logic_error);
REQUIRE_THROWS_WITH_AS(program.at("--folder"), "No such argument: --folder",
std::logic_error);
}
}
@ -44,7 +44,8 @@ TEST_CASE("Get subparser with .at()" * test_suite("as_container")) {
SUBCASE("and its argument") {
program.parse_args({"test", "walk", "4km/h"});
REQUIRE(&(program.at<argparse::ArgumentParser>("walk")) == &walk_cmd);
REQUIRE(&(program.at<argparse::ArgumentParser>("walk").at("speed")) == &speed);
REQUIRE(&(program.at<argparse::ArgumentParser>("walk").at("speed")) ==
&speed);
REQUIRE(program.at<argparse::ArgumentParser>("walk").is_used("speed"));
}

View File

@ -8,8 +8,7 @@ import argparse;
using doctest::test_suite;
TEST_CASE("ArgumentParser in bool context" *
test_suite("argument_parser")) {
TEST_CASE("ArgumentParser in bool context" * test_suite("argument_parser")) {
argparse::ArgumentParser program("test");
program.add_argument("cases").remaining();

70
test/test_choices.cpp Normal file
View File

@ -0,0 +1,70 @@
#ifdef WITH_MODULE
import argparse;
#else
#include <argparse/argparse.hpp>
#endif
#include <doctest.hpp>
using doctest::test_suite;
TEST_CASE("Parse argument that is provided zero choices" *
test_suite("choices")) {
argparse::ArgumentParser program("test");
REQUIRE_THROWS_WITH_AS(program.add_argument("color").choices(),
"Zero choices provided", std::runtime_error);
}
TEST_CASE("Parse argument that is in the fixed number of allowed choices" *
test_suite("choices")) {
argparse::ArgumentParser program("test");
program.add_argument("color").choices("red", "green", "blue");
program.parse_args({"test", "red"});
}
TEST_CASE("Parse argument that is in the fixed number of allowed choices, with "
"invalid default" *
test_suite("choices")) {
argparse::ArgumentParser program("test");
program.add_argument("color").default_value("yellow").choices("red", "green",
"blue");
REQUIRE_THROWS_WITH_AS(
program.parse_args({"test"}),
"Invalid default value \"yellow\" - allowed options: {red, green, blue}",
std::runtime_error);
}
TEST_CASE("Parse invalid argument that is not in the fixed number of allowed "
"choices" *
test_suite("choices")) {
argparse::ArgumentParser program("test");
program.add_argument("color").choices("red", "green", "blue");
REQUIRE_THROWS_WITH_AS(
program.parse_args({"test", "red2"}),
"Invalid argument \"red2\" - allowed options: {red, green, blue}",
std::runtime_error);
}
TEST_CASE(
"Parse multiple arguments that are in the fixed number of allowed choices" *
test_suite("choices")) {
argparse::ArgumentParser program("test");
program.add_argument("color").nargs(2).choices("red", "green", "blue");
program.parse_args({"test", "red", "green"});
}
TEST_CASE("Parse multiple arguments one of which is not in the fixed number of "
"allowed choices" *
test_suite("choices")) {
argparse::ArgumentParser program("test");
program.add_argument("color").nargs(2).choices("red", "green", "blue");
REQUIRE_THROWS_WITH_AS(
program.parse_args({"test", "red", "green2"}),
"Invalid argument \"green2\" - allowed options: {red, green, blue}",
std::runtime_error);
}

View File

@ -5,9 +5,9 @@ import argparse;
#endif
#include <doctest.hpp>
#include <iostream>
#include <sstream>
#include <streambuf>
#include <iostream>
using doctest::test_suite;

View File

@ -6,8 +6,8 @@ import argparse;
#include <doctest.hpp>
#include <iostream>
#include <vector>
#include <string>
#include <vector>
using doctest::test_suite;

View File

@ -5,8 +5,8 @@ import argparse;
#endif
#include <doctest.hpp>
#include <sstream>
#include <optional>
#include <sstream>
using doctest::test_suite;
@ -82,22 +82,19 @@ TEST_CASE("Users can replace default -h/--help" * test_suite("help")) {
TEST_CASE("Multiline help message alignment") {
// '#' is used at the beginning of each help message line to simplify testing.
// It is important to ensure that this character doesn't appear elsewhere in the test case.
// Default arguments (e.g., -h/--help, -v/--version) are not included in this test.
// It is important to ensure that this character doesn't appear elsewhere in
// the test case. Default arguments (e.g., -h/--help, -v/--version) are not
// included in this test.
argparse::ArgumentParser program("program");
program.add_argument("INPUT1")
.help(
program.add_argument("INPUT1").help(
"#This is the first line of help message.\n"
"#And this is the second line of help message."
);
program.add_argument("program_input2")
.help("#There is only one line.");
"#And this is the second line of help message.");
program.add_argument("program_input2").help("#There is only one line.");
program.add_argument("-p", "--prog_input3")
.help(
R"(#Lorem ipsum dolor sit amet, consectetur adipiscing elit.
#Sed ut perspiciatis unde omnis iste natus error sit voluptatem
#accusantium doloremque laudantium, totam rem aperiam...)"
);
#accusantium doloremque laudantium, totam rem aperiam...)");
program.add_argument("--verbose").default_value(false).implicit_value(true);
std::ostringstream stream;
@ -107,7 +104,8 @@ R"(#Lorem ipsum dolor sit amet, consectetur adipiscing elit.
auto help_message_start = std::string::npos;
std::string line;
while (std::getline(iss, line)) {
// Find the position of '#', which indicates the start of the help message line
// Find the position of '#', which indicates the start of the help message
// line
auto pos = line.find('#');
if (pos == std::string::npos) {

View File

@ -5,8 +5,8 @@ import argparse;
#endif
#include <doctest.hpp>
#include <vector>
#include <string>
#include <vector>
using doctest::test_suite;

View File

@ -6,8 +6,8 @@ import argparse.details;
#endif
#include <doctest.hpp>
#include <set>
#include <list>
#include <set>
#include <sstream>
using doctest::test_suite;

View File

@ -6,8 +6,8 @@ import argparse;
#include <doctest.hpp>
#include <cmath>
#include <vector>
#include <string>
#include <vector>
using doctest::test_suite;