Merge pull request #295 from p-ranav/feature/277_choices

Closes #277
This commit is contained in:
Pranav 2023-10-27 10:57:40 -05:00 committed by GitHub
commit b10afeb50c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 283 additions and 58 deletions

View File

@ -1060,13 +1060,7 @@ argparse::ArgumentParser program("test");
program.add_argument("input")
.default_value(std::string{"baz"})
.action([](const std::string& value) {
static const std::vector<std::string> choices = { "foo", "bar", "baz" };
if (std::find(choices.begin(), choices.end(), value) != choices.end()) {
return value;
}
return std::string{ "baz" };
});
.choices("foo", "bar", "baz");
try {
program.parse_args(argc, argv);
@ -1083,7 +1077,34 @@ std::cout << input << std::endl;
```console
foo@bar:/home/dev/$ ./main fex
baz
Invalid argument "fex" - allowed options: {foo, bar, baz}
```
Using choices also works with integer types, e.g.,
```cpp
argparse::ArgumentParser program("test");
program.add_argument("input")
.default_value(0)
.choices(0, 1, 2, 3, 4, 5);
try {
program.parse_args(argc, argv);
}
catch (const std::exception& err) {
std::cerr << err.what() << std::endl;
std::cerr << program;
std::exit(1);
}
auto input = program.get("input");
std::cout << input << std::endl;
```
```console
foo@bar:/home/dev/$ ./main 6
Invalid argument "6" - allowed options: {0, 1, 2, 3, 4, 5}
```
## Using `option=value` syntax

View File

@ -350,6 +350,26 @@ std::string join(StrIt first, StrIt last, const std::string &separator) {
return value.str();
}
template <typename T>
struct can_invoke_to_string {
template <typename U>
static auto test(int) -> decltype(std::to_string(std::declval<U>()), std::true_type{});
template <typename U>
static auto test(...) -> std::false_type;
static constexpr bool value = decltype(test<T>(0))::value;
};
template <typename T>
struct IsChoiceTypeSupported {
using CleanType = typename std::decay<T>::type;
static const bool value = std::is_integral<CleanType>::value ||
std::is_same<CleanType, std::string>::value ||
std::is_same<CleanType, std::string_view>::value ||
std::is_same<CleanType, const char*>::value;
};
} // namespace details
enum class nargs_pattern { optional, any, at_least_one };
@ -408,6 +428,14 @@ public:
template <typename T> Argument &default_value(T &&value) {
m_default_value_repr = details::repr(value);
if constexpr (std::is_convertible_v<T, std::string_view>) {
m_default_value_str = std::string{std::string_view{value}};
}
else if constexpr (details::can_invoke_to_string<T>::value) {
m_default_value_str = std::to_string(value);
}
m_default_value = std::forward<T>(value);
return *this;
}
@ -428,7 +456,7 @@ public:
}
template <class F, class... Args>
auto action(F &&callable, Args &&... bound_args)
auto action(F &&callable, Args &&...bound_args)
-> std::enable_if_t<std::is_invocable_v<F, Args..., std::string const>,
Argument &> {
using action_type = std::conditional_t<
@ -509,10 +537,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 +553,80 @@ public:
return nargs(nargs_pattern::any);
}
template <typename T>
void add_choice(T&& choice) {
static_assert(details::IsChoiceTypeSupported<T>::value, "Only string or integer type supported for choice");
static_assert(std::is_convertible_v<T, std::string_view> || details::can_invoke_to_string<T>::value, "Choice is not convertible to string_type");
if (!m_choices.has_value()) {
m_choices = std::vector<std::string>{};
}
if constexpr (std::is_convertible_v<T, std::string_view>) {
m_choices.value().push_back(std::string{std::string_view{std::forward<T>(choice)}});
}
else if constexpr (details::can_invoke_to_string<T>::value) {
m_choices.value().push_back(std::to_string(std::forward<T>(choice)));
}
}
Argument &choices() {
if (!m_choices.has_value()) {
throw std::runtime_error("Zero choices provided");
}
return *this;
}
template <typename T, typename... U>
Argument &choices(T&& first, U&&...rest) {
add_choice(std::forward<T>(first));
choices(std::forward<U>(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_str) ==
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 +636,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 +714,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 +856,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;
});
}
@ -1063,7 +1180,9 @@ private:
std::string m_metavar;
std::any m_default_value;
std::string m_default_value_repr;
std::optional<std::string> m_default_value_str; // used for checking default_value against choices
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 +1271,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);
}
@ -1186,7 +1300,7 @@ public:
// Parameter packed add_parents method
// Accepts a variadic number of ArgumentParser objects
template <typename... Targs>
ArgumentParser &add_parents(const Targs &... f_args) {
ArgumentParser &add_parents(const Targs &...f_args) {
for (const ArgumentParser &parent_parser : {std::ref(f_args)...}) {
for (const auto &argument : parent_parser.m_positional_arguments) {
auto it = m_positional_arguments.insert(
@ -1215,8 +1329,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 +1805,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();

91
test/test_choices.cpp Normal file
View File

@ -0,0 +1,91 @@
#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);
}
TEST_CASE(
"Parse multiple arguments that are in the fixed number of allowed INTEGER choices" *
test_suite("choices")) {
argparse::ArgumentParser program("test");
program.add_argument("indices").nargs(2).choices(1, 2, 3, 4, 5);
program.parse_args({"test", "1", "2"});
}
TEST_CASE(
"Parse multiple arguments that are not in fixed number of allowed INTEGER choices" *
test_suite("choices")) {
argparse::ArgumentParser program("test");
program.add_argument("indices").nargs(2).choices(1, 2, 3, 4, 5);
REQUIRE_THROWS_WITH_AS(
program.parse_args({"test", "6", "7"}),
"Invalid argument \"6\" - allowed options: {1, 2, 3, 4, 5}",
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;
@ -30,7 +30,7 @@ TEST_CASE("Do not exit on default arguments" * test_suite("default_args")) {
argparse::ArgumentParser parser("test", "1.0",
argparse::default_arguments::all, false);
std::stringstream buf;
std::streambuf* saved_cout_buf = std::cout.rdbuf(buf.rdbuf());
std::streambuf *saved_cout_buf = std::cout.rdbuf(buf.rdbuf());
parser.parse_args({"test", "--help"});
std::cout.rdbuf(saved_cout_buf);
REQUIRE(parser.is_used("--help"));

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.
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;