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 all 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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,29 @@ catch (const std::runtime_error& err) {
auto query_point = program.get<std::vector<double>>("--query_point"); // {3.5, 4.7, 9.2}
```

You can also make a variable length list of arguments with the ```.nargs```.
Below are some examples.

```cpp
program.add_argument("--input_files")
.nargs(1, 3); // This accepts 1 to 3 arguments.
```

Some useful patterns are defined like "?", "*", "+" of argparse in Python.

```cpp
program.add_argument("--input_files")
.nargs(argparse::nargs_pattern::any); // "*" in Python. This accepts any number of arguments including 0.
```
```cpp
program.add_argument("--input_files")
.nargs(argparse::nargs_pattern::at_least_one); // "+" in Python. This accepts one or more number of arguments.
```
```cpp
program.add_argument("--input_files")
.nargs(argparse::nargs_pattern::optional); // "?" in Python. This accepts an argument optionally.
```

### Compound Arguments

Compound arguments are optional arguments that are combined and provided as a single argument. Example: ```ps -aux```
Expand Down
170 changes: 124 additions & 46 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,12 @@ template <class T> struct parse_number<T, chars_format::fixed> {

} // namespace details

enum class nargs_pattern {
optional,
any,
at_least_one
};

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

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

Expand Down Expand Up @@ -452,17 +459,34 @@ 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 = NArgsRange{num_args, num_args};
return *this;
}

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

Argument &nargs(nargs_pattern pattern) {
switch (pattern) {
case nargs_pattern::optional:
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()};
break;
case nargs_pattern::at_least_one:
m_num_args_range = NArgsRange{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(nargs_pattern::any);
}

template <typename Iterator>
Expand All @@ -473,16 +497,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 +525,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 +547,21 @@ class Argument {
* @throws std::runtime_error if argument values are not valid
*/
void validate() const {
if (auto expected = maybe_nargs()) {
if (m_is_optional) {
// 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());
}
} else if (m_values.size() != expected && !m_default_value.has_value()) {
std::stringstream stream;
if (!m_used_name.empty()) {
stream << m_used_name << ": ";
}
stream << *expected << " argument(s) expected. " << m_values.size()
<< " provided.";
throw std::runtime_error(stream.str());
if (m_is_optional) {
// TODO: check if an implicit value was programmed for this argument
if (!m_is_used && !m_default_value.has_value() && m_is_required) {
throw_required_arg_not_used_error();
}
if (m_is_used && m_is_required && m_values.empty()) {
throw_required_arg_no_value_provided_error();
}
} 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 +613,66 @@ class Argument {
}

private:

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

public:
NArgsRange(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;
}
};

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 +862,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 +911,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;
NArgsRange 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::nargs_pattern::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
Loading