mirror of
https://github.com/KeqingMoe/argparse.git
synced 2025-07-03 22:54:39 +00:00
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:
parent
6ee8de5f4e
commit
991df83d97
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user