From 6e23d5b22e03886baaab07b9b25ed199e2d22169 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Wed, 21 Sep 2022 05:47:47 -0700 Subject: [PATCH 1/2] Closes #181 --- include/argparse/argparse.hpp | 101 +++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/test_parse_known_args.cpp | 82 ++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 test/test_parse_known_args.cpp diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 24c68cc4..ee9ccd99 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -1041,6 +1041,21 @@ class ArgumentParser { } } + /* Call parse_known_args_internal - which does all the work + * Then, validate the parsed arguments + * This variant is used mainly for testing + * @throws std::runtime_error in case of any invalid argument + */ + std::vector + parse_known_args(const std::vector &arguments) { + auto unknown_arguments = parse_known_args_internal(arguments); + // Check if all arguments are parsed + for ([[maybe_unused]] const auto &[unused, argument] : m_argument_map) { + argument->validate(); + } + return unknown_arguments; + } + /* Main entry point for parsing command-line arguments using this * ArgumentParser * @throws std::runtime_error in case of any invalid argument @@ -1050,6 +1065,15 @@ class ArgumentParser { parse_args({argv, argv + argc}); } + /* Main entry point for parsing command-line arguments using this + * ArgumentParser + * @throws std::runtime_error in case of any invalid argument + */ + // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays) + void parse_known_args(int argc, const char *const argv[]) { + parse_known_args({argv, argv + argc}); + } + /* Getter for options with default values. * @throws std::logic_error if parse_args() has not been previously called * @throws std::logic_error if there is no such option @@ -1262,6 +1286,83 @@ class ArgumentParser { m_is_parsed = true; } + /* + * Like parse_args_internal but collects unused args into a vector + */ + std::vector + parse_known_args_internal(const std::vector &arguments) { + + std::vector unknown_arguments{}; + + if (m_program_name.empty() && !arguments.empty()) { + m_program_name = arguments.front(); + } + auto end = std::end(arguments); + auto positional_argument_it = std::begin(m_positional_arguments); + for (auto it = std::next(std::begin(arguments)); it != end;) { + const auto ¤t_argument = *it; + if (Argument::is_positional(current_argument)) { + if (positional_argument_it == std::end(m_positional_arguments)) { + + std::string_view maybe_command = current_argument; + + // Check sub-parsers + auto subparser_it = m_subparser_map.find(maybe_command); + if (subparser_it != m_subparser_map.end()) { + + // build list of remaining args + const auto unprocessed_arguments = + std::vector(it, end); + + // invoke subparser + m_is_parsed = true; + m_subparser_used[maybe_command] = true; + return subparser_it->second->get().parse_known_args_internal( + unprocessed_arguments); + } + + // save current argument as unknown and go to next argument + unknown_arguments.push_back(current_argument); + ++it; + } else { + // current argument is the value of a positional argument + // consume it + auto argument = positional_argument_it++; + it = argument->consume(it, end); + } + continue; + } + + auto arg_map_it = m_argument_map.find(current_argument); + if (arg_map_it != m_argument_map.end()) { + auto argument = arg_map_it->second; + it = argument->consume(std::next(it), end, arg_map_it->first); + } else if (const auto &compound_arg = current_argument; + compound_arg.size() > 1 && compound_arg[0] == '-' && + compound_arg[1] != '-') { + ++it; + for (std::size_t j = 1; j < compound_arg.size(); j++) { + auto hypothetical_arg = std::string{'-', compound_arg[j]}; + auto arg_map_it2 = m_argument_map.find(hypothetical_arg); + if (arg_map_it2 != m_argument_map.end()) { + auto argument = arg_map_it2->second; + it = argument->consume(it, end, arg_map_it2->first); + } else { + unknown_arguments.push_back(current_argument); + break; + } + } + } else { + // current argument is an optional-like argument that is unknown + // save it and move to next argument + unknown_arguments.push_back(current_argument); + ++it; + } + } + m_is_parsed = true; + return unknown_arguments; + } + // Used by print_help. std::size_t get_length_of_longest_argument() const { if (m_argument_map.empty()) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1db364c5..543f4b41 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -47,6 +47,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_value_semantics.cpp test_version.cpp test_subparsers.cpp + test_parse_known_args.cpp ) set_source_files_properties(main.cpp PROPERTIES diff --git a/test/test_parse_known_args.cpp b/test/test_parse_known_args.cpp new file mode 100644 index 00000000..2a9da845 --- /dev/null +++ b/test/test_parse_known_args.cpp @@ -0,0 +1,82 @@ +#include +#include + +using doctest::test_suite; + +TEST_CASE("Parse unknown optional and positional arguments without exceptions" * + test_suite("parse_known_args")) { + argparse::ArgumentParser program("test"); + program.add_argument("--foo").implicit_value(true).default_value(false); + program.add_argument("bar"); + + SUBCASE("Parse unknown optional and positional arguments") { + auto unknown_args = + program.parse_known_args({"test", "--foo", "--badger", "BAR", "spam"}); + REQUIRE((unknown_args == std::vector{"--badger", "spam"})); + REQUIRE(program.get("--foo") == true); + REQUIRE(program.get("bar") == std::string{"BAR"}); + } + + SUBCASE("Parse unknown compound arguments") { + auto unknown_args = program.parse_known_args({"test", "-jc", "BAR"}); + REQUIRE((unknown_args == std::vector{"-jc"})); + REQUIRE(program.get("--foo") == false); + REQUIRE(program.get("bar") == std::string{"BAR"}); + } +} + +TEST_CASE("Parse unknown optional and positional arguments in subparsers " + "without exceptions" * + test_suite("parse_known_args")) { + argparse::ArgumentParser program("test"); + program.add_argument("--output"); + + argparse::ArgumentParser command_1("add"); + command_1.add_argument("file").nargs(2); + + argparse::ArgumentParser command_2("clean"); + command_2.add_argument("--fullclean") + .default_value(false) + .implicit_value(true); + + program.add_subparser(command_1); + program.add_subparser(command_2); + + SUBCASE("Parse unknown optional argument") { + auto unknown_args = + program.parse_known_args({"test", "add", "--badger", "BAR", "spam"}); + REQUIRE(program.is_subcommand_used("add") == true); + REQUIRE((command_1.get>("file") == + std::vector{"BAR", "spam"})); + REQUIRE((unknown_args == std::vector{"--badger"})); + } + + SUBCASE("Parse unknown positional argument") { + auto unknown_args = + program.parse_known_args({"test", "add", "FOO", "BAR", "spam"}); + REQUIRE(program.is_subcommand_used("add") == true); + REQUIRE((command_1.get>("file") == + std::vector{"FOO", "BAR"})); + REQUIRE((unknown_args == std::vector{"spam"})); + } + + SUBCASE("Parse unknown positional and optional arguments") { + auto unknown_args = program.parse_known_args( + {"test", "add", "--verbose", "FOO", "5", "BAR", "-jn", "spam"}); + REQUIRE(program.is_subcommand_used("add") == true); + REQUIRE((command_1.get>("file") == + std::vector{"FOO", "5"})); + REQUIRE((unknown_args == + std::vector{"--verbose", "BAR", "-jn", "spam"})); + } + + SUBCASE("Parse unknown positional and optional arguments 2") { + auto unknown_args = + program.parse_known_args({"test", "clean", "--verbose", "FOO", "5", + "BAR", "--fullclean", "-jn", "spam"}); + REQUIRE(program.is_subcommand_used("clean") == true); + REQUIRE(command_2.get("--fullclean") == true); + REQUIRE((unknown_args == std::vector{"--verbose", "FOO", "5", + "BAR", "-jn", "spam"})); + } +} \ No newline at end of file From 6a3c6e06e6fd4988170718f78308cf14864fe9d4 Mon Sep 17 00:00:00 2001 From: Pranav Srinivas Kumar Date: Wed, 21 Sep 2022 05:54:21 -0700 Subject: [PATCH 2/2] Added 'Parse Known Args' section --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 853cff24..2ac64c7b 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ * [Gathering Remaining Arguments](#gathering-remaining-arguments) * [Parent Parsers](#parent-parsers) * [Subcommands](#subcommands) + * [Parse Known Args](#parse-known-args) * [Further Examples](#further-examples) * [Construct a JSON object from a filename argument](#construct-a-json-object-from-a-filename-argument) * [Positional Arguments with Compound Toggle Arguments](#positional-arguments-with-compound-toggle-arguments) @@ -799,6 +800,28 @@ When a help message is requested from a subparser, only the help for that partic Additionally, every parser has a `.is_subcommand_used("")` member function to check if a subcommand was used. +### Parse Known Args + +Sometimes a program may only parse a few of the command-line arguments, passing the remaining arguments on to another script or program. In these cases, the `parse_known_args()` function can be useful. It works much like `parse_args()` except that it does not produce an error when extra arguments are present. Instead, it returns a list of remaining argument strings. + +```cpp +#include +#include + +int main(int argc, char *argv[]) { + argparse::ArgumentParser program("test"); + program.add_argument("--foo").implicit_value(true).default_value(false); + program.add_argument("bar"); + + auto unknown_args = + program.parse_known_args({"test", "--foo", "--badger", "BAR", "spam"}); + + assert(program.get("--foo") == true); + assert(program.get("bar") == std::string{"BAR"}); + assert((unknown_args == std::vector{"--badger", "spam"})); +} +``` + ## Further Examples ### Construct a JSON object from a filename argument