Support capturing remaining() arguments

This kind of argument works as if having the "remaining" nargs,
inspired by Python's `argparse.REMAINDER`.

This change also reduces the size of `Argument` by 8 bytes.

See also: https://docs.python.org/2/library/argparse.html#nargs

fixes: p-ranav/argparse#17
This commit is contained in:
Zhihao Yuan 2019-11-20 17:47:20 -06:00
parent 6ee8de5f4e
commit 991df83d97
No known key found for this signature in database
GPG Key ID: 0BC0BC626A0C5A8E
4 changed files with 147 additions and 30 deletions

View File

@ -36,6 +36,7 @@ SOFTWARE.
#include <list> #include <list>
#include <map> #include <map>
#include <numeric> #include <numeric>
#include <optional>
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
@ -159,11 +160,18 @@ public:
return *this; return *this;
} }
Argument &nargs(size_t aNumArgs) { Argument &nargs(int aNumArgs) {
if (aNumArgs < 0)
throw std::logic_error("Number of arguments must be non-negative");
mNumArgs = aNumArgs; mNumArgs = aNumArgs;
return *this; return *this;
} }
Argument &remaining() {
mNumArgs = -1;
return *this;
}
template <typename Iterator> template <typename Iterator>
Iterator consume(Iterator start, Iterator end, std::string usedName = {}) { Iterator consume(Iterator start, Iterator end, std::string usedName = {}) {
if (mIsUsed) { if (mIsUsed) {
@ -174,11 +182,14 @@ public:
if (mNumArgs == 0) { if (mNumArgs == 0) {
mValues.emplace_back(mImplicitValue); mValues.emplace_back(mImplicitValue);
return start; return start;
} else if (mNumArgs <= static_cast<size_t>(std::distance(start, end))) { } else if (mNumArgs <= std::distance(start, end)) {
end = std::next(start, mNumArgs); if (auto expected = maybe_nargs()) {
if (std::any_of(start, end, Argument::is_optional)) { end = std::next(start, *expected);
throw std::runtime_error("optional argument in parameter sequence"); if (std::any_of(start, end, Argument::is_optional)) {
throw std::runtime_error("optional argument in parameter sequence");
}
} }
struct action_apply { struct action_apply {
void operator()(valued_action &f) { void operator()(valued_action &f) {
std::transform(start, end, std::back_inserter(self.mValues), f); std::transform(start, end, std::back_inserter(self.mValues), f);
@ -186,8 +197,10 @@ public:
void operator()(void_action &f) { void operator()(void_action &f) {
std::for_each(start, end, f); std::for_each(start, end, f);
if (!self.mDefaultValue.has_value()) if (!self.mDefaultValue.has_value()) {
self.mValues.resize(self.mNumArgs); if (auto expected = self.maybe_nargs())
self.mValues.resize(*expected);
}
} }
Iterator start, end; Iterator start, end;
@ -206,35 +219,45 @@ 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 (mIsOptional) { if (auto expected = maybe_nargs()) {
if (mIsUsed && mValues.size() != mNumArgs && !mDefaultValue.has_value()) { if (mIsOptional) {
std::stringstream stream; if (mIsUsed && mValues.size() != *expected &&
stream << mUsedName << ": expected " << mNumArgs << " argument(s). " !mDefaultValue.has_value()) {
<< mValues.size() << " provided."; std::stringstream stream;
throw std::runtime_error(stream.str()); stream << mUsedName << ": expected " << *expected << " argument(s). "
<< mValues.size() << " provided.";
throw std::runtime_error(stream.str());
} else {
// TODO: check if an implicit value was programmed for this argument
if (!mIsUsed && !mDefaultValue.has_value() && mIsRequired) {
std::stringstream stream;
stream << mNames[0] << ": required.";
throw std::runtime_error(stream.str());
}
if (mIsUsed && mIsRequired && mValues.size() == 0) {
std::stringstream stream;
stream << mUsedName << ": no value provided.";
throw std::runtime_error(stream.str());
}
}
} else { } else {
// TODO: check if an implicit value was programmed for this argument if (mValues.size() != expected && !mDefaultValue.has_value()) {
if (!mIsUsed && !mDefaultValue.has_value() && mIsRequired) {
std::stringstream stream; std::stringstream stream;
stream << mNames[0] << ": required."; stream << mUsedName << ": expected " << *expected << " argument(s). "
<< mValues.size() << " provided.";
throw std::runtime_error(stream.str()); throw std::runtime_error(stream.str());
} }
if (mIsUsed && mIsRequired && mValues.size() == 0) {
std::stringstream stream;
stream << mUsedName << ": no value provided.";
throw std::runtime_error(stream.str());
}
}
} else {
if (mValues.size() != mNumArgs && !mDefaultValue.has_value()) {
std::stringstream stream;
stream << mUsedName << ": expected " << mNumArgs << " argument(s). "
<< mValues.size() << " provided.";
throw std::runtime_error(stream.str());
} }
} }
} }
auto maybe_nargs() const -> std::optional<size_t> {
if (mNumArgs < 0)
return std::nullopt;
else
return static_cast<size_t>(mNumArgs);
}
size_t get_arguments_length() const { size_t get_arguments_length() const {
return std::accumulate(std::begin(mNames), std::end(mNames), size_t(0), return std::accumulate(std::begin(mNames), std::end(mNames), size_t(0),
[](const auto &sum, const auto &s) { [](const auto &sum, const auto &s) {
@ -345,7 +368,7 @@ private:
std::in_place_type<valued_action>, std::in_place_type<valued_action>,
[](const std::string &aValue) { return aValue; }}; [](const std::string &aValue) { return aValue; }};
std::vector<std::any> mValues; std::vector<std::any> mValues;
size_t mNumArgs = 1; int mNumArgs = 1;
bool mIsOptional : 1; bool mIsOptional : 1;
bool mIsRequired : 1; bool mIsRequired : 1;
bool mIsUsed : 1; // True if the optional argument is used by user bool mIsUsed : 1; // True if the optional argument is used by user

View File

@ -119,3 +119,15 @@ TEST_CASE("Users can bind arguments to actions", "[actions]") {
} }
} }
} }
TEST_CASE("Users can use actions on remaining arguments", "[actions]") {
argparse::ArgumentParser program("sum");
int result = 0;
program.add_argument("all").remaining().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);
}

View File

@ -45,6 +45,35 @@ TEST_CASE("Parse multiple toggle arguments with implicit values", "[optional_arg
REQUIRE(program.get<bool>("-x") == true); REQUIRE(program.get<bool>("-x") == true);
} }
TEST_CASE("Parse optional arguments of many values", "[optional_arguments]") {
GIVEN("a program that accepts an optional argument of many values") {
argparse::ArgumentParser program("test");
program.add_argument("-i").remaining().action(
[](const std::string &value) { return std::stoi(value); });
WHEN("provided no argument") {
THEN("the program accepts it but gets nothing") {
REQUIRE_NOTHROW(program.parse_args({"test"}));
REQUIRE_THROWS_AS(program.get<std::vector<int>>("-i"),
std::logic_error);
}
}
WHEN("provided remaining arguments follow the option") {
program.parse_args({"test", "-i", "-42", "8", "100", "300"});
THEN("the optional parameter consumes all of them") {
auto inputs = program.get<std::vector<int>>("-i");
REQUIRE(inputs.size() == 4);
REQUIRE(inputs[0] == -42);
REQUIRE(inputs[1] == 8);
REQUIRE(inputs[2] == 100);
REQUIRE(inputs[3] == 300);
}
}
}
}
TEST_CASE("Parse arguments of different types", "[optional_arguments]") { TEST_CASE("Parse arguments of different types", "[optional_arguments]") {
using namespace std::literals; using namespace std::literals;

View File

@ -47,6 +47,59 @@ 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 remaining arguments deemed positional",
"[positional_arguments]") {
GIVEN("a program that accepts an optional argument and remaining arguments") {
argparse::ArgumentParser program("test");
program.add_argument("-o");
program.add_argument("input").remaining();
WHEN("provided no argument") {
THEN("the program accepts it but gets nothing") {
REQUIRE_NOTHROW(program.parse_args({"test"}));
REQUIRE_THROWS_AS(program.get<std::vector<std::string>>("input"),
std::logic_error);
}
}
WHEN("provided an optional followed by remaining 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 remaining arguments including optional arguments") {
program.parse_args({"test", "a.c", "b.c", "main.c", "-o", "a.out"});
THEN("the optional argument is deemed remaining") {
REQUIRE_THROWS_AS(program.get("-o"), std::logic_error);
auto inputs = program.get<std::vector<std::string>>("input");
REQUIRE(inputs.size() == 5);
REQUIRE(inputs[0] == "a.c");
REQUIRE(inputs[1] == "b.c");
REQUIRE(inputs[2] == "main.c");
REQUIRE(inputs[3] == "-o");
REQUIRE(inputs[4] == "a.out");
}
}
}
}
TEST_CASE("Negative nargs is not allowed", "[positional_arguments]") {
argparse::ArgumentParser program("test");
REQUIRE_THROWS_AS(program.add_argument("output").nargs(-1), std::logic_error);
}
TEST_CASE("Square a number", "[positional_arguments]") { TEST_CASE("Square a number", "[positional_arguments]") {
argparse::ArgumentParser program; argparse::ArgumentParser program;
program.add_argument("--verbose", "-v") program.add_argument("--verbose", "-v")
@ -60,4 +113,4 @@ TEST_CASE("Square a number", "[positional_arguments]") {
program.parse_args({"./main", "15"}); program.parse_args({"./main", "15"});
REQUIRE(program.get<double>("square") == 225); REQUIRE(program.get<double>("square") == 225);
} }