Loading...
Searching...
No Matches
argument_parser.hpp
Go to the documentation of this file.
// SPDX-FileCopyrightText: 2006-2025 Knut Reinert & Freie Universität Berlin
// SPDX-FileCopyrightText: 2016-2025 Knut Reinert & MPI für molekulare Genetik
// SPDX-License-Identifier: BSD-3-Clause
#pragma once
#include <future>
#include <iostream>
#include <regex>
#include <set>
#include <sstream>
#include <string>
#include <variant>
#include <vector>
// #include <seqan3/argument_parser/detail/format_ctd.hpp>
#include <seqan3/argument_parser/detail/format_help.hpp>
#include <seqan3/argument_parser/detail/format_html.hpp>
#include <seqan3/argument_parser/detail/format_man.hpp>
#include <seqan3/argument_parser/detail/format_parse.hpp>
#include <seqan3/argument_parser/detail/terminal.hpp>
#include <seqan3/argument_parser/detail/version_check.hpp>
#include <seqan3/core/debug_stream/detail/to_string.hpp>
#include <seqan3/core/detail/test_accessor.hpp>
SEQAN3_DEPRECATED_HEADER("This header and its functionality is deprecated and will be removed in a future version of SeqAn. Please use the sharg-parser (url: https://github.com/seqan/sharg-parser) instead.");
namespace seqan3
{
class argument_parser
{
public:
argument_parser() = delete;
argument_parser(argument_parser const &) = delete;
argument_parser & operator=(argument_parser const &) = delete;
argument_parser(argument_parser &&) = default;
argument_parser & operator=(argument_parser &&) = default;
argument_parser(std::string const app_name,
int const argc,
char const * const * const argv,
update_notifications version_updates = update_notifications::on,
std::vector<std::string> subcommands = {}) :
version_check_dev_decision{version_updates},
subcommands{std::move(subcommands)}
{
if (!std::regex_match(app_name, app_name_regex))
{
throw design_error{("The application name must only contain alpha-numeric characters or '_' and '-' "
"(regex: \"^[a-zA-Z0-9_-]+$\").")};
}
for (auto & sub : this->subcommands)
{
if (!std::regex_match(sub, app_name_regex))
{
throw design_error{"The subcommand name must only contain alpha-numeric characters or '_' and '-' "
"(regex: \"^[a-zA-Z0-9_-]+$\")."};
}
}
info.app_name = std::move(app_name);
init(argc, argv);
}
~argument_parser()
{
// wait for another 3 seconds
if (version_check_future.valid())
version_check_future.wait_for(std::chrono::seconds(3));
}
template <typename option_type, validator validator_type = detail::default_validator<option_type>>
requires (argument_parser_compatible_option<option_type>
|| argument_parser_compatible_option<std::ranges::range_value_t<option_type>>)
&& std::invocable<validator_type, option_type>
void add_option(option_type & value,
char const short_id,
std::string const & long_id,
std::string const & desc,
option_spec const spec = option_spec::standard,
validator_type option_validator = validator_type{}) // copy to bind rvalues
{
if (sub_parser != nullptr)
throw 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.
std::visit(
[=, &value](auto & f)
{
f.add_option(value, short_id, long_id, desc, spec, option_validator);
},
format);
}
void add_flag(bool & value,
char const short_id,
std::string const & long_id,
std::string const & desc,
option_spec const spec = option_spec::standard)
{
if (value)
throw design_error("A flag's default value must be false.");
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.
std::visit(
[=, &value](auto & f)
{
f.add_flag(value, short_id, long_id, desc, spec);
},
format);
}
template <typename option_type, validator validator_type = detail::default_validator<option_type>>
requires (argument_parser_compatible_option<option_type>
|| argument_parser_compatible_option<std::ranges::range_value_t<option_type>>)
&& std::invocable<validator_type, option_type>
void add_positional_option(option_type & value,
std::string const & desc,
validator_type option_validator = validator_type{}) // copy to bind rvalues
{
if (sub_parser != nullptr)
throw design_error{"You may only specify flags for the top-level parser."};
if (has_positional_list_option)
throw design_error{"You added a positional option with a list value before so you cannot add "
"any other positional options."};
if constexpr (detail::is_container_option<option_type>)
has_positional_list_option = true; // keep track of a list option because there must be only one!
// copy variables into the lambda because the calls are pushed to a stack
// and the references would go out of scope.
std::visit(
[=, &value](auto & f)
{
f.add_positional_option(value, desc, option_validator);
},
format);
}
void parse()
{
if (parse_was_called)
throw design_error("The function parse() must only be called once!");
detail::version_checker app_version{info.app_name, info.version, info.url};
if (std::holds_alternative<detail::format_parse>(format) && !subcommands.empty() && sub_parser == nullptr)
{
throw too_few_arguments{detail::to_string("You either forgot or misspelled the subcommand! Please specify"
" which sub-program you want to use: one of ",
subcommands,
". Use -h/--help for more information.")};
}
if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
{
// must be done before calling parse on the format because this might std::exit
std::promise<bool> app_version_prom;
version_check_future = app_version_prom.get_future();
app_version(std::move(app_version_prom));
}
std::visit(
[this](auto & f)
{
f.parse(info);
},
format);
parse_was_called = true;
}
argument_parser & get_sub_parser()
{
if (sub_parser == nullptr)
{
throw design_error("No subcommand was provided at the construction of the argument parser!");
}
return *sub_parser;
}
template <typename id_type>
requires std::same_as<id_type, char> || std::constructible_from<std::string, id_type>
bool is_option_set(id_type const & id) const
{
if (!parse_was_called)
throw design_error{"You can only ask which options have been set after calling the function `parse()`."};
// the detail::format_parse::find_option_id call in the end expects either a char or std::string
using char_or_string_t = std::conditional_t<std::same_as<id_type, char>, char, std::string>;
char_or_string_t short_or_long_id = {id}; // e.g. convert char * to string here if necessary
if constexpr (!std::same_as<id_type, char>) // long id was given
{
if (short_or_long_id.size() == 1)
{
throw design_error{"Long option identifiers must be longer than one character! If " + short_or_long_id
+ "' was meant to be a short identifier, please pass it as a char ('') not a string"
" (\"\")!"};
}
}
if (std::find(used_option_ids.begin(), used_option_ids.end(), std::string{id}) == used_option_ids.end())
throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
// we only need to search for an option before the `end_of_options_indentifier` (`--`)
auto end_of_options = std::find(cmd_arguments.begin(), cmd_arguments.end(), end_of_options_indentifier);
auto option_it = detail::format_parse::find_option_id(cmd_arguments.begin(), end_of_options, short_or_long_id);
return option_it != end_of_options;
}
void add_section(std::string const & title, option_spec const spec = option_spec::standard)
{
std::visit(
[&](auto & f)
{
f.add_section(title, spec);
},
format);
}
void add_subsection(std::string const & title, option_spec const spec = option_spec::standard)
{
std::visit(
[&](auto & f)
{
f.add_subsection(title, spec);
},
format);
}
void add_line(std::string const & text, bool is_paragraph = false, option_spec const spec = option_spec::standard)
{
std::visit(
[&](auto & f)
{
f.add_line(text, is_paragraph, spec);
},
format);
}
void
add_list_item(std::string const & key, std::string const & desc, option_spec const spec = option_spec::standard)
{
std::visit(
[&](auto & f)
{
f.add_list_item(key, desc, spec);
},
format);
}
argument_parser_meta_data info;
private:
bool parse_was_called{false};
bool has_positional_list_option{false};
update_notifications version_check_dev_decision{};
std::optional<bool> version_check_user_decision;
friend struct ::seqan3::detail::test_accessor;
std::future<bool> version_check_future;
std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"};
static constexpr std::string_view const end_of_options_indentifier{"--"};
std::unique_ptr<argument_parser> sub_parser{nullptr};
std::vector<std::string> subcommands{};
std::variant<detail::format_parse,
detail::format_help,
detail::format_short_help,
detail::format_version,
detail::format_html,
detail::format_man,
detail::format_copyright/*,
detail::format_ctd*/> format{detail::format_help{{}, false}}; // Will be overwritten in any case.
std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
std::vector<std::string> cmd_arguments{};
void init(int argc, char const * const * const argv)
{
if (argc <= 1) // no arguments provided
{
format = detail::format_short_help{};
return;
}
bool special_format_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,
update_notifications::off);
break;
}
if (arg == "-h" || arg == "--help")
{
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{subcommands, true};
init_standard_options();
special_format_was_set = true;
}
else if (arg == "--version")
{
format = detail::format_version{};
special_format_was_set = true;
}
else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed
{
std::string export_format;
if (arg.size() > 13)
{
export_format = arg.substr(14);
}
else
{
if (argv_len <= i + 1)
throw too_few_arguments{"Option --export-help must be followed by a value."};
export_format = argv[i + 1];
}
if (export_format == "html")
format = detail::format_html{subcommands};
else if (export_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_error{"Validation failed for option --export-help: "
"Value must be one of [html, man]"};
init_standard_options();
special_format_was_set = true;
}
else if (arg == "--copyright")
{
format = detail::format_copyright{};
special_format_was_set = true;
}
else if (arg == "--version-check")
{
if (++i >= argv_len)
throw too_few_arguments{"Option --version-check must be followed by a value."};
arg = argv[i];
if (arg == "1" || arg == "true")
version_check_user_decision = true;
else if (arg == "0" || arg == "false")
version_check_user_decision = false;
else
throw validation_error{"Value for option --version-check must be true (1) or false (0)."};
// in case --version-check is specified it shall not be passed to format_parse()
argc -= 2;
}
else
{
cmd_arguments.push_back(std::move(arg));
}
}
if (!special_format_was_set)
format = detail::format_parse(argc, cmd_arguments);
}
void init_standard_options()
{
add_subsection("Basic options:");
add_list_item("\\fB-h\\fP, \\fB--help\\fP", "Prints the help page.");
add_list_item("\\fB-hh\\fP, \\fB--advanced-help\\fP", "Prints the help page including advanced options.");
add_list_item("\\fB--version\\fP", "Prints the version information.");
add_list_item("\\fB--copyright\\fP", "Prints the copyright/license information.");
add_list_item("\\fB--export-help\\fP (std::string)",
"Export the help page information. Value must be one of [html, man].");
if (version_check_dev_decision == update_notifications::on)
add_list_item("\\fB--version-check\\fP (bool)",
"Whether to check for the newest app version. Default: true.");
}
template <typename id_type>
bool id_exists(id_type const & id)
{
if (detail::format_parse::is_empty_id(id))
return false;
return (!(used_option_ids.insert(std::string({id}))).second);
}
void verify_identifiers(char const short_id, std::string const & long_id)
{
constexpr auto allowed = is_alnum || is_char<'_'> || is_char<'@'>;
if (id_exists(short_id))
throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
if (id_exists(long_id))
throw design_error("Option Identifier '" + long_id + "' was already used before.");
if (long_id.length() == 1)
throw design_error("Long IDs must be either empty, or longer than one character.");
if (!allowed(short_id) && !is_char<'\0'>(short_id))
throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
if (long_id.size() > 0 && is_char<'-'>(long_id[0]))
throw design_error("First character of long ID cannot be '-'.");
std::for_each(long_id.begin(),
long_id.end(),
[&allowed](char c)
{
if (!(allowed(c) || is_char<'-'>(c)))
throw design_error(
"Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
});
if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
throw design_error("Option Identifiers cannot both be empty.");
}
};
} // namespace seqan3
std::string
std::string::begin
T begin(T... args)
seqan3::argument_parser::add_positional_option
void add_positional_option(option_type &value, std::string const &desc, validator_type option_validator=validator_type{})
Adds a positional option to the seqan3::argument_parser.
Definition argument_parser.hpp:314
seqan3::argument_parser::add_flag
void add_flag(bool &value, char const short_id, std::string const &long_id, std::string const &desc, option_spec const spec=option_spec::standard)
Adds a flag to the seqan3::argument_parser.
Definition argument_parser.hpp:270
seqan3::argument_parser::is_option_set
bool is_option_set(id_type const &id) const
Checks whether the option identifier (id) was set on the command line by the user.
Definition argument_parser.hpp:476
seqan3::argument_parser::argument_parser
argument_parser(argument_parser const &)=delete
Deleted. Holds std::future.
seqan3::argument_parser::add_option
void add_option(option_type &value, char const short_id, std::string const &long_id, std::string const &desc, option_spec const spec=option_spec::standard, validator_type option_validator=validator_type{})
Adds an option to the seqan3::argument_parser.
Definition argument_parser.hpp:238
seqan3::argument_parser::argument_parser
argument_parser(std::string const app_name, int const argc, char const *const *const argv, update_notifications version_updates=update_notifications::on, std::vector< std::string > subcommands={})
Initializes an seqan3::argument_parser object from the command line arguments.
Definition argument_parser.hpp:174
seqan3::argument_parser::operator=
argument_parser & operator=(argument_parser &&)=default
Defaulted.
seqan3::argument_parser::~argument_parser
~argument_parser()
The destructor.
Definition argument_parser.hpp:203
seqan3::argument_parser::operator=
argument_parser & operator=(argument_parser const &)=delete
Deleted. Holds std::future.
seqan3::argument_parser::info
argument_parser_meta_data info
Aggregates all parser related meta data (see seqan3::argument_parser_meta_data struct).
Definition argument_parser.hpp:636
seqan3::argument_parser::argument_parser
argument_parser(argument_parser &&)=default
Defaulted.
seqan3::argument_parser::parse
void parse()
Initiates the actual command line parsing.
Definition argument_parser.hpp:404
seqan3::argument_parser::argument_parser
argument_parser()=delete
Deleted.
seqan3::argument_parser::add_line
void add_line(std::string const &text, bool is_paragraph=false, option_spec const spec=option_spec::standard)
Adds an help page text line to the seqan3::argument_parser.
Definition argument_parser.hpp:548
seqan3::argument_parser::add_list_item
void add_list_item(std::string const &key, std::string const &desc, option_spec const spec=option_spec::standard)
Adds an help page list item (key-value) to the seqan3::argument_parser.
Definition argument_parser.hpp:577
seqan3::argument_parser::add_section
void add_section(std::string const &title, option_spec const spec=option_spec::standard)
Adds an help page section to the seqan3::argument_parser.
Definition argument_parser.hpp:513
seqan3::argument_parser::add_subsection
void add_subsection(std::string const &title, option_spec const spec=option_spec::standard)
Adds an help page subsection to the seqan3::argument_parser.
Definition argument_parser.hpp:529
seqan3::argument_parser::get_sub_parser
argument_parser & get_sub_parser()
Returns a reference to the sub-parser instance if subcommand parsing was enabled.
Definition argument_parser.hpp:438
seqan3::design_error
Argument parser exception that is thrown whenever there is an design error directed at the developer ...
Definition exceptions.hpp:150
seqan3::too_few_arguments
Argument parser exception thrown when too few arguments are provided.
Definition exceptions.hpp:78
std::conditional_t
std::chrono::seconds
std::string::end
T end(T... args)
std::find
T find(T... args)
std::for_each
T for_each(T... args)
format_help.hpp
Provides the format_help struct that print the help page to the command line and the two child format...
format_html.hpp
Provides the format_html struct and its helper functions.
format_man.hpp
Provides the format_man struct and its helper functions.
format_parse.hpp
Provides the format_parse class.
future
std::promise::get_future
T get_future(T... args)
seqan3::option_spec
option_spec
Used to further specify argument_parser options/flags.
Definition auxiliary.hpp:249
seqan3::standard
@ standard
The default were no checking or special displaying is happening.
Definition auxiliary.hpp:250
seqan3::is_char
constexpr auto is_char
Checks whether a given letter is the same as the template non-type argument.
Definition predicate.hpp:60
std::holds_alternative
T holds_alternative(T... args)
argument_parser_compatible_option
Checks whether the the type can be used in an add_(positional_)option call on the argument parser.
iostream
std::make_unique
T make_unique(T... args)
seqan3
The main SeqAn3 namespace.
Definition aligned_sequence_concept.hpp:26
seqan3::update_notifications
update_notifications
Indicates whether application allows automatic update notifications by the seqan3::argument_parser.
Definition auxiliary.hpp:268
seqan3::update_notifications::off
@ off
Automatic update notifications should be disabled.
Definition auxiliary.hpp:270
seqan3::update_notifications::on
@ on
Automatic update notifications should be enabled.
Definition auxiliary.hpp:269
std
SeqAn specific customisations in the standard namespace.
std::promise
regex
std::regex_match
T regex_match(T... args)
set
std::string::size
T size(T... args)
sstream
string
seqan3::argument_parser_meta_data
Stores all parser related meta information of the seqan3::argument_parser.
Definition auxiliary.hpp:287
std::string::substr
T substr(T... args)
terminal.hpp
Checks if program is run interactively and retrieves dimensions of terminal (Transferred from seqan2)...
test_accessor.hpp
Forward declares seqan3::detail::test_accessor.
to_string.hpp
Auxiliary for pretty printing of exception messages.
variant
vector
version_check.hpp
Provides the version check functionality.
std::visit
T visit(T... args)
Hide me