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 Sep 6, 2019
1 parent 91e86a0 commit f3a8f3c
Show file tree
Hide file tree
Showing 13 changed files with 375 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 subcommand argument parsing ([How-to](https://docs.seqan.de/seqan/3-master-user/subcommand_arg_parse.html)).

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

[TOC]

This HowTo shows you how to write an argument parser with subcommand like `git push` 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 subcommand argument parser

In order to keep parsing with subcommands 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/subcommand_argument_parser/subcommand_arg_parse.cpp construction

\attention You can still add flags to your top-level parser if needed but **no (positional) options**.
This avoids ambiguous parsing (e.g. subcommand fasta given file extension fasta
`./myfasta_parser --filext fasta fasta ...`).

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/subcommand_argument_parser/subcommand_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 subcommand argument parser you can try and adjust to your needs:

\include doc/howto/subcommand_argument_parser/subcommand_arg_parse.cpp
110 changes: 110 additions & 0 deletions doc/howto/subcommand_argument_parser/subcommand_arg_parse.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#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 git pull] " << ext.what() << "\n";
return -1;
}

debug_stream << "Git pull with repository " << args.repository << " and branch " << args.branch << std::endl;

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

debug_stream << "Git push with repository " << args.repository << " and branches " << args.branches << std::endl;

return 0;
}

// =====================================================================================================================
// main
// =====================================================================================================================

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

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

std::cout << "Proceed to sub parser." << std::endl;

if (sub_parser.info.app_name == std::string_view{"mygit-pull"})
run_git_pull(sub_parser);
else if (sub_parser.info.app_name == std::string_view{"mygit-push"})
run_git_push(sub_parser);
else
throw std::logic_error{"I do not know sub parser " + sub_parser.info.app_name};
}
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

# Subcommand 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 subcommand parsing can be done with the seqan3::argument_parser,
take a look at our \link subcommand_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
67 changes: 56 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 (see \link subcommand_arg_parse subcommand parsing \endlink).
*
* \throws seqan3::parser_design_error if the application name contains illegal characters.
*
Expand All @@ -178,14 +180,19 @@ 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_-]+$\")."};
for (auto & sub : subcommands)
if (!std::regex_match(sub, std::regex{"^[a-zA-Z0-9_]+$"}))
throw parser_design_error{"The subcommand name must only contain alpha-numeric characters or '_'."};

info.app_name = std::move(app_name);
init(argc, argv);
init(argc, argv, std::move(subcommands));
}

//!\brief The destructor.
Expand Down Expand Up @@ -217,6 +224,8 @@ class argument_parser
* \param[in] desc The description of the option to be shown in the help page.
* \param[in] spec Advanced option specification, see seqan3::option_spec.
* \param[in] validator The validator applied to the value after parsing (callable).
*
* \throws seqan3::parser_design_error
*/
template <typename option_type, validator validator_type = detail::default_validator<option_type>>
//!\cond
Expand All @@ -231,6 +240,9 @@ class argument_parser
option_spec const & spec = option_spec::DEFAULT,
validator_type validator = validator_type{}) // copy to bind rvalues
{
if (sub_parser != nullptr)
throw parser_design_error{"You may only specify flags for the top-level parser."};

verify_identifiers(short_id, long_id);
// copy variables into the lambda because the calls are pushed to a stack
// and the references would go out of scope.
Expand Down Expand Up @@ -287,6 +299,9 @@ class argument_parser
std::string const & desc,
validator_type validator = validator_type{}) // copy to bind rvalues
{
if (sub_parser != nullptr)
throw parser_design_error{"You may only specify flags for the top-level parser."};

if (has_positional_list_option)
throw parser_design_error{"You added a positional option with a list value before so you cannot add "
"any other positional options."};
Expand Down Expand Up @@ -389,6 +404,19 @@ class argument_parser
parse_was_called = true;
}

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

return *sub_parser;
}

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

Expand Down Expand Up @@ -511,13 +539,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 subcommand_arg_parse subcommand parsing \endlink is enabled.
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 +574,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 @@ -559,15 +591,20 @@ class argument_parser
{
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);
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 +629,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 +669,15 @@ class argument_parser
}

if (!special_format_was_set)
{
if (!subcommands.empty() && sub_parser == nullptr)
{
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 +753,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}}; // Will be overwritten in any case.

//!\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
Loading

0 comments on commit f3a8f3c

Please sign in to comment.