Skip to content

Commit

Permalink
[FEATURE] Multi-level argument parsing.
Browse files Browse the repository at this point in the history
  • Loading branch information
smehringer committed Aug 27, 2019
1 parent b10205d commit e22099c
Show file tree
Hide file tree
Showing 12 changed files with 336 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 multi-level argument parsing ([How-to](https://docs.seqan.de/seqan/3-master-user/multi_level_arg_parse.html)).

#### Core
* Added traits for "metaprogramming" with `seqan3::type_list` and type packs.
Expand Down
44 changes: 44 additions & 0 deletions doc/howto/multi_level_argument_parser/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# How to write a multi-level argument parser {#multi_level_arg_parse}

[TOC]

This HowTo shows you how to write a multi-level argument parser 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 multi-level argument parser

In order to keep multi-level parsing 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/multi_level_argument_parser/multi_level_arg_parse.cpp construction

You can still add options and flags to your top-level parser if needed.
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/multi_level_argument_parser/multi_level_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 multi-level argument parser you can try and adjust to your needs:

\include doc/howto/multi_level_argument_parser/multi_level_arg_parse.cpp
97 changes: 97 additions & 0 deletions doc/howto/multi_level_argument_parser/multi_level_arg_parse.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#include <seqan3/argument_parser/all.hpp>

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] " << ext.what() << "\n";
return -1;
}

return 0;
}

// =====================================================================================================================
// push
// =====================================================================================================================

struct push_arguments
{
std::string repository{};
std::vector<std::string> 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] " << ext.what() << "\n";
return -1;
}

return 0;
}

int main(int argc, char const ** argv)
{
//![construction]
argument_parser top_level_parser{"mygit", argc, argv, true, {"push", "pull"}};
//![construction]

// Add information and options to your top-level parser just as you would do with a normal one.
// Note that all options directed at the top-level parser must be specified BEFORE the subcommand key word.
top_level_parser.info.description.push_back("You can push or pull from a remote repository.");
bool flag{false};
top_level_parser.add_option(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]

if (sub_parser.info.app_name == std::string_view{"git-pull"})
run_git_pull(sub_parser);
else if (sub_parser.info.app_name == std::string_view{"git-push"})
run_git_push(sub_parser);
}
7 changes: 7 additions & 0 deletions doc/tutorial/argument_parser/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,13 @@ that can serve as a copy'n'paste source for your application.
\include doc/tutorial/argument_parser/solution6.cpp
\endsolution

# Multi-level 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 multi-level parsing can be done with the seqan3::argument_parser,
take a look at our \link multi_level_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".
Expand Down
57 changes: 46 additions & 11 deletions include/seqan3/argument_parser/argument_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <seqan3/argument_parser/detail/version_check.hpp>
#include <seqan3/core/char_operations/predicate.hpp>
#include <seqan3/core/detail/terminal.hpp>
#include <seqan3/core/detail/to_string.hpp>
#include <seqan3/io/stream/concept.hpp>

namespace seqan3
Expand Down Expand Up @@ -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 for \link multi_level_arg_parse multi-level parsing \endlink.
*
* \throws seqan3::parser_design_error if the application name contains illegal characters.
*
Expand All @@ -178,14 +180,15 @@ 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<std::string> 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_-]+$\")."};
info.app_name = std::move(app_name);
init(argc, argv);
init(argc, argv, subcommands);
}

//!\brief The destructor.
Expand Down Expand Up @@ -389,6 +392,19 @@ class argument_parser
parse_was_called = true;
}

//!\brief Returns a reference to the sub-parser instance if
//! \link multi_level_arg_parse multi-level parsing \endlink was enabled.
argument_parser & get_sub_parser()
{
if (sub_parser == nullptr)
{
throw parser_design_error("You did not enable multi-level parsing on construction "
"so you cannot access the sub-parser!");
}

return *sub_parser;
}

//!\name Structuring the Help Page
//!\{

Expand Down Expand Up @@ -511,13 +527,17 @@ class argument_parser
//!\brief The future object that keeps track of the detached version check call thread.
std::future<bool> 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 multi_level_arg_parse multi-level parsing \endlink is enabled on construction.
std::unique_ptr<argument_parser> 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
*
Expand All @@ -542,7 +562,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<std::string> const & subcommands)
{
// cash command line input, in case --version-check is specified but shall not be passed to format_parse()
std::vector<std::string> argv_new{};
Expand All @@ -554,20 +574,27 @@ class argument_parser
}

bool special_format_was_set{false};
bool sub_parser_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<argument_parser>(info.app_name + "-" + arg, argc - i, argv + i, false);
sub_parser_was_set = true;
break;
}
if (arg == "-h" || arg == "--help")
{
format = detail::format_help{false};
format = detail::format_help{subcommands, false};
init_standard_options();
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();
special_format_was_set = true;
}
Expand All @@ -592,9 +619,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{};
Expand Down Expand Up @@ -632,7 +659,15 @@ class argument_parser
}

if (!special_format_was_set)
{
if (!subcommands.empty() && !sub_parser_was_set)
{
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.
Expand Down Expand Up @@ -708,7 +743,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}};

//!\brief List of option/flag identifiers that are already used.
std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
Expand Down
20 changes: 18 additions & 2 deletions include/seqan3/argument_parser/detail/format_base.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 subcommand names for multi-level argument parsing.
* \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<std::string> const & names, bool const advanced) :
command_names{names}, show_advanced_options{advanced}
{}
//!\}

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -452,6 +466,8 @@ class format_help_base : public format_base
std::vector<std::function<void()>> 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<std::string> command_names{};
//!\brief Whether to show advanced options or not.
bool show_advanced_options{true};
};
Expand Down
4 changes: 2 additions & 2 deletions include/seqan3/argument_parser/detail/format_help.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ class format_help : public format_help_base<format_help>
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<std::string> const &, bool const)
format_help(std::vector<std::string> const & names, bool const advanced = false) : base_type{names, advanced}
{};
//!\}

Expand Down
4 changes: 2 additions & 2 deletions include/seqan3/argument_parser/detail/format_html.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ class format_html : public format_help_base<format_html>
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<std::string> const &, bool const)
format_html(std::vector<std::string> const & names, bool const advanced = false) : base_type{names, advanced}
{};
//!\}

Expand Down
4 changes: 2 additions & 2 deletions include/seqan3/argument_parser/detail/format_man.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ class format_man : public format_help_base<format_man>
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<std::string> const &, bool const)
format_man(std::vector<std::string> const & names, bool const advanced = false) : base_type{names, advanced}
{};
//!\}

Expand Down
1 change: 1 addition & 0 deletions test/documentation/DoxygenLayout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<tab type="user" visible="yes" title="Implement your own read mapper" url="@ref tutorial_read_mapper" intro=""/>
</tab>
<tab type="usergroup" visible="yes" title="How-To" intro="">
<tab type="user" visible="yes" title="Multi-level argument parsing" url="\ref multi_level_arg_parse" intro=""/>
<tab type="user" visible="yes" title="Write your own view" url="\ref howto_write_a_view" intro=""/>
<tab type="user" visible="yes" title="Write your own alphabet" url="\ref howto_write_an_alphabet" intro=""/>
<tab type="user" visible="yes" title="Port from SeqAn2" url="\ref howto_porting" intro=""/>
Expand Down
Loading

0 comments on commit e22099c

Please sign in to comment.