Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve nargs #125

Merged
merged 23 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c6c3be0
1st version of handling variable length nargs
hokacci Sep 10, 2021
3d559d3
Revive remaining method
hokacci Sep 11, 2021
c99272b
Remove negative parameter test for now unsigned
hokacci Sep 10, 2021
10ddd39
Fix tests for remaining
hokacci Sep 15, 2021
2cfe115
Change test for remaining to test for * nargs
hokacci Sep 15, 2021
aae2e93
Add another test for remaining
hokacci Sep 15, 2021
bb115c0
Add test for variable nargs
hokacci Sep 15, 2021
8845260
Add test for reversed order nargs
hokacci Sep 15, 2021
bec93ac
Avoid use ALL_CAPS for enumerators
hokacci Sep 15, 2021
14abaa4
Use member initializer list to SizeRange ctor
hokacci Sep 15, 2021
6dfaa1c
Restore a "remaining" test case for compat
hokacci Jun 20, 2022
0195a50
Complete "remainig" backward compatibility
hokacci Jun 20, 2022
12fcae6
Prefer pre-const to post-const
hokacci Jun 20, 2022
3459eec
Make throw_* funcs and make validate() clearer
hokacci Jun 20, 2022
08943f4
Merge branch 'master' into feature/variable-length-nargs
hokacci Jun 20, 2022
b869b5a
NArgsPattern -> nargs_pattern (to snake case)
hokacci Jun 21, 2022
5d6544a
Retrieve changes on 37a1f3b9e6ddb27ad70fb3b52c83266066949488
hokacci Jun 21, 2022
df6e7de
Prefer empty() to size() == 0
hokacci Jun 21, 2022
7b5084c
Fix a typo
hokacci Jun 21, 2022
e44023f
Explain variable-length arguments in README
hokacci Jun 21, 2022
acff046
Use optional instead of zero_or_one
hokacci Jun 22, 2022
25d24c7
SizeRange -> NArgsRange
hokacci Jun 22, 2022
ed84d90
Move NArgsRange to private: because it is detail of implementation
hokacci Jun 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 130 additions & 42 deletions include/argparse/argparse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ SOFTWARE.
#include <functional>
#include <iostream>
#include <iterator>
#include <limits>
#include <list>
#include <map>
#include <numeric>
Expand Down Expand Up @@ -327,6 +328,43 @@ template <class T> struct parse_number<T, chars_format::fixed> {

} // namespace details

class SizeRange {
std::size_t m_min;
std::size_t m_max;

public:
SizeRange(std::size_t minimum, std::size_t maximum) : m_min(minimum), m_max(maximum) {
if (minimum > maximum)
throw std::logic_error("Range of number of arguments is invalid");
}

bool contains(std::size_t value) const {
return value >= m_min && value <= m_max;
}

bool is_exact() const {
return m_min == m_max;
}

bool is_right_bounded() const {
return m_max < std::numeric_limits<std::size_t>::max();
}

std::size_t get_min() const {
return m_min;
}

std::size_t get_max() const {
return m_max;
}
};

enum class NArgsPattern {
ZeroOrOne,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency, let's use lower_case underscore-separated field names for enum classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b869b5a

Any,
AtLeastOne
};

enum class default_arguments : unsigned int {
none = 0,
help = 1,
Expand Down Expand Up @@ -383,7 +421,7 @@ class Argument {

Argument &implicit_value(std::any value) {
m_implicit_value = std::move(value);
m_num_args = 0;
m_num_args_range = SizeRange{0, 0};
return *this;
}

Expand Down Expand Up @@ -452,17 +490,39 @@ class Argument {
return *this;
}

Argument &nargs(int num_args) {
if (num_args < 0) {
throw std::logic_error("Number of arguments must be non-negative");
Argument &nargs(std::size_t num_args) {
m_num_args_range = SizeRange{num_args, num_args};
return *this;
}

Argument &nargs(std::size_t num_args_min, std::size_t num_args_max) {
m_num_args_range = SizeRange{num_args_min, num_args_max};
return *this;
}

Argument &nargs(SizeRange num_args_range) {
m_num_args_range = num_args_range;
return *this;
}

Argument &nargs(NArgsPattern num_args_pattern) {
switch (num_args_pattern) {
case NArgsPattern::ZeroOrOne:
m_num_args_range = SizeRange{0, 1};
break;
case NArgsPattern::Any:
m_num_args_range = SizeRange{0, std::numeric_limits<std::size_t>::max()};
break;
case NArgsPattern::AtLeastOne:
m_num_args_range = SizeRange{1, std::numeric_limits<std::size_t>::max()};
break;
}
m_num_args = num_args;
return *this;
}

Argument &remaining() {
m_num_args = -1;
return *this;
m_accepts_optional_like_value = true;
return nargs(NArgsPattern::Any);
}

template <typename Iterator>
Expand All @@ -473,16 +533,23 @@ class Argument {
}
m_is_used = true;
m_used_name = used_name;
if (m_num_args == 0) {

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;
if (num_args_max == 0) {
m_values.emplace_back(m_implicit_value);
std::visit([](const auto &f) { f({}); }, m_action);
return start;
}
if (m_num_args <= std::distance(start, end)) {
if (auto expected = maybe_nargs()) {
end = std::next(start, *expected);
if (std::any_of(start, end, Argument::is_optional)) {
throw std::runtime_error("optional argument in parameter sequence");
} else if ((dist = static_cast<std::size_t>(std::distance(start, end))) >= num_args_min) {
if (num_args_max < dist) {
end = std::next(start, num_args_max);
}
if (!m_accepts_optional_like_value) {
end = std::find_if(start, end, Argument::is_optional);
dist = static_cast<std::size_t>(std::distance(start, end));
if (dist < num_args_min) {
throw std::runtime_error("Too few arguments");
}
}

Expand All @@ -494,9 +561,8 @@ class Argument {
void operator()(void_action &f) {
std::for_each(first, last, f);
if (!self.m_default_value.has_value()) {
if (auto expected = self.maybe_nargs()) {
self.m_values.resize(*expected);
}
if (!self.m_accepts_optional_like_value)
self.m_values.resize(std::distance(first, last));
}
}

Expand All @@ -517,38 +583,26 @@ class Argument {
* @throws std::runtime_error if argument values are not valid
*/
void validate() const {
if (auto expected = maybe_nargs()) {
if (m_is_optional) {
if (m_is_optional) {
if (m_is_used && !m_num_args_range.contains(m_values.size()) && !m_is_repeatable &&
!m_default_value.has_value()) {
throw_nargs_range_validation_error();
} else {
// TODO: check if an implicit value was programmed for this argument
if (!m_is_used && !m_default_value.has_value() && m_is_required) {
std::stringstream stream;
stream << m_names[0] << ": required.";
throw std::runtime_error(stream.str());
}
if (m_is_used && m_is_required && m_values.empty()) {
std::stringstream stream;
stream << m_used_name << ": no value provided.";
throw std::runtime_error(stream.str());
throw_required_arg_not_used_error();
}
} else if (m_values.size() != expected && !m_default_value.has_value()) {
std::stringstream stream;
if (!m_used_name.empty()) {
stream << m_used_name << ": ";
if (m_is_used && m_is_required && m_values.size() == 0) {
throw_required_arg_no_value_provided_error();
}
stream << *expected << " argument(s) expected. " << m_values.size()
<< " provided.";
throw std::runtime_error(stream.str());
}
} else {
if (!m_num_args_range.contains(m_values.size()) && !m_default_value.has_value()) {
throw_nargs_range_validation_error();
}
}
}

auto maybe_nargs() const -> std::optional<std::size_t> {
if (m_num_args < 0) {
return std::nullopt;
}
return static_cast<std::size_t>(m_num_args);
}

std::size_t get_arguments_length() const {
return std::accumulate(std::begin(m_names), std::end(m_names),
std::size_t(0), [](const auto &sum, const auto &s) {
Expand Down Expand Up @@ -600,6 +654,35 @@ class Argument {
}

private:

void throw_nargs_range_validation_error() const {
std::stringstream stream;
if (!m_used_name.empty())
stream << m_used_name << ": ";
if (m_num_args_range.is_exact()) {
stream << m_num_args_range.get_min();
} else if (m_num_args_range.is_right_bounded()) {
stream << m_num_args_range.get_min() << " to " << m_num_args_range.get_max();
} else {
stream << m_num_args_range.get_min() << " or more";
}
stream << " argument(s) expected. "
<< m_values.size() << " provided.";
throw std::runtime_error(stream.str());
}

void throw_required_arg_not_used_error() const {
std::stringstream stream;
stream << m_names[0] << ": required.";
throw std::runtime_error(stream.str());
}

void throw_required_arg_no_value_provided_error() const {
std::stringstream stream;
stream << m_used_name << ": no value provided.";
throw std::runtime_error(stream.str());
}

static constexpr int eof = std::char_traits<char>::eof();

static auto lookahead(std::string_view s) -> int {
Expand Down Expand Up @@ -789,6 +872,10 @@ class Argument {
}
if (m_default_value.has_value()) {
return std::any_cast<T>(m_default_value);
} else {
if constexpr (details::IsContainer<T>)
if (!m_accepts_optional_like_value)
return any_cast_container<T>(m_values);
}
throw std::logic_error("No value provided for '" + m_names.back() + "'.");
}
Expand Down Expand Up @@ -834,7 +921,8 @@ class Argument {
std::in_place_type<valued_action>,
[](const std::string &value) { return value; }};
std::vector<std::any> m_values;
int m_num_args = 1;
SizeRange m_num_args_range {1, 1};
bool m_accepts_optional_like_value = false;
bool m_is_optional : true;
bool m_is_required : true;
bool m_is_repeatable : true;
Expand Down
17 changes: 15 additions & 2 deletions test/test_actions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,32 @@ TEST_CASE("Users can bind arguments to actions" * test_suite("actions")) {
}
}

TEST_CASE("Users can use actions on remaining arguments" *
TEST_CASE("Users can use actions on nargs=ANY arguments" *
test_suite("actions")) {
argparse::ArgumentParser program("sum");

int result = 0;
program.add_argument("all").remaining().action(
program.add_argument("all").nargs(argparse::NArgsPattern::Any).action(
[](int &sum, std::string const &value) { sum += std::stoi(value); },
std::ref(result));

program.parse_args({"sum", "42", "100", "-3", "-20"});
REQUIRE(result == 119);
}

TEST_CASE("Users can use actions on remaining arguments" *
test_suite("actions")) {
argparse::ArgumentParser program("concat");

std::string result = "";
program.add_argument("all").remaining().action(
[](std::string &sum, const std::string &value) { sum += value; },
std::ref(result));

program.parse_args({"concat", "a", "-b", "-c", "--d"});
REQUIRE(result == "a-b-c--d");
}

TEST_CASE("Users can run actions on parameterless optional arguments" *
test_suite("actions")) {
argparse::ArgumentParser program("test");
Expand Down
82 changes: 81 additions & 1 deletion test/test_optional_arguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ TEST_CASE("Parse optional arguments of many values" *
program.add_argument("-i").remaining().scan<'i', int>();

WHEN("provided no argument") {
THEN("the program accepts it but gets nothing") {
THEN("the program accepts it bug gets nothing") {
REQUIRE_NOTHROW(program.parse_args({"test"}));
REQUIRE_THROWS_AS(program.get<std::vector<int>>("-i"),
std::logic_error);
Expand All @@ -110,6 +110,86 @@ TEST_CASE("Parse optional arguments of many values" *
}
}

TEST_CASE("Parse 2 optional arguments of many values" *
test_suite("optional_arguments")) {
GIVEN("a program that accepts 2 optional arguments of many values") {
argparse::ArgumentParser program("test");
program.add_argument("-i").nargs(argparse::NArgsPattern::Any).scan<'i', int>();
program.add_argument("-s").nargs(argparse::NArgsPattern::Any);

WHEN("provided no argument") {
THEN("the program accepts it and gets empty container") {
REQUIRE_NOTHROW(program.parse_args({"test"}));
auto i = program.get<std::vector<int>>("-i");
REQUIRE(i.size() == 0);

auto s = program.get<std::vector<std::string>>("-s");
REQUIRE(s.size() == 0);
}
}

WHEN("provided 2 options with many arguments") {
program.parse_args(
{"test", "-i", "-42", "8", "100", "300", "-s", "ok", "this", "works"});

THEN("the optional parameter consumes each arguments") {
auto i = program.get<std::vector<int>>("-i");
REQUIRE(i.size() == 4);
REQUIRE(i[0] == -42);
REQUIRE(i[1] == 8);
REQUIRE(i[2] == 100);
REQUIRE(i[3] == 300);

auto s = program.get<std::vector<std::string>>("-s");
REQUIRE(s.size() == 3);
REQUIRE(s[0] == "ok");
REQUIRE(s[1] == "this");
REQUIRE(s[2] == "works");
}
}
}
}

TEST_CASE("Parse an optional argument of many values"
" and a positional argument of many values" *
test_suite("optional_arguments")) {
GIVEN("a program that accepts an optional argument of many values"
" and a positional argument of many values") {
argparse::ArgumentParser program("test");
program.add_argument("-s").nargs(argparse::NArgsPattern::Any);
program.add_argument("input").nargs(argparse::NArgsPattern::Any);

WHEN("provided no argument") {
program.parse_args({"test"});
THEN("the program accepts it and gets empty containers") {
auto s = program.get<std::vector<std::string>>("-s");
REQUIRE(s.size() == 0);

auto input = program.get<std::vector<std::string>>("input");
REQUIRE(input.size() == 0);
}
}

WHEN("provided many arguments followed by an option with many arguments") {
program.parse_args(
{"test", "foo", "bar", "-s", "ok", "this", "works"});

THEN("the parameters consume each arguments") {
auto s = program.get<std::vector<std::string>>("-s");
REQUIRE(s.size() == 3);
REQUIRE(s[0] == "ok");
REQUIRE(s[1] == "this");
REQUIRE(s[2] == "works");

auto input = program.get<std::vector<std::string>>("input");
REQUIRE(input.size() == 2);
REQUIRE(input[0] == "foo");
REQUIRE(input[1] == "bar");
}
}
}
}

TEST_CASE("Parse arguments of different types" *
test_suite("optional_arguments")) {
using namespace std::literals;
Expand Down
Loading