diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6bef83da..e09b8bad62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ If possible, provide tooling that performs the changes, e.g. a shell-script. #### Argument parser * Simplified reading file extensions from formatted files in the input/output file validators. +* Enable subcommand argument parsing ([How-to](https://docs.seqan.de/seqan/3-master-user/subcommand_arg_parse.html)). #### Core * Added traits for "metaprogramming" with `seqan3::type_list` and type packs. diff --git a/doc/howto/subcommand_argument_parser/index.md b/doc/howto/subcommand_argument_parser/index.md new file mode 100644 index 0000000000..605f7df87d --- /dev/null +++ b/doc/howto/subcommand_argument_parser/index.md @@ -0,0 +1,47 @@ +# How to write an argument parser with subcommands {#subcommand_arg_parse} + +[TOC] + +This HowTo shows you how to write an argument parser with subcommand like `git push` using SeqAn3. + +\tutorial_head{Easy, 15 min, \ref tutorial_argument_parser, } + +# Motivation + +A common use case for command line tools, e.g. `git`, is to have multiple subcommands, e.g. `git fetch` or `git push`. +Each subcommand has its own set of options and its own help page. +This HowTo explains how this can be done with the seqan3::argument_parser and serves as a copy'n'paste source. +If you are new to SeqAn, we recommend to do the basic +\link tutorial_argument_parser argument parser tutorial \endlink before you read further. + +# A subcommand argument parser + +In order to keep parsing with subcommands straightforward and simple, +the seqan3::argument_parser provides an advanced interface that internally takes care of the correct input parsing. + +You simply need to specify the names of the subcommands when constructing your top-level argument parser: + +\snippet doc/howto/subcommand_argument_parser/subcommand_arg_parse.cpp construction + +\attention You can still add flags to your top-level parser if needed but **no (positional) options**. +This avoids ambiguous parsing (e.g. subcommand fasta given file extension fasta +`./myfasta_parser --filext fasta fasta ...`). + +After calling seqan3::argument_parser::parse() on your top-level parser, +you can then access the sub-parser via the function seqan3::argument_parser::get_sub_parser(): + +\snippet doc/howto/subcommand_argument_parser/subcommand_arg_parse.cpp get_sub_parser + +The sub-parser's **seqan3::argument_parser::info::app_name will be set to the user chosen sub command**. +For example, if the user calls + +``` +max$ ./mygit push -h +``` + +then the sub-parser will be named `mygit-push` and will be instantiated with all arguments +followed by the keyword `push` which in this case triggers printing the help page (`-h`). + +That's it. Here is a full example of a subcommand argument parser you can try and adjust to your needs: + +\include doc/howto/subcommand_argument_parser/subcommand_arg_parse.cpp diff --git a/doc/howto/subcommand_argument_parser/subcommand_arg_parse.cpp b/doc/howto/subcommand_argument_parser/subcommand_arg_parse.cpp new file mode 100644 index 0000000000..fee1f98dfb --- /dev/null +++ b/doc/howto/subcommand_argument_parser/subcommand_arg_parse.cpp @@ -0,0 +1,110 @@ +#include + +using namespace seqan3; + +// ===================================================================================================================== +// pull +// ===================================================================================================================== + +struct pull_arguments +{ + std::string repository{}; + std::string branch{}; + bool progress{false}; +}; + +int run_git_pull(argument_parser & parser) +{ + pull_arguments args{}; + + parser.add_positional_option(args.repository, "The repository name to pull from."); + parser.add_positional_option(args.branch, "The branch name to pull from."); + + try + { + parser.parse(); + } + catch (parser_invalid_argument const & ext) + { + debug_stream << "[Error git pull] " << ext.what() << "\n"; + return -1; + } + + debug_stream << "Git pull with repository " << args.repository << " and branch " << args.branch << std::endl; + + return 0; +} + +// ===================================================================================================================== +// push +// ===================================================================================================================== + +struct push_arguments +{ + std::string repository{}; + std::vector branches{}; + bool push_all{false}; +}; + +int run_git_push(argument_parser & parser) +{ + push_arguments args{}; + + parser.add_positional_option(args.repository, "The repository name to push to."); + parser.add_positional_option(args.branches, "The branch names to push (if none are given, push current)."); + + try + { + parser.parse(); + } + catch (parser_invalid_argument const & ext) + { + debug_stream << "[Error git push] " << ext.what() << "\n"; + return -1; + } + + debug_stream << "Git push with repository " << args.repository << " and branches " << args.branches << std::endl; + + return 0; +} + +// ===================================================================================================================== +// main +// ===================================================================================================================== + +int main(int argc, char const ** argv) +{ + //![construction] + argument_parser top_level_parser{"mygit", argc, argv, true, {"push", "pull"}}; + //![construction] + + // Add information and flags to your top-level parser just as you would do with a normal one. + // Note that all flags directed at the top-level parser must be specified BEFORE the subcommand key word. + // Because of ambiguity, we do not allow any (positional) options for the top-level parser. + top_level_parser.info.description.push_back("You can push or pull from a remote repository."); + bool flag{false}; + top_level_parser.add_flag(flag, 'f', "flag", "some flag"); + + try + { + top_level_parser.parse(); // trigger command line parsing + } + catch (parser_invalid_argument const & ext) // catch user errors + { + debug_stream << "[Error] " << ext.what() << "\n"; // customise your error message + return -1; + } + + //![get_sub_parser] + argument_parser & sub_parser = top_level_parser.get_sub_parser(); // hold a reference to the sub_parser + //![get_sub_parser] + + std::cout << "Proceed to sub parser." << std::endl; + + if (sub_parser.info.app_name == std::string_view{"mygit-pull"}) + run_git_pull(sub_parser); + else if (sub_parser.info.app_name == std::string_view{"mygit-push"}) + run_git_push(sub_parser); + else + throw std::logic_error{"I do not know sub parser " + sub_parser.info.app_name}; +} diff --git a/doc/tutorial/argument_parser/index.md b/doc/tutorial/argument_parser/index.md index 94f0223a52..20fb706592 100644 --- a/doc/tutorial/argument_parser/index.md +++ b/doc/tutorial/argument_parser/index.md @@ -466,6 +466,13 @@ that can serve as a copy'n'paste source for your application. \include doc/tutorial/argument_parser/solution6.cpp \endsolution +# Subcommand argument parsing + +Many applications provide several sub programs, e.g. `git` comes with many functionalities like `git push`, +`git pull`, `git checkout`, etc. each having their own help page. +If you are interested in how this subcommand parsing can be done with the seqan3::argument_parser, +take a look at our \link subcommand_arg_parse HowTo\endlink. + # Update Notifications When you run a SeqAn-based application for the first time, you will likely be asked about "update notifications". diff --git a/include/seqan3/argument_parser/argument_parser.hpp b/include/seqan3/argument_parser/argument_parser.hpp index 4c90080626..9ba89dccb6 100644 --- a/include/seqan3/argument_parser/argument_parser.hpp +++ b/include/seqan3/argument_parser/argument_parser.hpp @@ -29,6 +29,7 @@ #include #include #include +#include #include namespace seqan3 @@ -166,6 +167,7 @@ class argument_parser * \param[in] argc The number of command line arguments. * \param[in] argv The command line arguments to parse. * \param[in] version_check Notify users about app version updates (default true). + * \param[in] subcommands A list of subcommands (see \link subcommand_arg_parse subcommand parsing \endlink). * * \throws seqan3::parser_design_error if the application name contains illegal characters. * @@ -178,14 +180,19 @@ class argument_parser argument_parser(std::string const app_name, int const argc, char const * const * const argv, - bool version_check = true) : + bool version_check = true, + std::vector subcommands = {}) : version_check_dev_decision{version_check} { if (!std::regex_match(app_name, app_name_regex)) throw parser_design_error{"The application name must only contain alpha-numeric characters " "or '_' and '-' (regex: \"^[a-zA-Z0-9_-]+$\")."}; + for (auto & sub : subcommands) + if (!std::regex_match(sub, std::regex{"^[a-zA-Z0-9_]+$"})) + throw parser_design_error{"The subcommand name must only contain alpha-numeric characters or '_'."}; + info.app_name = std::move(app_name); - init(argc, argv); + init(argc, argv, std::move(subcommands)); } //!\brief The destructor. @@ -217,6 +224,8 @@ class argument_parser * \param[in] desc The description of the option to be shown in the help page. * \param[in] spec Advanced option specification, see seqan3::option_spec. * \param[in] validator The validator applied to the value after parsing (callable). + * + * \throws seqan3::parser_design_error */ template > //!\cond @@ -231,6 +240,9 @@ class argument_parser option_spec const & spec = option_spec::DEFAULT, validator_type validator = validator_type{}) // copy to bind rvalues { + if (sub_parser != nullptr) + throw parser_design_error{"You may only specify flags for the top-level parser."}; + verify_identifiers(short_id, long_id); // copy variables into the lambda because the calls are pushed to a stack // and the references would go out of scope. @@ -287,6 +299,9 @@ class argument_parser std::string const & desc, validator_type validator = validator_type{}) // copy to bind rvalues { + if (sub_parser != nullptr) + throw parser_design_error{"You may only specify flags for the top-level parser."}; + if (has_positional_list_option) throw parser_design_error{"You added a positional option with a list value before so you cannot add " "any other positional options."}; @@ -389,6 +404,19 @@ class argument_parser parse_was_called = true; } + //!\brief Returns a reference to the sub-parser instance if + //! \link subcommand_arg_parse subcommand parsing \endlink was enabled. + argument_parser & get_sub_parser() + { + if (sub_parser == nullptr) + { + throw parser_design_error("You did not enable subcommand parsing on construction " + "so you cannot access the sub-parser!"); + } + + return *sub_parser; + } + //!\name Structuring the Help Page //!\{ @@ -511,13 +539,17 @@ class argument_parser //!\brief The future object that keeps track of the detached version check call thread. std::future version_check_future; - //!\brief Validates the application name to ensure an escaped server call + //!\brief Validates the application name to ensure an escaped server call. std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"}; + //!\brief Stores the sub-parser in case \link subcommand_arg_parse subcommand parsing \endlink is enabled. + std::unique_ptr sub_parser{nullptr}; + /*!\brief Initializes the seqan3::argument_parser class on construction. * - * \param[in] argc The number of command line arguments. - * \param[in] argv The command line arguments. + * \param[in] argc The number of command line arguments. + * \param[in] argv The command line arguments. + * \param[in] subcommands The subcommand key words to split command line arguments into top-level and sub-parser. * * \throws seqan3::parser_invalid_argument * @@ -542,7 +574,7 @@ class argument_parser * If `--export-help` is specified with a value other than html/man or ctd * a parser_invalid_argument is thrown. */ - void init(int argc, char const * const * const argv) + void init(int argc, char const * const * const argv, std::vector const & subcommands) { // cash command line input, in case --version-check is specified but shall not be passed to format_parse() std::vector argv_new{}; @@ -553,26 +585,33 @@ class argument_parser return; } + bool special_format_was_set{false}; + for(int i = 1, argv_len = argc; i < argv_len; ++i) // start at 1 to skip binary name { std::string arg{argv[i]}; + if (std::ranges::find(subcommands, arg) != subcommands.end()) + { + sub_parser = std::make_unique(info.app_name + "-" + arg, argc - i, argv + i, false); + break; + } if (arg == "-h" || arg == "--help") { - format = detail::format_help{false}; + format = detail::format_help{subcommands, false}; init_standard_options(); - return; + special_format_was_set = true; } else if (arg == "-hh" || arg == "--advanced-help") { - format = detail::format_help{true}; + format = detail::format_help{subcommands, true}; init_standard_options(); - return; + special_format_was_set = true; } else if (arg == "--version") { format = detail::format_version{}; - return; + special_format_was_set = true; } else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed { @@ -590,9 +629,9 @@ class argument_parser } if (export_format == "html") - format = detail::format_html{}; + format = detail::format_html{subcommands}; else if (export_format == "man") - format = detail::format_man{}; + format = detail::format_man{subcommands}; // TODO (smehringer) use when CTD support is available // else if (export_format == "ctd") // format = detail::format_ctd{}; @@ -600,12 +639,12 @@ class argument_parser throw validation_failed{"Validation failed for option --export-help: " "Value must be one of [html, man]"}; init_standard_options(); - return; + special_format_was_set = true; } else if (arg == "--copyright") { format = detail::format_copyright{}; - return; + special_format_was_set = true; } else if (arg == "--version-check") { @@ -629,7 +668,16 @@ class argument_parser } } - format = detail::format_parse(argc, std::move(argv_new)); + if (!special_format_was_set) + { + if (!subcommands.empty() && sub_parser == nullptr) + { + throw parser_invalid_argument{detail::to_string("Please specify which sub program you want to use ", + "(one of ", subcommands, "). Use -h/--help for more information.")}; + } + + format = detail::format_parse(argc, std::move(argv_new)); + } } //!\brief Adds standard options to the help page. @@ -705,7 +753,7 @@ class argument_parser detail::format_html, detail::format_man, detail::format_copyright/*, - detail::format_ctd*/> format{detail::format_help(0)}; + detail::format_ctd*/> format{detail::format_help{{}, false}}; // Will be overwritten in any case. //!\brief List of option/flag identifiers that are already used. std::set used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"}; diff --git a/include/seqan3/argument_parser/detail/format_base.hpp b/include/seqan3/argument_parser/detail/format_base.hpp index fe66a6e0ff..8479078a34 100644 --- a/include/seqan3/argument_parser/detail/format_base.hpp +++ b/include/seqan3/argument_parser/detail/format_base.hpp @@ -214,10 +214,11 @@ class format_help_base : public format_base ~format_help_base() = default; //!< Defaulted. /*!\brief Initializes a format_help_base object. + * \param[in] names A list of subcommands (see \link subcommand_arg_parse subcommand parsing \endlink). * \param[in] advanced Set to `true` to show advanced options. */ - format_help_base(bool const advanced) : - show_advanced_options{advanced} + format_help_base(std::vector const & names, bool const advanced) : + command_names{names}, show_advanced_options{advanced} {} //!\} @@ -325,6 +326,19 @@ class format_help_base : public format_base print_line(desc); } + if (!command_names.empty()) + { + derived_t().print_section("Subcommands"); + derived_t().print_line("This program must be invoked with one of the following subcommands:", false); + for (std::string const & name : command_names) + derived_t().print_line("- \\fB" + name + "\\fP", false); + derived_t().print_line("See the respective help page for further details (e.g. by calling " + + meta.app_name + " " + command_names[0] + " -h).", true); + derived_t().print_line("The following options below belong to the top-level parser and need to be " + "specified \\fBbefore\\fP the subcommand key word. Every argument after the " + "subcommand key word is passed on to the corresponding sub-parser.", true); + } + // add positional options if specified if (!positional_option_calls.empty()) derived_t().print_section("Positional Arguments"); @@ -452,6 +466,8 @@ class format_help_base : public format_base std::vector> positional_option_calls; // singled out to be printed on top //!\brief Keeps track of the number of positional options unsigned positional_option_count{0}; + //!\brief The names of subcommand programs. + std::vector command_names{}; //!\brief Whether to show advanced options or not. bool show_advanced_options{true}; }; diff --git a/include/seqan3/argument_parser/detail/format_help.hpp b/include/seqan3/argument_parser/detail/format_help.hpp index 66cd79d495..8d9347e869 100644 --- a/include/seqan3/argument_parser/detail/format_help.hpp +++ b/include/seqan3/argument_parser/detail/format_help.hpp @@ -56,8 +56,8 @@ class format_help : public format_help_base format_help & operator=(format_help &&) = default; //!< Defaulted. ~format_help() = default; //!< Defaulted. - //!\copydoc format_help_base(bool) - format_help(bool const advanced) : base_type{advanced} + //!\copydoc format_help_base(std::vector const &, bool const) + format_help(std::vector const & names, bool const advanced = false) : base_type{names, advanced} {}; //!\} diff --git a/include/seqan3/argument_parser/detail/format_html.hpp b/include/seqan3/argument_parser/detail/format_html.hpp index 9e72e32228..28b333d868 100644 --- a/include/seqan3/argument_parser/detail/format_html.hpp +++ b/include/seqan3/argument_parser/detail/format_html.hpp @@ -50,8 +50,8 @@ class format_html : public format_help_base format_html & operator=(format_html &&) = default; //!< Defaulted. ~format_html() = default; //!< Defaulted. - //!\copydoc format_help_base(bool) - format_html(bool const advanced) : base_type{advanced} + //!\copydoc format_help_base(std::vector const &, bool const) + format_html(std::vector const & names, bool const advanced = false) : base_type{names, advanced} {}; //!\} diff --git a/include/seqan3/argument_parser/detail/format_man.hpp b/include/seqan3/argument_parser/detail/format_man.hpp index 2a4bf646e6..84c0a0df84 100644 --- a/include/seqan3/argument_parser/detail/format_man.hpp +++ b/include/seqan3/argument_parser/detail/format_man.hpp @@ -51,8 +51,8 @@ class format_man : public format_help_base format_man & operator=(format_man &&) = default; //!< Defaulted. ~format_man() = default; //!< Defaulted. - //!\copydoc format_help_base(bool) - format_man(bool const advanced) : base_type{advanced} + //!\copydoc format_help_base(std::vector const &, bool const) + format_man(std::vector const & names, bool const advanced = false) : base_type{names, advanced} {}; //!\} diff --git a/test/documentation/DoxygenLayout.xml b/test/documentation/DoxygenLayout.xml index 09114e62a9..2fa7b00978 100644 --- a/test/documentation/DoxygenLayout.xml +++ b/test/documentation/DoxygenLayout.xml @@ -22,6 +22,7 @@ + diff --git a/test/unit/argument_parser/argument_parser_design_error_test.cpp b/test/unit/argument_parser/argument_parser_design_error_test.cpp index 1f0076453e..9281ffae7e 100644 --- a/test/unit/argument_parser/argument_parser_design_error_test.cpp +++ b/test/unit/argument_parser/argument_parser_design_error_test.cpp @@ -115,3 +115,36 @@ TEST(parse_test, parse_called_twice) EXPECT_THROW(parser.parse(), parser_design_error); } + +TEST(parse_test, subcommand_argument_parser_error) +{ + bool flag_value{}; + + // subcommand parsing was not enabled on construction but get_sub_parser() is called + { + const char * argv[]{"./top_level", "-f"}; + argument_parser top_level_parser{"top_level", 2, argv, false}; + top_level_parser.add_flag(flag_value, 'f', "foo", "foo bar"); + + EXPECT_NO_THROW(top_level_parser.parse()); + EXPECT_EQ(true, flag_value); + + EXPECT_THROW(top_level_parser.get_sub_parser(), parser_design_error); + } + + // subcommand key word must only contain alpha numeric characters + { + const char * argv[]{"./top_level", "-f"}; + EXPECT_THROW((argument_parser{"top_level", 2, argv, false, {"with space"}}), parser_design_error); + EXPECT_THROW((argument_parser{"top_level", 2, argv, false, {"-dash"}}), parser_design_error); + } + + // no positional/options are allowed + { + const char * argv[]{"./top_level", "foo"}; + argument_parser top_level_parser{"top_level", 2, argv, false, {"foo"}}; + + EXPECT_THROW((top_level_parser.add_option(flag_value, 'f', "foo", "foo bar")), parser_design_error); + EXPECT_THROW((top_level_parser.add_positional_option(flag_value, "foo bar")), parser_design_error); + } +} diff --git a/test/unit/argument_parser/detail/format_help_test.cpp b/test/unit/argument_parser/detail/format_help_test.cpp index 578c851f16..0b343e2aba 100644 --- a/test/unit/argument_parser/detail/format_help_test.cpp +++ b/test/unit/argument_parser/detail/format_help_test.cpp @@ -325,3 +325,37 @@ TEST(help_page_printing, copyright) EXPECT_EQ(std_cout, expected); } } + +TEST(parse_test, subcommand_argument_parser) +{ + int option_value{}; + std::string option_value2{}; + + const char * argv[]{"./test_parser", "-h"}; + argument_parser top_level_parser{"test_parser", 2, argv, true, {"sub1", "sub2"}}; + top_level_parser.info.description.push_back("description"); + top_level_parser.add_option(option_value, 'f', "foo", "foo bar."); + + testing::internal::CaptureStdout(); + EXPECT_EXIT(top_level_parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), ""); + std::string std_cout = testing::internal::GetCapturedStdout(); + + std::string expected = "test_parser\n" + "===========\n" + "DESCRIPTION\n" + " description\n" + "SUB COMMANDS\n" + " This program must be invoked with one of the following subcommands:\n" + " - sub1\n" + " - sub2\n" + " See the respective help page for further details (e.g. by calling test_parser sub1 -h)." + " The following options below belong to the top-level parser and need to be specified " + " before the subcommand key word. Every argument after the subcommand key word is " + " passed on to the corresponding sub-parser.\n" + + basic_options_str + + " -f, --foo (signed 32 bit integer)\n" + " foo bar. Default: 0.\n" + + basic_version_str; + + EXPECT_TRUE(ranges::equal((std_cout | std::view::filter(!is_space)), expected | std::view::filter(!is_space))); +} diff --git a/test/unit/argument_parser/detail/version_check_test.hpp b/test/unit/argument_parser/detail/version_check_test.hpp index 43483e1a30..b8c3dd7078 100644 --- a/test/unit/argument_parser/detail/version_check_test.hpp +++ b/test/unit/argument_parser/detail/version_check_test.hpp @@ -270,6 +270,33 @@ TEST_F(version_check, option_off) EXPECT_FALSE(std::filesystem::exists(APP_VERSION_FILENAME)) << APP_VERSION_FILENAME; EXPECT_TRUE(remove_files_from_path()); // clear files again + + // Version check option always needs to be parsed, even if special formats get selected + const char * argv2[4] = {APP_NAME.c_str(), "-h", OPTION_VERSION_CHECK, OPTION_OFF}; + + char * env{std::getenv("SEQAN3_NO_VERSION_CHECK")}; + if (env != nullptr) + unsetenv("SEQAN3_NO_VERSION_CHECK"); + + argument_parser parser{APP_NAME, 4, argv2}; + parser.info.version = "2.3.4"; + + EXPECT_EXIT(parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), ""); + + // call future.get() to artificially wait for the thread to finish and avoid + // any interference with following tests + if (parser.version_check_future.valid()) + { + EXPECT_FALSE(parser.version_check_future.get()); + } + + if (env != nullptr) + setenv("SEQAN3_NO_VERSION_CHECK", env, 1); + + // no timestamp is written since the decision was made explicitly + EXPECT_FALSE(std::filesystem::exists(APP_VERSION_FILENAME)) << APP_VERSION_FILENAME; + + EXPECT_TRUE(remove_files_from_path()); // clear files again } // case: the current argument parser has a smaller seqan version than is present in the version file diff --git a/test/unit/argument_parser/format_parse_test.cpp b/test/unit/argument_parser/format_parse_test.cpp index ad711ba399..904f3e085d 100644 --- a/test/unit/argument_parser/format_parse_test.cpp +++ b/test/unit/argument_parser/format_parse_test.cpp @@ -804,3 +804,65 @@ TEST(parse_test, version_check_option_error) EXPECT_THROW((argument_parser{"test_parser", 3, argv}), parser_invalid_argument); } } + +TEST(parse_test, subcommand_argument_parser_success) +{ + bool flag_value{}; + std::string option_value{}; + + // parsing + { + const char * argv[]{"./top_level", "-f", "sub1", "foo"}; + argument_parser top_level_parser{"top_level", 4, argv, false, {"sub1", "sub2"}}; + top_level_parser.add_flag(flag_value, 'f', "foo", "foo bar"); + + EXPECT_NO_THROW(top_level_parser.parse()); + EXPECT_EQ(true, flag_value); + + argument_parser & sub_parser = top_level_parser.get_sub_parser(); + + EXPECT_EQ(sub_parser.info.app_name, "top_level-sub1"); + + sub_parser.add_positional_option(option_value, "foo bar"); + + EXPECT_NO_THROW(sub_parser.parse()); + EXPECT_EQ("foo", option_value); + } + + // top-level help page + { + const char * argv[]{"./top_level", "-h", "-f", "sub1", "foo"}; + argument_parser top_level_parser{"top_level", 5, argv, false, {"sub1", "sub2"}}; + top_level_parser.add_flag(flag_value, 'f', "foo", "foo bar"); + + testing::internal::CaptureStdout(); + EXPECT_EXIT(top_level_parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), ""); + EXPECT_FALSE(std::string{testing::internal::GetCapturedStdout()}.empty()); + } + + // sub-parser help page + { + const char * argv[]{"./top_level", "-f", "sub1", "-h"}; + argument_parser top_level_parser{"top_level", 4, argv, false, {"sub1", "sub2"}}; + top_level_parser.add_flag(flag_value, 'f', "foo", "foo bar"); + + EXPECT_NO_THROW(top_level_parser.parse()); + EXPECT_EQ(true, flag_value); + + argument_parser & sub_parser = top_level_parser.get_sub_parser(); + + EXPECT_EQ(sub_parser.info.app_name, "top_level-sub1"); + + sub_parser.add_positional_option(option_value, "foo bar"); + + testing::internal::CaptureStdout(); + EXPECT_EXIT(sub_parser.parse(), ::testing::ExitedWithCode(EXIT_SUCCESS), ""); + EXPECT_FALSE(std::string{testing::internal::GetCapturedStdout()}.empty()); + } + + // incorrect sub command + { + const char * argv[]{"./top_level", "-f", "2", "subiddysub", "foo"}; + EXPECT_THROW((argument_parser{"top_level", 5, argv, false, {"sub1", "sub2"}}), parser_invalid_argument); + } +}