From ef1d6f5240a55ff533dd825ca076c665d023810d Mon Sep 17 00:00:00 2001 From: Enrico Seiler Date: Thu, 19 Sep 2024 22:23:44 +0200 Subject: [PATCH] [FEATURE] recursive subcommands --- doc/howto/subcommand_parser/index.md | 7 +++ include/sharg/parser.hpp | 27 +++++++-- test/snippet/add_subcommands.cpp | 65 ++++++++++++++++++++++ test/snippet/add_subcommands.out | 3 + test/snippet/add_subcommands.out.license | 3 + test/unit/detail/format_cwl_test.cpp | 70 ++++++++++++++++++++++++ test/unit/parser/subcommand_test.cpp | 26 +++++++++ 7 files changed, 197 insertions(+), 4 deletions(-) create mode 100644 test/snippet/add_subcommands.cpp create mode 100644 test/snippet/add_subcommands.out create mode 100644 test/snippet/add_subcommands.out.license diff --git a/doc/howto/subcommand_parser/index.md b/doc/howto/subcommand_parser/index.md index c1f01c43..c96bfaec 100644 --- a/doc/howto/subcommand_parser/index.md +++ b/doc/howto/subcommand_parser/index.md @@ -51,3 +51,10 @@ followed by the keyword `push` which in this case triggers printing the help pag That's it. Here is a full example of a subcommand parser you can try and adjust to your needs: \include doc/howto/subcommand_parser/subcommand_parse.cpp + +# Recursive subcommands + +If you want to have subcommands with subcommands, you can add subcommands to the sub-parser +with sharg::parser::add_subcommands(): + +\include test/snippet/add_subcommands.cpp diff --git a/include/sharg/parser.hpp b/include/sharg/parser.hpp index be1b753d..7f996ae0 100644 --- a/include/sharg/parser.hpp +++ b/include/sharg/parser.hpp @@ -169,8 +169,6 @@ class parser * \param[in] version_updates Notify users about version updates (default sharg::update_notifications::on). * \param[in] subcommands A list of subcommands (see \link subcommand_parse subcommand parsing \endlink). * - * \throws sharg::design_error if the application name contains illegal characters. - * * The application name must only contain alpha-numeric characters, `_` or `-` , * i.e. the following regex must evaluate to true: `"^[a-zA-Z0-9_-]+$"` . * @@ -185,9 +183,9 @@ class parser update_notifications version_updates = update_notifications::on, std::vector subcommands = {}) : version_check_dev_decision{version_updates}, - subcommands{std::move(subcommands)}, arguments{arguments} { + add_subcommands(subcommands); info.app_name = app_name; } @@ -352,7 +350,7 @@ class parser * related code and should be enclosed in a try catch block as the parser may throw. * * \throws sharg::design_error if this function was already called before. - * + * \throws sharg::design_error if the application name or subcommands contain illegal characters. * \throws sharg::option_declared_multiple_times if an option that is not a list was declared multiple times. * \throws sharg::user_input_error if an incorrect argument is given as (positional) option value. * \throws sharg::required_option_missing if the user did not provide a required option. @@ -639,6 +637,27 @@ class parser operations.push_back(std::move(operation)); } + + /*!\brief Adds subcommands to the parser. + * \param[in] subcommands A list of subcommands. + * \details + * Adds subcommands to the current parser. The list of subcommands is sorted and duplicates are removed. + * + * ### Example + * + * \include test/snippet/add_subcommands.cpp + * + * \experimentalapi{Experimental since version 1.1.2} + */ + void add_subcommands(std::vector const & subcommands) + { + auto & parser_subcommands = this->subcommands; + parser_subcommands.insert(parser_subcommands.end(), subcommands.cbegin(), subcommands.cend()); + + std::ranges::sort(parser_subcommands); + auto const [first, last] = std::ranges::unique(parser_subcommands); + parser_subcommands.erase(first, last); + } //!\} /*!\brief Aggregates all parser related meta data (see sharg::parser_meta_data struct). diff --git a/test/snippet/add_subcommands.cpp b/test/snippet/add_subcommands.cpp new file mode 100644 index 00000000..2499ea0c --- /dev/null +++ b/test/snippet/add_subcommands.cpp @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2006-2024 Knut Reinert & Freie Universität Berlin +// SPDX-FileCopyrightText: 2016-2024 Knut Reinert & MPI für molekulare Genetik +// SPDX-License-Identifier: CC0-1.0 + +#include + +void run(std::vector const & arguments) +{ + sharg::parser git_parser{"git", arguments, sharg::update_notifications::off, {"pull", "push"}}; + git_parser.add_subcommands({"remote"}); + git_parser.parse(); + + sharg::parser & sub_parser = git_parser.get_sub_parser(); + + if (sub_parser.info.app_name == std::string_view{"git-pull"}) + { + auto & pull_parser = sub_parser; + std::string repository{}; + pull_parser.add_positional_option(repository, sharg::config{}); + pull_parser.parse(); + } + else if (sub_parser.info.app_name == std::string_view{"git-push"}) + { + auto & push_parser = sub_parser; + std::string repository{}; + push_parser.add_positional_option(repository, sharg::config{}); + push_parser.parse(); + } + else if (sub_parser.info.app_name == std::string_view{"git-remote"}) + { + auto & remote_parser = sub_parser; + remote_parser.add_subcommands({"set-url", "show"}); + remote_parser.parse(); + + sharg::parser & recursive_sub_parser = remote_parser.get_sub_parser(); + + if (recursive_sub_parser.info.app_name == std::string_view{"git-remote-set-url"}) + { + auto & set_url_parser = recursive_sub_parser; + std::string repository{}; + set_url_parser.add_positional_option(repository, sharg::config{}); + set_url_parser.parse(); + } + else if (recursive_sub_parser.info.app_name == std::string_view{"git-remote-show"}) + { + auto & show_parser = recursive_sub_parser; + show_parser.parse(); + } + } +} + +int main(int argc, char ** argv) +{ + try + { + run({argv, argv + argc}); + } + catch (sharg::parser_error const & ext) + { + std::cerr << "[Error] " << ext.what() << '\n'; + std::exit(-1); + } + + return 0; +} diff --git a/test/snippet/add_subcommands.out b/test/snippet/add_subcommands.out new file mode 100644 index 00000000..43c24aef --- /dev/null +++ b/test/snippet/add_subcommands.out @@ -0,0 +1,3 @@ +git +=== + Try -h or --help for more information. diff --git a/test/snippet/add_subcommands.out.license b/test/snippet/add_subcommands.out.license new file mode 100644 index 00000000..b8b3e609 --- /dev/null +++ b/test/snippet/add_subcommands.out.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2006-2024 Knut Reinert & Freie Universität Berlin +SPDX-FileCopyrightText: 2016-2024 Knut Reinert & MPI für molekulare Genetik +SPDX-License-Identifier: CC0-1.0 diff --git a/test/unit/detail/format_cwl_test.cpp b/test/unit/detail/format_cwl_test.cpp index ee24c14f..7f967b25 100644 --- a/test/unit/detail/format_cwl_test.cpp +++ b/test/unit/detail/format_cwl_test.cpp @@ -277,4 +277,74 @@ TEST_F(format_cwl_test, subparser) " - index\n"; EXPECT_EQ(get_parse_cout_on_exit(sub_parser), expected_short); } + +TEST_F(format_cwl_test, subsubparser) +{ + // Create variables for the arguments + int option_value{5}; + std::string option_value_string{}; + std::filesystem::path option_value_path{}; + + // Create the dummy parser. + auto parser = get_subcommand_parser({"index", "show", "--export-help", "cwl"}, {"index"}); + + EXPECT_NO_THROW(parser.parse()); + + auto & sub_parser = parser.get_sub_parser(); + ASSERT_EQ(sub_parser.info.app_name, "test_parser-index"); + + sub_parser.add_subcommands({"show"}); + EXPECT_NO_THROW(sub_parser.parse()); + + auto & sub_sub_parser = sub_parser.get_sub_parser(); + sub_sub_parser.add_option(option_value, + sharg::config{.short_id = 'j', + .long_id = "jint", + .description = "this is a required int option.", + .required = true}); + sub_sub_parser.add_option(option_value_string, + sharg::config{.short_id = 's', + .long_id = "string", + .description = "this is a string option (advanced).", + .advanced = true, + .required = false}); + sub_sub_parser.add_option(option_value_path, + sharg::config{.short_id = '\0', + .long_id = "path04", + .description = "a output file.", + .validator = sharg::output_file_validator{}}); + + std::string expected_short = + "label: test_parser-index-show\n" + "doc: \"\"\n" + "inputs:\n" + " jint:\n" + " doc: this is a required int option.\n" + " type: long\n" + " inputBinding:\n" + " prefix: --jint\n" + " string:\n" + " doc: \"this is a string option (advanced). Default: \\\"\\\"\"\n" + " type: string?\n" + " inputBinding:\n" + " prefix: --string\n" + " path04:\n" + " doc: \"a output file. Default: \\\"\\\". The output file must not exist already and write permissions " + "must be granted.\"\n" + " type: string?\n" + " inputBinding:\n" + " prefix: --path04\n" + "outputs:\n" + " path04:\n" + " type: File?\n" + " outputBinding:\n" + " glob: $(inputs.path04)\n" + "cwlVersion: v1.2\n" + "class: CommandLineTool\n" + "baseCommand:\n" + " - test_parser\n" + " - index\n" + " - show\n"; + EXPECT_EQ(get_parse_cout_on_exit(sub_sub_parser), expected_short); +} #endif diff --git a/test/unit/parser/subcommand_test.cpp b/test/unit/parser/subcommand_test.cpp index 6fd2cbfb..6d4c6460 100644 --- a/test/unit/parser/subcommand_test.cpp +++ b/test/unit/parser/subcommand_test.cpp @@ -245,3 +245,29 @@ TEST_F(subcommand_test, option_value_is_special_command) EXPECT_NO_THROW(parser.parse()); EXPECT_EQ(value, "--help"); } + +TEST_F(subcommand_test, recursive_subcommands) +{ + auto parser = get_subcommand_parser({"index", "show", "--help"}, {"index"}); + EXPECT_NO_THROW(parser.parse()); + + auto & sub_parser = parser.get_sub_parser(); + ASSERT_EQ(sub_parser.info.app_name, "test_parser-index"); + sub_parser.add_subcommands({"show"}); + EXPECT_NO_THROW(sub_parser.parse()); + + auto & sub_sub_parser = sub_parser.get_sub_parser(); + ASSERT_EQ(sub_sub_parser.info.app_name, "test_parser-index-show"); + clear_and_add_option(sub_sub_parser); + + std::string expected_sub_sub_full_help = "test_parser-index-show\n" + "======================\n" + "\n" + "OPTIONS\n" + " -o (std::string)\n" + " Default: \"\"\n" + "\n" + + basic_options_str + '\n' + version_str("-index-show"); + + EXPECT_EQ(get_parse_cout_on_exit(sub_sub_parser), expected_sub_sub_full_help); +}