Skip to content

Commit

Permalink
Merge pull request #125 from hokacci/feature/variable-length-nargs
Browse files Browse the repository at this point in the history
Improve nargs
  • Loading branch information
p-ranav authored Jun 22, 2022
2 parents 234f0cd + ed84d90 commit 24c599d
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 50 deletions.
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

0 comments on commit 24c599d

Please sign in to comment.