Merge pull request #125 from hokacci/feature/variable-length-nargs

Improve nargs
This commit is contained in:
Pranav 2022-06-22 10:57:38 -07:00 committed by GitHub
commit 24c599dfde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 368 additions and 50 deletions

View File

@ -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} 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
Compound arguments are optional arguments that are combined and provided as a single argument. Example: ```ps -aux``` Compound arguments are optional arguments that are combined and provided as a single argument. Example: ```ps -aux```

View File

@ -38,6 +38,7 @@ SOFTWARE.
#include <functional> #include <functional>
#include <iostream> #include <iostream>
#include <iterator> #include <iterator>
#include <limits>
#include <list> #include <list>
#include <map> #include <map>
#include <numeric> #include <numeric>
@ -327,6 +328,12 @@ template <class T> struct parse_number<T, chars_format::fixed> {
} // namespace details } // namespace details
enum class nargs_pattern {
optional,
any,
at_least_one
};
enum class default_arguments : unsigned int { enum class default_arguments : unsigned int {
none = 0, none = 0,
help = 1, help = 1,
@ -383,7 +390,7 @@ public:
Argument &implicit_value(std::any value) { Argument &implicit_value(std::any value) {
m_implicit_value = std::move(value); m_implicit_value = std::move(value);
m_num_args = 0; m_num_args_range = NArgsRange{0, 0};
return *this; return *this;
} }
@ -452,17 +459,34 @@ public:
return *this; return *this;
} }
Argument &nargs(int num_args) { Argument &nargs(std::size_t num_args) {
if (num_args < 0) { m_num_args_range = NArgsRange{num_args, num_args};
throw std::logic_error("Number of arguments must be non-negative"); 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; return *this;
} }
Argument &remaining() { Argument &remaining() {
m_num_args = -1; m_accepts_optional_like_value = true;
return *this; return nargs(nargs_pattern::any);
} }
template <typename Iterator> template <typename Iterator>
@ -473,16 +497,23 @@ public:
} }
m_is_used = true; m_is_used = true;
m_used_name = used_name; 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); m_values.emplace_back(m_implicit_value);
std::visit([](const auto &f) { f({}); }, m_action); std::visit([](const auto &f) { f({}); }, m_action);
return start; return start;
} 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_num_args <= std::distance(start, end)) { if (!m_accepts_optional_like_value) {
if (auto expected = maybe_nargs()) { end = std::find_if(start, end, Argument::is_optional);
end = std::next(start, *expected); dist = static_cast<std::size_t>(std::distance(start, end));
if (std::any_of(start, end, Argument::is_optional)) { if (dist < num_args_min) {
throw std::runtime_error("optional argument in parameter sequence"); throw std::runtime_error("Too few arguments");
} }
} }
@ -494,9 +525,8 @@ public:
void operator()(void_action &f) { void operator()(void_action &f) {
std::for_each(first, last, f); std::for_each(first, last, f);
if (!self.m_default_value.has_value()) { if (!self.m_default_value.has_value()) {
if (auto expected = self.maybe_nargs()) { if (!self.m_accepts_optional_like_value)
self.m_values.resize(*expected); self.m_values.resize(std::distance(first, last));
}
} }
} }
@ -517,38 +547,21 @@ public:
* @throws std::runtime_error if argument values are not valid * @throws std::runtime_error if argument values are not valid
*/ */
void validate() const { void validate() const {
if (auto expected = maybe_nargs()) {
if (m_is_optional) { if (m_is_optional) {
// TODO: check if an implicit value was programmed for this argument // TODO: check if an implicit value was programmed for this argument
if (!m_is_used && !m_default_value.has_value() && m_is_required) { if (!m_is_used && !m_default_value.has_value() && m_is_required) {
std::stringstream stream; throw_required_arg_not_used_error();
stream << m_names[0] << ": required.";
throw std::runtime_error(stream.str());
} }
if (m_is_used && m_is_required && m_values.empty()) { if (m_is_used && m_is_required && m_values.empty()) {
std::stringstream stream; throw_required_arg_no_value_provided_error();
stream << m_used_name << ": no value provided.";
throw std::runtime_error(stream.str());
} }
} else if (m_values.size() != expected && !m_default_value.has_value()) { } else {
std::stringstream stream; if (!m_num_args_range.contains(m_values.size()) && !m_default_value.has_value()) {
if (!m_used_name.empty()) { throw_nargs_range_validation_error();
stream << m_used_name << ": ";
}
stream << *expected << " argument(s) expected. " << m_values.size()
<< " provided.";
throw std::runtime_error(stream.str());
} }
} }
} }
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 { std::size_t get_arguments_length() const {
return std::accumulate(std::begin(m_names), std::end(m_names), return std::accumulate(std::begin(m_names), std::end(m_names),
std::size_t(0), [](const auto &sum, const auto &s) { std::size_t(0), [](const auto &sum, const auto &s) {
@ -600,6 +613,66 @@ public:
} }
private: 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 constexpr int eof = std::char_traits<char>::eof();
static auto lookahead(std::string_view s) -> int { static auto lookahead(std::string_view s) -> int {
@ -789,6 +862,10 @@ private:
} }
if (m_default_value.has_value()) { if (m_default_value.has_value()) {
return std::any_cast<T>(m_default_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() + "'."); throw std::logic_error("No value provided for '" + m_names.back() + "'.");
} }
@ -834,7 +911,8 @@ private:
std::in_place_type<valued_action>, std::in_place_type<valued_action>,
[](const std::string &value) { return value; }}; [](const std::string &value) { return value; }};
std::vector<std::any> m_values; 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_optional : true;
bool m_is_required : true; bool m_is_required : true;
bool m_is_repeatable : true; bool m_is_repeatable : true;

View File

@ -121,12 +121,12 @@ 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")) { test_suite("actions")) {
argparse::ArgumentParser program("sum"); argparse::ArgumentParser program("sum");
int result = 0; 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); }, [](int &sum, std::string const &value) { sum += std::stoi(value); },
std::ref(result)); std::ref(result));
@ -134,6 +134,19 @@ TEST_CASE("Users can use actions on remaining arguments" *
REQUIRE(result == 119); 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_CASE("Users can run actions on parameterless optional arguments" *
test_suite("actions")) { test_suite("actions")) {
argparse::ArgumentParser program("test"); argparse::ArgumentParser program("test");

View File

@ -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::nargs_pattern::any).scan<'i', int>();
program.add_argument("-s").nargs(argparse::nargs_pattern::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::nargs_pattern::any);
program.add_argument("input").nargs(argparse::nargs_pattern::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_CASE("Parse arguments of different types" *
test_suite("optional_arguments")) { test_suite("optional_arguments")) {
using namespace std::literals; using namespace std::literals;

View File

@ -61,6 +61,130 @@ TEST_CASE("Parse positional arguments with optional arguments in the middle" *
REQUIRE_THROWS(program.parse_args({ "test", "rocket.mesh", "thrust_profile.csv", "--num_iterations", "15", "output.mesh" })); REQUIRE_THROWS(program.parse_args({ "test", "rocket.mesh", "thrust_profile.csv", "--num_iterations", "15", "output.mesh" }));
} }
TEST_CASE("Parse positional nargs=1..2 arguments" *
test_suite("positional_arguments")) {
GIVEN("a program that accepts an optional argument and nargs=1..2 positional arguments") {
argparse::ArgumentParser program("test");
program.add_argument("-o");
program.add_argument("input").nargs(1, 2);
WHEN("provided no argument") {
THEN("the program does not accept it") {
REQUIRE_THROWS(program.parse_args({"test"}));
}
}
WHEN("provided 1 argument") {
THEN("the program accepts it") {
REQUIRE_NOTHROW(program.parse_args({"test", "a.c"}));
auto inputs = program.get<std::vector<std::string>>("input");
REQUIRE(inputs.size() == 1);
REQUIRE(inputs[0] == "a.c");
}
}
WHEN("provided 2 arguments") {
THEN("the program accepts it") {
REQUIRE_NOTHROW(program.parse_args({"test", "a.c", "b.c"}));
auto inputs = program.get<std::vector<std::string>>("input");
REQUIRE(inputs.size() == 2);
REQUIRE(inputs[0] == "a.c");
REQUIRE(inputs[1] == "b.c");
}
}
WHEN("provided 3 arguments") {
THEN("the program does not accept it") {
REQUIRE_THROWS(program.parse_args({"test", "a.c", "b.c", "main.c"}));
}
}
WHEN("provided an optional followed by positional arguments") {
program.parse_args({"test", "-o", "a.out", "a.c", "b.c"});
THEN("the optional parameter consumes an argument") {
using namespace std::literals;
REQUIRE(program["-o"] == "a.out"s);
auto inputs = program.get<std::vector<std::string>>("input");
REQUIRE(inputs.size() == 2);
REQUIRE(inputs[0] == "a.c");
REQUIRE(inputs[1] == "b.c");
}
}
WHEN("provided an optional preceded by positional arguments") {
program.parse_args({"test", "a.c", "b.c", "-o", "a.out"});
THEN("the optional parameter consumes an argument") {
using namespace std::literals;
REQUIRE(program["-o"] == "a.out"s);
auto inputs = program.get<std::vector<std::string>>("input");
REQUIRE(inputs.size() == 2);
REQUIRE(inputs[0] == "a.c");
REQUIRE(inputs[1] == "b.c");
}
}
WHEN("provided an optional in between positional arguments") {
THEN("the program does not accept it") {
REQUIRE_THROWS(program.parse_args({"test", "a.c", "-o", "a.out", "b.c"}));
}
}
}
}
TEST_CASE("Parse positional nargs=ANY arguments" *
test_suite("positional_arguments")) {
GIVEN("a program that accepts an optional argument and nargs=ANY positional arguments") {
argparse::ArgumentParser program("test");
program.add_argument("-o");
program.add_argument("input").nargs(argparse::nargs_pattern::any);
WHEN("provided no argument") {
THEN("the program accepts it and gets empty container") {
REQUIRE_NOTHROW(program.parse_args({"test"}));
auto inputs = program.get<std::vector<std::string>>("input");
REQUIRE(inputs.size() == 0);
}
}
WHEN("provided an optional followed by positional arguments") {
program.parse_args({"test", "-o", "a.out", "a.c", "b.c", "main.c"});
THEN("the optional parameter consumes an argument") {
using namespace std::literals;
REQUIRE(program["-o"] == "a.out"s);
auto inputs = program.get<std::vector<std::string>>("input");
REQUIRE(inputs.size() == 3);
REQUIRE(inputs[0] == "a.c");
REQUIRE(inputs[1] == "b.c");
REQUIRE(inputs[2] == "main.c");
}
}
WHEN("provided an optional preceded by positional arguments") {
program.parse_args({"test", "a.c", "b.c", "main.c", "-o", "a.out"});
THEN("the optional parameter consumes an argument") {
using namespace std::literals;
REQUIRE(program["-o"] == "a.out"s);
auto inputs = program.get<std::vector<std::string>>("input");
REQUIRE(inputs.size() == 3);
REQUIRE(inputs[0] == "a.c");
REQUIRE(inputs[1] == "b.c");
REQUIRE(inputs[2] == "main.c");
}
}
}
}
TEST_CASE("Parse remaining arguments deemed positional" * TEST_CASE("Parse remaining arguments deemed positional" *
test_suite("positional_arguments")) { test_suite("positional_arguments")) {
GIVEN("a program that accepts an optional argument and remaining arguments") { GIVEN("a program that accepts an optional argument and remaining arguments") {
@ -109,10 +233,10 @@ TEST_CASE("Parse remaining arguments deemed positional" *
} }
} }
TEST_CASE("Negative nargs is not allowed" * TEST_CASE("Reversed order nargs is not allowed" *
test_suite("positional_arguments")) { test_suite("positional_arguments")) {
argparse::ArgumentParser program("test"); argparse::ArgumentParser program("test");
REQUIRE_THROWS_AS(program.add_argument("output").nargs(-1), std::logic_error); REQUIRE_THROWS_AS(program.add_argument("output").nargs(2, 1), std::logic_error);
} }
TEST_CASE("Square a number" * test_suite("positional_arguments")) { TEST_CASE("Square a number" * test_suite("positional_arguments")) {