Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MISC] Allow recursive subcommands #233

Merged
merged 1 commit into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/howto/subcommand_parser/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 23 additions & 4 deletions include/sharg/parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_-]+$"` .
*
Expand All @@ -185,9 +183,9 @@ class parser
update_notifications version_updates = update_notifications::on,
std::vector<std::string> subcommands = {}) :
version_check_dev_decision{version_updates},
subcommands{std::move(subcommands)},
arguments{arguments}
{
add_subcommands(subcommands);
info.app_name = app_name;
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<std::string> 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).
Expand Down
65 changes: 65 additions & 0 deletions test/snippet/add_subcommands.cpp
Original file line number Diff line number Diff line change
@@ -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 <sharg/all.hpp>

void run(std::vector<std::string> 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;
}
3 changes: 3 additions & 0 deletions test/snippet/add_subcommands.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
git
===
Try -h or --help for more information.
3 changes: 3 additions & 0 deletions test/snippet/add_subcommands.out.license
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions test/unit/detail/format_cwl_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 26 additions & 0 deletions test/unit/parser/subcommand_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading