Skip to content

Commit

Permalink
p-ranav#277 Added in-built support for string_type choices
Browse files Browse the repository at this point in the history
  • Loading branch information
p-ranav authored and russkel committed Dec 11, 2023
1 parent 9f0f458 commit bcf0a8f
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 50 deletions.
123 changes: 104 additions & 19 deletions include/argparse/argparse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ class Argument {
}

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<
Expand Down Expand Up @@ -509,10 +509,12 @@ class Argument {
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;
Expand All @@ -523,6 +525,80 @@ class Argument {
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 = {}) {
Expand All @@ -532,6 +608,14 @@ class Argument {
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;
Expand Down Expand Up @@ -602,6 +686,12 @@ class Argument {
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 {
Expand Down Expand Up @@ -738,8 +828,7 @@ class Argument {
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;
});
}
Expand Down Expand Up @@ -1064,6 +1153,7 @@ class Argument {
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{
Expand Down Expand Up @@ -1152,16 +1242,11 @@ class ArgumentParser {
}

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);
}
Expand All @@ -1186,7 +1271,7 @@ class ArgumentParser {
// 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(
Expand Down Expand Up @@ -1215,8 +1300,7 @@ class ArgumentParser {
/* 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 {
Expand Down Expand Up @@ -1692,7 +1776,8 @@ class ArgumentParser {
}
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());
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/test_append.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import argparse;
#endif
#include <doctest.hpp>

#include <vector>
#include <string>
#include <vector>

using doctest::test_suite;

Expand Down
7 changes: 4 additions & 3 deletions test/test_as_container.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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"));
}

Expand Down
7 changes: 3 additions & 4 deletions test/test_bool_operator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -39,7 +38,7 @@ TEST_CASE("With subparsers in bool context" * test_suite("argument_parser")) {
}

TEST_CASE("Parsers remain false with unknown arguments" *
test_suite("argument_parser")) {
test_suite("argument_parser")) {
argparse::ArgumentParser program("test");

argparse::ArgumentParser cmd_build("build");
Expand All @@ -59,7 +58,7 @@ TEST_CASE("Parsers remain false with unknown arguments" *
}

TEST_CASE("Multi-level parsers match subparser bool" *
test_suite("argument_parser")) {
test_suite("argument_parser")) {
argparse::ArgumentParser program("test");

argparse::ArgumentParser cmd_cook("cook");
Expand Down
70 changes: 70 additions & 0 deletions test/test_choices.cpp
Original file line number Diff line number Diff line change
@@ -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);
}
4 changes: 2 additions & 2 deletions test/test_default_args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import argparse;
#endif
#include <doctest.hpp>

#include <iostream>
#include <sstream>
#include <streambuf>
#include <iostream>

using doctest::test_suite;

Expand All @@ -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"));
Expand Down
2 changes: 1 addition & 1 deletion test/test_equals_form.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import argparse;
#include <doctest.hpp>

#include <iostream>
#include <vector>
#include <string>
#include <vector>

using doctest::test_suite;

Expand Down
2 changes: 1 addition & 1 deletion test/test_get.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ TEST_CASE("Implicit argument" * test_suite("ArgumentParser::get")) {

TEST_CASE("Mismatched type for argument" * test_suite("ArgumentParser::get")) {
argparse::ArgumentParser program("test");
program.add_argument("-s", "--stuff"); // as default type, a std::string
program.add_argument("-s", "--stuff"); // as default type, a std::string
REQUIRE_NOTHROW(program.parse_args({"test", "-s", "321"}));
REQUIRE_THROWS_AS(program.get<int>("--stuff"), std::bad_any_cast);
}
Loading

0 comments on commit bcf0a8f

Please sign in to comment.