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

[FIX, FEATURE] Fix initialising the version_check_option. Add multi-level argument parsing. #1185

Merged
merged 2 commits into from
Sep 6, 2019
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
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
82 changes: 65 additions & 17 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 @@ -553,26 +585,33 @@ class argument_parser
return;
}

bool special_format_was_set{false};
h-2 marked this conversation as resolved.
Show resolved Hide resolved

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);
break;
h-2 marked this conversation as resolved.
Show resolved Hide resolved
}
if (arg == "-h" || arg == "--help")
{
format = detail::format_help{false};
format = detail::format_help{subcommands, false};
init_standard_options();
return;
special_format_was_set = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that if you pass multiple special formats (e.g., h and hh), it will overwrite it in the next loop iteration. I think we should either print an error message or always take the first one. Currently it would choose the last argument.

}
else if (arg == "-hh" || arg == "--advanced-help")
{
format = detail::format_help{true};
format = detail::format_help{subcommands, true};
init_standard_options();
return;
special_format_was_set = true;
}
else if (arg == "--version")
{
format = detail::format_version{};
return;
special_format_was_set = true;
}
else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed
{
Expand All @@ -590,22 +629,22 @@ 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{};
else
throw validation_failed{"Validation failed for option --export-help: "
"Value must be one of [html, man]"};
init_standard_options();
return;
special_format_was_set = true;
}
else if (arg == "--copyright")
{
format = detail::format_copyright{};
return;
special_format_was_set = true;
}
else if (arg == "--version-check")
{
Expand All @@ -629,7 +668,16 @@ class argument_parser
}
}

format = detail::format_parse(argc, std::move(argv_new));
if (!special_format_was_set)
{
if (!subcommands.empty() && sub_parser == nullptr)
h-2 marked this conversation as resolved.
Show resolved Hide resolved
{
throw parser_invalid_argument{detail::to_string("Please specify which sub program you want to use ",
smehringer marked this conversation as resolved.
Show resolved Hide resolved
"(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 @@ -705,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