diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index cb20ee7..aeb8943 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -1167,6 +1167,11 @@ public: return out.str(); } + void add_subparser(ArgumentParser& parser) { + auto it = m_subparsers.emplace(std::cend(m_subparsers), parser); + m_subparser_map.insert_or_assign(parser.m_program_name, it); + } + private: /* * @throws std::runtime_error in case of any invalid argument @@ -1181,6 +1186,21 @@ private: 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; + return subparser_it->second->get().parse_args(unprocessed_arguments); + } + throw std::runtime_error( "Maximum number of positional arguments exceeded"); } @@ -1226,9 +1246,10 @@ private: return max_size; } - using list_iterator = std::list::iterator; + using argument_it = std::list::iterator; + using argument_parser_it = std::list>::iterator; - void index_argument(list_iterator it) { + void index_argument(argument_it it) { for (const auto &name : std::as_const(it->m_names)) { m_argument_map.insert_or_assign(name, it); } @@ -1241,7 +1262,9 @@ private: bool m_is_parsed = false; std::list m_positional_arguments; std::list m_optional_arguments; - std::map> m_argument_map; + std::map> m_argument_map; + std::list> m_subparsers; + std::map> m_subparser_map; }; } // namespace argparse diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3c502d9..1db364c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -46,6 +46,7 @@ file(GLOB ARGPARSE_TEST_SOURCES test_scan.cpp test_value_semantics.cpp test_version.cpp + test_subparsers.cpp ) set_source_files_properties(main.cpp PROPERTIES diff --git a/test/test_subparsers.cpp b/test/test_subparsers.cpp new file mode 100644 index 0000000..19d4e60 --- /dev/null +++ b/test/test_subparsers.cpp @@ -0,0 +1,153 @@ +#include +#include +#include + +using doctest::test_suite; + +TEST_CASE("Add subparsers" * test_suite("subparsers")) { + 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"); + + program.add_subparser(command_1); + program.add_subparser(command_2); + + program.parse_args({ "test", "--output", "thrust_profile.csv" }); + REQUIRE(program.get("--output") == "thrust_profile.csv"); +} + +TEST_CASE("Parse subparser command" * test_suite("subparsers")) { + 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("command 1") { + program.parse_args({ "test", "add", "file1.txt", "file2.txt" }); + REQUIRE(command_1.is_used("file")); + REQUIRE((command_1.get>("file") == std::vector{"file1.txt", "file2.txt"})); + } + + SUBCASE("command 2") { + program.parse_args({ "test", "clean", "--fullclean" }); + REQUIRE(command_2.get("--fullclean") == true); + } +} + +TEST_CASE("Parse subparser command with optional argument" * test_suite("subparsers")) { + argparse::ArgumentParser program("test"); + program.add_argument("--verbose") + .default_value(false) + .implicit_value(true); + + argparse::ArgumentParser command_1("add"); + command_1.add_argument("file"); + + 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("Optional argument BEFORE subcommand") { + program.parse_args({ "test", "--verbose", "clean", "--fullclean" }); + REQUIRE(program.get("--verbose") == true); + REQUIRE(command_2.get("--fullclean") == true); + } + + SUBCASE("Optional argument AFTER subcommand") { + REQUIRE_THROWS_WITH_AS(program.parse_args({ "test", "clean", "--fullclean", "--verbose" }), + "Unknown argument: --verbose", std::runtime_error); + } +} + +TEST_CASE("Parse git commands" * test_suite("subparsers")) { + argparse::ArgumentParser program("git"); + + argparse::ArgumentParser add_command("add"); + add_command.add_argument("files") + .remaining(); + + argparse::ArgumentParser commit_command("commit"); + commit_command.add_argument("-a") + .default_value(false) + .implicit_value(true); + + commit_command.add_argument("-m"); + + argparse::ArgumentParser catfile_command("cat-file"); + catfile_command.add_argument("-t"); + catfile_command.add_argument("-p"); + + argparse::ArgumentParser submodule_command("submodule"); + argparse::ArgumentParser submodule_update_command("update"); + submodule_update_command.add_argument("--init") + .default_value(false) + .implicit_value(true); + submodule_update_command.add_argument("--recursive") + .default_value(false) + .implicit_value(true); + submodule_command.add_subparser(submodule_update_command); + + program.add_subparser(add_command); + program.add_subparser(commit_command); + program.add_subparser(catfile_command); + program.add_subparser(submodule_command); + + SUBCASE("git add") { + program.parse_args({ "git", "add", "main.cpp", "foo.hpp", "foo.cpp" }); + REQUIRE((add_command.get>("files") == std::vector{"main.cpp", "foo.hpp", "foo.cpp"})); + } + + SUBCASE("git commit") { + program.parse_args({ "git", "commit", "-am", "Initial commit" }); + REQUIRE(commit_command.get("-a") == true); + REQUIRE(commit_command.get("-m") == std::string{"Initial commit"}); + } + + SUBCASE("git cat-file -t") { + program.parse_args({ "git", "cat-file", "-t", "3739f5" }); + REQUIRE(catfile_command.get("-t") == std::string{"3739f5"}); + } + + SUBCASE("git cat-file -p") { + program.parse_args({ "git", "cat-file", "-p", "3739f5" }); + REQUIRE(catfile_command.get("-p") == std::string{"3739f5"}); + } + + SUBCASE("git submodule update") { + program.parse_args({ "git", "submodule", "update" }); + } + + SUBCASE("git submodule update --init") { + program.parse_args({ "git", "submodule", "update", "--init" }); + REQUIRE(submodule_update_command.get("--init") == true); + REQUIRE(submodule_update_command.get("--recursive") == false); + } + + SUBCASE("git submodule update --recursive") { + program.parse_args({ "git", "submodule", "update", "--recursive" }); + REQUIRE(submodule_update_command.get("--recursive") == true); + } + + SUBCASE("git submodule update --init --recursive") { + program.parse_args({ "git", "submodule", "update", "--init", "--recursive" }); + REQUIRE(submodule_update_command.get("--init") == true); + REQUIRE(submodule_update_command.get("--recursive") == true); + } +} \ No newline at end of file