Skip to content

Commit

Permalink
Merge pull request #201 from p-ranav/feature/parse_known_args
Browse files Browse the repository at this point in the history
parse_known_args
  • Loading branch information
p-ranav authored Sep 21, 2022
2 parents 4f10f37 + 6a3c6e0 commit 4dbc910
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 0 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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("<command_name>")` 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 <argparse/argparse.hpp>
#include <cassert>

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<bool>("--foo") == true);
assert(program.get<std::string>("bar") == std::string{"BAR"});
assert((unknown_args == std::vector<std::string>{"--badger", "spam"}));
}
```
## Further Examples
### Construct a JSON object from a filename argument
Expand Down
101 changes: 101 additions & 0 deletions include/argparse/argparse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>
parse_known_args(const std::vector<std::string> &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
Expand All @@ -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
Expand Down Expand Up @@ -1262,6 +1286,83 @@ class ArgumentParser {
m_is_parsed = true;
}

/*
* Like parse_args_internal but collects unused args into a vector<string>
*/
std::vector<std::string>
parse_known_args_internal(const std::vector<std::string> &arguments) {

std::vector<std::string> 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 &current_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<std::string>(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()) {
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions test/test_parse_known_args.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include <argparse/argparse.hpp>
#include <doctest.hpp>

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<std::string>{"--badger", "spam"}));
REQUIRE(program.get<bool>("--foo") == true);
REQUIRE(program.get<std::string>("bar") == std::string{"BAR"});
}

SUBCASE("Parse unknown compound arguments") {
auto unknown_args = program.parse_known_args({"test", "-jc", "BAR"});
REQUIRE((unknown_args == std::vector<std::string>{"-jc"}));
REQUIRE(program.get<bool>("--foo") == false);
REQUIRE(program.get<std::string>("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<std::vector<std::string>>("file") ==
std::vector<std::string>{"BAR", "spam"}));
REQUIRE((unknown_args == std::vector<std::string>{"--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<std::vector<std::string>>("file") ==
std::vector<std::string>{"FOO", "BAR"}));
REQUIRE((unknown_args == std::vector<std::string>{"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<std::vector<std::string>>("file") ==
std::vector<std::string>{"FOO", "5"}));
REQUIRE((unknown_args ==
std::vector<std::string>{"--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<bool>("--fullclean") == true);
REQUIRE((unknown_args == std::vector<std::string>{"--verbose", "FOO", "5",
"BAR", "-jn", "spam"}));
}
}

0 comments on commit 4dbc910

Please sign in to comment.