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 <map>
#include <numeric>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
@ -159,11 +160,18 @@ public:
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;
return *this;
}
Argument &remaining() {
mNumArgs = -1;
return *this;
}
template <typename Iterator>
Iterator consume(Iterator start, Iterator end, std::string usedName = {}) {
if (mIsUsed) {
@ -174,11 +182,14 @@ public:
if (mNumArgs == 0) {
mValues.emplace_back(mImplicitValue);
return start;
} else if (mNumArgs <= static_cast<size_t>(std::distance(start, end))) {
end = std::next(start, mNumArgs);
if (std::any_of(start, end, Argument::is_optional)) {
throw std::runtime_error("optional argument in parameter sequence");
} else if (mNumArgs <= 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");
}
}
struct action_apply {
void operator()(valued_action &f) {
std::transform(start, end, std::back_inserter(self.mValues), f);
@ -186,8 +197,10 @@ public:
void operator()(void_action &f) {
std::for_each(start, end, f);
if (!self.mDefaultValue.has_value())
self.mValues.resize(self.mNumArgs);
if (!self.mDefaultValue.has_value()) {
if (auto expected = self.maybe_nargs())
self.mValues.resize(*expected);
}
}
Iterator start, end;
@ -206,35 +219,45 @@ public:
* @throws std::runtime_error if argument values are not valid
*/
void validate() const {
if (mIsOptional) {
if (mIsUsed && mValues.size() != mNumArgs && !mDefaultValue.has_value()) {
std::stringstream stream;
stream << mUsedName << ": expected " << mNumArgs << " argument(s). "
<< mValues.size() << " provided.";
throw std::runtime_error(stream.str());
if (auto expected = maybe_nargs()) {
if (mIsOptional) {
if (mIsUsed && mValues.size() != *expected &&
!mDefaultValue.has_value()) {
std::stringstream stream;
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 {
// TODO: check if an implicit value was programmed for this argument
if (!mIsUsed && !mDefaultValue.has_value() && mIsRequired) {
if (mValues.size() != expected && !mDefaultValue.has_value()) {
std::stringstream stream;
stream << mNames[0] << ": required.";
stream << mUsedName << ": expected " << *expected << " argument(s). "
<< mValues.size() << " provided.";
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 {
return std::accumulate(std::begin(mNames), std::end(mNames), size_t(0),
[](const auto &sum, const auto &s) {
@ -345,7 +368,7 @@ private:
std::in_place_type<valued_action>,
[](const std::string &aValue) { return aValue; }};
std::vector<std::any> mValues;
size_t mNumArgs = 1;
int mNumArgs = 1;
bool mIsOptional : 1;
bool mIsRequired : 1;
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);
}
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]") {
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" }));
}
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]") {
argparse::ArgumentParser program;
program.add_argument("--verbose", "-v")
@ -60,4 +113,4 @@ TEST_CASE("Square a number", "[positional_arguments]") {
program.parse_args({"./main", "15"});
REQUIRE(program.get<double>("square") == 225);
}
}