Skip to content

Commit 2d1779b

Browse files
committed
[FEATURE] recursive subcommands
1 parent 7534545 commit 2d1779b

File tree

7 files changed

+194
-0
lines changed

7 files changed

+194
-0
lines changed

doc/howto/subcommand_parser/index.md

+6
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,9 @@ followed by the keyword `push` which in this case triggers printing the help pag
5151
That's it. Here is a full example of a subcommand parser you can try and adjust to your needs:
5252

5353
\include doc/howto/subcommand_parser/subcommand_parse.cpp
54+
55+
# Recursive subcommands
56+
57+
If you want to have subcommands with subcommands, you add subcommands to the sub-parser with sharg::parser::add_subcommands():
58+
59+
\include test/snippet/add_subcommands.cpp

include/sharg/parser.hpp

+21
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,27 @@ class parser
639639

640640
operations.push_back(std::move(operation));
641641
}
642+
643+
/*!\brief Adds subcommands to the parser.
644+
* \param[in] subcommands A list of subcommands.
645+
* \details
646+
* Adds subcommands to the current parser. The list of subcommands is sorted and duplicates are removed.
647+
*
648+
* ### Example
649+
*
650+
* \include test/snippet/add_subcommands.cpp
651+
*
652+
* \experimentalapi{Experimental since version 1.1.2}
653+
*/
654+
void add_subcommands(std::vector<std::string> const & subcommands)
655+
{
656+
auto & parser_subcommands = this->subcommands;
657+
parser_subcommands.insert(parser_subcommands.end(), subcommands.cbegin(), subcommands.cend());
658+
659+
std::ranges::sort(parser_subcommands);
660+
auto const [first, last] = std::ranges::unique(parser_subcommands);
661+
parser_subcommands.erase(first, last);
662+
}
642663
//!\}
643664

644665
/*!\brief Aggregates all parser related meta data (see sharg::parser_meta_data struct).

test/snippet/add_subcommands.cpp

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// SPDX-FileCopyrightText: 2006-2024 Knut Reinert & Freie Universität Berlin
2+
// SPDX-FileCopyrightText: 2016-2024 Knut Reinert & MPI für molekulare Genetik
3+
// SPDX-License-Identifier: CC0-1.0
4+
5+
#include <sharg/all.hpp>
6+
7+
void run(std::vector<std::string> const & arguments)
8+
{
9+
sharg::parser git_parser{"git", arguments, sharg::update_notifications::off, {"pull", "push"}};
10+
git_parser.add_subcommands({"remote"});
11+
git_parser.parse();
12+
13+
sharg::parser & sub_parser = git_parser.get_sub_parser();
14+
15+
if (sub_parser.info.app_name == std::string_view{"git-pull"})
16+
{
17+
auto & pull_parser = sub_parser;
18+
std::string repository{};
19+
pull_parser.add_positional_option(repository, sharg::config{});
20+
pull_parser.parse();
21+
}
22+
else if (sub_parser.info.app_name == std::string_view{"git-push"})
23+
{
24+
auto & push_parser = sub_parser;
25+
std::string repository{};
26+
push_parser.add_positional_option(repository, sharg::config{});
27+
push_parser.parse();
28+
}
29+
else if (sub_parser.info.app_name == std::string_view{"git-remote"})
30+
{
31+
auto & remote_parser = sub_parser;
32+
remote_parser.add_subcommands({"set-url", "show"});
33+
remote_parser.parse();
34+
35+
sharg::parser & recursive_sub_parser = remote_parser.get_sub_parser();
36+
37+
if (recursive_sub_parser.info.app_name == std::string_view{"git-remote-set-url"})
38+
{
39+
auto & set_url_parser = recursive_sub_parser;
40+
std::string repository{};
41+
set_url_parser.add_positional_option(repository, sharg::config{});
42+
set_url_parser.parse();
43+
}
44+
else if (recursive_sub_parser.info.app_name == std::string_view{"git-remote-show"})
45+
{
46+
auto & show_parser = recursive_sub_parser;
47+
show_parser.parse();
48+
}
49+
}
50+
}
51+
52+
int main(int argc, char ** argv)
53+
{
54+
try
55+
{
56+
run({argv, argv + argc});
57+
}
58+
catch (sharg::parser_error const & ext)
59+
{
60+
std::cerr << "[Error] " << ext.what() << '\n';
61+
std::exit(-1);
62+
}
63+
64+
return 0;
65+
}

test/snippet/add_subcommands.out

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
git
2+
===
3+
Try -h or --help for more information.
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
SPDX-FileCopyrightText: 2006-2024 Knut Reinert & Freie Universität Berlin
2+
SPDX-FileCopyrightText: 2016-2024 Knut Reinert & MPI für molekulare Genetik
3+
SPDX-License-Identifier: CC0-1.0

test/unit/detail/format_cwl_test.cpp

+70
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,74 @@ TEST_F(format_cwl_test, subparser)
277277
" - index\n";
278278
EXPECT_EQ(get_parse_cout_on_exit(sub_parser), expected_short);
279279
}
280+
281+
TEST_F(format_cwl_test, subsubparser)
282+
{
283+
// Create variables for the arguments
284+
int option_value{5};
285+
std::string option_value_string{};
286+
std::filesystem::path option_value_path{};
287+
288+
// Create the dummy parser.
289+
auto parser = get_subcommand_parser({"index", "show", "--export-help", "cwl"}, {"index"});
290+
291+
EXPECT_NO_THROW(parser.parse());
292+
293+
auto & sub_parser = parser.get_sub_parser();
294+
ASSERT_EQ(sub_parser.info.app_name, "test_parser-index");
295+
296+
sub_parser.add_subcommands({"show"});
297+
EXPECT_NO_THROW(sub_parser.parse());
298+
299+
auto & sub_sub_parser = sub_parser.get_sub_parser();
300+
sub_sub_parser.add_option(option_value,
301+
sharg::config{.short_id = 'j',
302+
.long_id = "jint",
303+
.description = "this is a required int option.",
304+
.required = true});
305+
sub_sub_parser.add_option(option_value_string,
306+
sharg::config{.short_id = 's',
307+
.long_id = "string",
308+
.description = "this is a string option (advanced).",
309+
.advanced = true,
310+
.required = false});
311+
sub_sub_parser.add_option(option_value_path,
312+
sharg::config{.short_id = '\0',
313+
.long_id = "path04",
314+
.description = "a output file.",
315+
.validator = sharg::output_file_validator{}});
316+
317+
std::string expected_short =
318+
"label: test_parser-index-show\n"
319+
"doc: \"\"\n"
320+
"inputs:\n"
321+
" jint:\n"
322+
" doc: this is a required int option.\n"
323+
" type: long\n"
324+
" inputBinding:\n"
325+
" prefix: --jint\n"
326+
" string:\n"
327+
" doc: \"this is a string option (advanced). Default: \\\"\\\"\"\n"
328+
" type: string?\n"
329+
" inputBinding:\n"
330+
" prefix: --string\n"
331+
" path04:\n"
332+
" doc: \"a output file. Default: \\\"\\\". The output file must not exist already and write permissions "
333+
"must be granted.\"\n"
334+
" type: string?\n"
335+
" inputBinding:\n"
336+
" prefix: --path04\n"
337+
"outputs:\n"
338+
" path04:\n"
339+
" type: File?\n"
340+
" outputBinding:\n"
341+
" glob: $(inputs.path04)\n"
342+
"cwlVersion: v1.2\n"
343+
"class: CommandLineTool\n"
344+
"baseCommand:\n"
345+
" - test_parser\n"
346+
" - index\n"
347+
" - show\n";
348+
EXPECT_EQ(get_parse_cout_on_exit(sub_sub_parser), expected_short);
349+
}
280350
#endif

test/unit/parser/subcommand_test.cpp

+26
Original file line numberDiff line numberDiff line change
@@ -245,3 +245,29 @@ TEST_F(subcommand_test, option_value_is_special_command)
245245
EXPECT_NO_THROW(parser.parse());
246246
EXPECT_EQ(value, "--help");
247247
}
248+
249+
TEST_F(subcommand_test, recursive_subcommands)
250+
{
251+
auto parser = get_subcommand_parser({"index", "show", "--help"}, {"index"});
252+
EXPECT_NO_THROW(parser.parse());
253+
254+
auto & sub_parser = parser.get_sub_parser();
255+
ASSERT_EQ(sub_parser.info.app_name, "test_parser-index");
256+
sub_parser.add_subcommands({"show"});
257+
EXPECT_NO_THROW(sub_parser.parse());
258+
259+
auto & sub_sub_parser = sub_parser.get_sub_parser();
260+
ASSERT_EQ(sub_sub_parser.info.app_name, "test_parser-index-show");
261+
clear_and_add_option(sub_sub_parser);
262+
263+
std::string expected_sub_sub_full_help = "test_parser-index-show\n"
264+
"======================\n"
265+
"\n"
266+
"OPTIONS\n"
267+
" -o (std::string)\n"
268+
" Default: \"\"\n"
269+
"\n"
270+
+ basic_options_str + '\n' + version_str("-index-show");
271+
272+
EXPECT_EQ(get_parse_cout_on_exit(sub_sub_parser), expected_sub_sub_full_help);
273+
}

0 commit comments

Comments
 (0)