diff --git a/README.md b/README.md index a318b00..c572264 100644 --- a/README.md +++ b/README.md @@ -402,6 +402,29 @@ catch (const std::runtime_error& err) { auto query_point = program.get>("--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 are optional arguments that are combined and provided as a single argument. Example: ```ps -aux``` diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 112534b..c9a256e 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -38,6 +38,7 @@ SOFTWARE. #include #include #include +#include #include #include #include @@ -327,6 +328,12 @@ template struct parse_number { } // namespace details +enum class nargs_pattern { + optional, + any, + at_least_one +}; + enum class default_arguments : unsigned int { none = 0, help = 1, @@ -383,7 +390,7 @@ public: Argument &implicit_value(std::any value) { m_implicit_value = std::move(value); - m_num_args = 0; + m_num_args_range = NArgsRange{0, 0}; return *this; } @@ -452,17 +459,34 @@ public: return *this; } - Argument &nargs(int num_args) { - if (num_args < 0) { - throw std::logic_error("Number of arguments must be non-negative"); + Argument &nargs(std::size_t num_args) { + m_num_args_range = NArgsRange{num_args, num_args}; + 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::max()}; + break; + case nargs_pattern::at_least_one: + m_num_args_range = NArgsRange{1, std::numeric_limits::max()}; + break; } - m_num_args = num_args; return *this; } Argument &remaining() { - m_num_args = -1; - return *this; + m_accepts_optional_like_value = true; + return nargs(nargs_pattern::any); } template @@ -473,16 +497,23 @@ public: } m_is_used = true; 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); std::visit([](const auto &f) { f({}); }, m_action); return start; - } - if (m_num_args <= 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"); + } else if ((dist = static_cast(std::distance(start, end))) >= num_args_min) { + if (num_args_max < dist) { + end = std::next(start, num_args_max); + } + if (!m_accepts_optional_like_value) { + end = std::find_if(start, end, Argument::is_optional); + dist = static_cast(std::distance(start, end)); + if (dist < num_args_min) { + throw std::runtime_error("Too few arguments"); } } @@ -494,9 +525,8 @@ public: void operator()(void_action &f) { std::for_each(first, last, f); if (!self.m_default_value.has_value()) { - if (auto expected = self.maybe_nargs()) { - self.m_values.resize(*expected); - } + if (!self.m_accepts_optional_like_value) + self.m_values.resize(std::distance(first, last)); } } @@ -517,38 +547,21 @@ public: * @throws std::runtime_error if argument values are not valid */ void validate() const { - if (auto expected = maybe_nargs()) { - if (m_is_optional) { - // TODO: check if an implicit value was programmed for this argument - if (!m_is_used && !m_default_value.has_value() && m_is_required) { - std::stringstream stream; - stream << m_names[0] << ": required."; - throw std::runtime_error(stream.str()); - } - if (m_is_used && m_is_required && m_values.empty()) { - std::stringstream stream; - stream << m_used_name << ": no value provided."; - throw std::runtime_error(stream.str()); - } - } else if (m_values.size() != expected && !m_default_value.has_value()) { - std::stringstream stream; - if (!m_used_name.empty()) { - stream << m_used_name << ": "; - } - stream << *expected << " argument(s) expected. " << m_values.size() - << " provided."; - throw std::runtime_error(stream.str()); + if (m_is_optional) { + // TODO: check if an implicit value was programmed for this argument + if (!m_is_used && !m_default_value.has_value() && m_is_required) { + throw_required_arg_not_used_error(); + } + if (m_is_used && m_is_required && m_values.empty()) { + throw_required_arg_no_value_provided_error(); + } + } else { + if (!m_num_args_range.contains(m_values.size()) && !m_default_value.has_value()) { + throw_nargs_range_validation_error(); } } } - auto maybe_nargs() const -> std::optional { - if (m_num_args < 0) { - return std::nullopt; - } - return static_cast(m_num_args); - } - std::size_t get_arguments_length() const { return std::accumulate(std::begin(m_names), std::end(m_names), std::size_t(0), [](const auto &sum, const auto &s) { @@ -600,6 +613,66 @@ public: } 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::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::eof(); static auto lookahead(std::string_view s) -> int { @@ -789,6 +862,10 @@ private: } if (m_default_value.has_value()) { return std::any_cast(m_default_value); + } else { + if constexpr (details::IsContainer) + if (!m_accepts_optional_like_value) + return any_cast_container(m_values); } throw std::logic_error("No value provided for '" + m_names.back() + "'."); } @@ -834,7 +911,8 @@ private: std::in_place_type, [](const std::string &value) { return value; }}; std::vector 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_required : true; bool m_is_repeatable : true; diff --git a/test/test_actions.cpp b/test/test_actions.cpp index 7c5fd10..5b0ef76 100644 --- a/test/test_actions.cpp +++ b/test/test_actions.cpp @@ -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")) { argparse::ArgumentParser program("sum"); 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); }, std::ref(result)); @@ -134,6 +134,19 @@ TEST_CASE("Users can use actions on remaining arguments" * 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_suite("actions")) { argparse::ArgumentParser program("test"); diff --git a/test/test_optional_arguments.cpp b/test/test_optional_arguments.cpp index 4d50254..5f90984 100644 --- a/test/test_optional_arguments.cpp +++ b/test/test_optional_arguments.cpp @@ -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>("-i"); + REQUIRE(i.size() == 0); + + auto s = program.get>("-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>("-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>("-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>("-s"); + REQUIRE(s.size() == 0); + + auto input = program.get>("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>("-s"); + REQUIRE(s.size() == 3); + REQUIRE(s[0] == "ok"); + REQUIRE(s[1] == "this"); + REQUIRE(s[2] == "works"); + + auto input = program.get>("input"); + REQUIRE(input.size() == 2); + REQUIRE(input[0] == "foo"); + REQUIRE(input[1] == "bar"); + } + } + } +} + TEST_CASE("Parse arguments of different types" * test_suite("optional_arguments")) { using namespace std::literals; diff --git a/test/test_positional_arguments.cpp b/test/test_positional_arguments.cpp index 4e2489b..459f5dc 100644 --- a/test/test_positional_arguments.cpp +++ b/test/test_positional_arguments.cpp @@ -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" })); } +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>("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>("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>("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>("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>("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>("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>("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_suite("positional_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")) { 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")) {