Skip to content

Commit

Permalink
Merge pull request #233 from eseiler/misc/subcommands
Browse files Browse the repository at this point in the history
[MISC] Allow recursive subcommands
  • Loading branch information
SGSSGene authored Sep 23, 2024
2 parents 7534545 + ef1d6f5 commit ef08477
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 4 deletions.
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);
}

0 comments on commit ef08477

Please sign in to comment.