diff --git a/include/argparse.hpp b/include/argparse.hpp index 400718e..b551865 100644 --- a/include/argparse.hpp +++ b/include/argparse.hpp @@ -36,6 +36,7 @@ SOFTWARE. #include #include #include +#include #include #include #include @@ -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 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(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 { + if (mNumArgs < 0) + return std::nullopt; + else + return static_cast(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, [](const std::string &aValue) { return aValue; }}; std::vector 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 diff --git a/test/test_actions.hpp b/test/test_actions.hpp index 0a259bd..7e4f0ff 100644 --- a/test/test_actions.hpp +++ b/test/test_actions.hpp @@ -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); +} diff --git a/test/test_optional_arguments.hpp b/test/test_optional_arguments.hpp index 4f316c7..dd8dc82 100644 --- a/test/test_optional_arguments.hpp +++ b/test/test_optional_arguments.hpp @@ -45,6 +45,35 @@ TEST_CASE("Parse multiple toggle arguments with implicit values", "[optional_arg REQUIRE(program.get("-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>("-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>("-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; diff --git a/test/test_positional_arguments.hpp b/test/test_positional_arguments.hpp index 6e605f1..de430a9 100644 --- a/test/test_positional_arguments.hpp +++ b/test/test_positional_arguments.hpp @@ -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>("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>("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>("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("square") == 225); -} \ No newline at end of file +}