SeqAn3  3.0.2
The Modern C++ library for sequence analysis.
argument_parser.hpp
Go to the documentation of this file.
1 // -----------------------------------------------------------------------------------------------------
2 // Copyright (c) 2006-2020, Knut Reinert & Freie Universität Berlin
3 // Copyright (c) 2016-2020, Knut Reinert & MPI für molekulare Genetik
4 // This file may be used, modified and/or redistributed under the terms of the 3-clause BSD-License
5 // shipped with this file and also available at: https://github.com/seqan/seqan3/blob/master/LICENSE.md
6 // -----------------------------------------------------------------------------------------------------
7 
13 #pragma once
14 
15 #include <future>
16 #include <iostream>
17 #include <set>
18 #include <sstream>
19 #include <string>
20 #include <variant>
21 #include <vector>
22 #include <regex>
23 
24 // #include <seqan3/argument_parser/detail/format_ctd.hpp>
35 
36 namespace seqan3
37 {
38 
154 {
155 public:
159  argument_parser() = delete;
160  argument_parser(argument_parser const &) = default;
164 
181  argument_parser(std::string const app_name,
182  int const argc,
183  char const * const * const argv,
184  bool version_check = true,
185  std::vector<std::string> subcommands = {}) :
186  version_check_dev_decision{version_check}
187  {
188  if (!std::regex_match(app_name, app_name_regex))
189  throw design_error{"The application name must only contain alpha-numeric characters "
190  "or '_' and '-' (regex: \"^[a-zA-Z0-9_-]+$\")."};
191  for (auto & sub : subcommands)
192  if (!std::regex_match(sub, std::regex{"^[a-zA-Z0-9_]+$"}))
193  throw design_error{"The subcommand name must only contain alpha-numeric characters or '_'."};
194 
195  info.app_name = std::move(app_name);
196  init(argc, argv, std::move(subcommands));
197  }
198 
201  {
202  // wait for another 3 seconds
203  if (version_check_future.valid())
204  version_check_future.wait_for(std::chrono::seconds(3));
205  }
207 
231  template <typename option_type, validator validator_type = detail::default_validator<option_type>>
234  argument_parser_compatible_option<std::ranges::range_value_t<option_type>>) &&
235  std::invocable<validator_type, option_type>
237  void add_option(option_type & value,
238  char const short_id,
239  std::string const & long_id,
240  std::string const & desc,
241  option_spec const spec = option_spec::DEFAULT,
242  validator_type option_validator = validator_type{}) // copy to bind rvalues
243  {
244  if (sub_parser != nullptr)
245  throw design_error{"You may only specify flags for the top-level parser."};
246 
247  verify_identifiers(short_id, long_id);
248  // copy variables into the lambda because the calls are pushed to a stack
249  // and the references would go out of scope.
250  std::visit([=, &value] (auto & f) { f.add_option(value, short_id, long_id, desc, spec, option_validator); },
251  format);
252  }
253 
262  void add_flag(bool & value,
263  char const short_id,
264  std::string const & long_id,
265  std::string const & desc,
266  option_spec const spec = option_spec::DEFAULT)
267  {
268  verify_identifiers(short_id, long_id);
269  // copy variables into the lambda because the calls are pushed to a stack
270  // and the references would go out of scope.
271  std::visit([=, &value] (auto & f) { f.add_flag(value, short_id, long_id, desc, spec); }, format);
272  }
273 
294  template <typename option_type, validator validator_type = detail::default_validator<option_type>>
297  argument_parser_compatible_option<std::ranges::range_value_t<option_type>>) &&
298  std::invocable<validator_type, option_type>
300  void add_positional_option(option_type & value,
301  std::string const & desc,
302  validator_type option_validator = validator_type{}) // copy to bind rvalues
303  {
304  if (sub_parser != nullptr)
305  throw design_error{"You may only specify flags for the top-level parser."};
306 
307  if (has_positional_list_option)
308  throw design_error{"You added a positional option with a list value before so you cannot add "
309  "any other positional options."};
310 
311  if constexpr (sequence_container<option_type> && !std::same_as<option_type, std::string>)
312  has_positional_list_option = true; // keep track of a list option because there must be only one!
313 
314  // copy variables into the lambda because the calls are pushed to a stack
315  // and the references would go out of scope.
316  std::visit([=, &value] (auto & f) { f.add_positional_option(value, desc, option_validator); }, format);
317  }
319 
387  void parse()
388  {
389  if (parse_was_called)
390  throw design_error("The function parse() must only be called once!");
391 
392  detail::version_checker app_version{info.app_name, info.version, info.url};
393 
394  if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
395  {
396  // must be done before calling parse on the format because this might std::exit
397  std::promise<bool> app_version_prom;
398  version_check_future = app_version_prom.get_future();
399  app_version(std::move(app_version_prom));
400  }
401 
402  std::visit([this] (auto & f) { f.parse(info); }, format);
403  parse_was_called = true;
404  }
405 
409  {
410  if (sub_parser == nullptr)
411  {
412  throw design_error("You did not enable subcommand parsing on construction "
413  "so you cannot access the sub-parser!");
414  }
415 
416  return *sub_parser;
417  }
418 
421 
428  void add_section(std::string const & title, option_spec const spec = option_spec::DEFAULT)
429  {
430  std::visit([&] (auto & f) { f.add_section(title, spec); }, format);
431  }
432 
439  void add_subsection(std::string const & title, option_spec const spec = option_spec::DEFAULT)
440  {
441  std::visit([&] (auto & f) { f.add_subsection(title, spec); }, format);
442  }
443 
453  void add_line(std::string const & text, bool is_paragraph = false, option_spec const spec = option_spec::DEFAULT)
454  {
455  std::visit([&] (auto & f) { f.add_line(text, is_paragraph, spec); }, format);
456  }
457 
476  void add_list_item(std::string const & key, std::string const & desc, option_spec const spec = option_spec::DEFAULT)
477  {
478  std::visit([&] (auto & f) { f.add_list_item(key, desc, spec); }, format);
479  }
481 
531 
532 private:
534  bool parse_was_called{false};
535 
537  bool has_positional_list_option{false};
538 
540  bool version_check_dev_decision{};
541 
543  std::optional<bool> version_check_user_decision;
544 
546  friend struct ::seqan3::detail::test_accessor;
547 
549  std::future<bool> version_check_future;
550 
552  std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"};
553 
555  std::unique_ptr<argument_parser> sub_parser{nullptr};
556 
590  void init(int argc, char const * const * const argv, std::vector<std::string> const & subcommands)
591  {
592  // cash command line input, in case --version-check is specified but shall not be passed to format_parse()
593  std::vector<std::string> argv_new{};
594 
595  if (argc <= 1) // no arguments provided
596  {
597  format = detail::format_short_help{};
598  return;
599  }
600 
601  bool special_format_was_set{false};
602 
603  for(int i = 1, argv_len = argc; i < argv_len; ++i) // start at 1 to skip binary name
604  {
605  std::string arg{argv[i]};
606 
607  if (std::ranges::find(subcommands, arg) != subcommands.end())
608  {
609  sub_parser = std::make_unique<argument_parser>(info.app_name + "-" + arg, argc - i, argv + i, false);
610  break;
611  }
612  if (arg == "-h" || arg == "--help")
613  {
614  format = detail::format_help{subcommands, false};
615  init_standard_options();
616  special_format_was_set = true;
617  }
618  else if (arg == "-hh" || arg == "--advanced-help")
619  {
620  format = detail::format_help{subcommands, true};
621  init_standard_options();
622  special_format_was_set = true;
623  }
624  else if (arg == "--version")
625  {
626  format = detail::format_version{};
627  special_format_was_set = true;
628  }
629  else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed
630  {
631  std::string export_format;
632 
633  if (arg.size() > 13)
634  {
635  export_format = arg.substr(14);
636  }
637  else
638  {
639  if (argv_len <= i + 1)
640  throw too_few_arguments{"Option --export-help must be followed by a value."};
641  export_format = {argv[i+1]};
642  }
643 
644  if (export_format == "html")
645  format = detail::format_html{subcommands};
646  else if (export_format == "man")
647  format = detail::format_man{subcommands};
648  // TODO (smehringer) use when CTD support is available
649  // else if (export_format == "ctd")
650  // format = detail::format_ctd{};
651  else
652  throw validation_error{"Validation failed for option --export-help: "
653  "Value must be one of [html, man]"};
654  init_standard_options();
655  special_format_was_set = true;
656  }
657  else if (arg == "--copyright")
658  {
659  format = detail::format_copyright{};
660  special_format_was_set = true;
661  }
662  else if (arg == "--version-check")
663  {
664  if (++i >= argv_len)
665  throw too_few_arguments{"Option --version-check must be followed by a value."};
666 
667  arg = argv[i];
668 
669  if (arg == "1")
670  version_check_user_decision = true;
671  else if (arg == "0")
672  version_check_user_decision = false;
673  else
674  throw validation_error{"Value for option --version-check must be 1 or 0."};
675 
676  argc -= 2;
677  }
678  else
679  {
680  argv_new.push_back(std::move(arg));
681  }
682  }
683 
684  if (!special_format_was_set)
685  {
686  if (!subcommands.empty() && sub_parser == nullptr)
687  {
688  throw too_few_arguments{detail::to_string("Please specify which sub program you want to use ",
689  "(one of ", subcommands, "). Use -h/--help for more information.")};
690  }
691 
692  format = detail::format_parse(argc, std::move(argv_new));
693  }
694  }
695 
697  void init_standard_options()
698  {
699  add_subsection("Basic options:");
700  add_list_item("\\fB-h\\fP, \\fB--help\\fP", "Prints the help page.");
701  add_list_item("\\fB-hh\\fP, \\fB--advanced-help\\fP",
702  "Prints the help page including advanced options.");
703  add_list_item("\\fB--version\\fP", "Prints the version information.");
704  add_list_item("\\fB--copyright\\fP", "Prints the copyright/license information.");
705  add_list_item("\\fB--export-help\\fP (std::string)",
706  "Export the help page information. Value must be one of [html, man].");
707  if (version_check_dev_decision)
708  add_list_item("\\fB--version-check\\fP (bool)", "Whether to to check for the newest app version. Default: 1.");
709  add_subsection(""); // add a new line (todo smehringer) add a add_newline() function
710  }
711 
717  template <typename id_type>
718  bool id_exists(id_type const & id)
719  {
720  if (detail::format_parse::is_empty_id(id))
721  return false;
722  return (!(used_option_ids.insert(std::string({id}))).second);
723  }
724 
734  void verify_identifiers(char const short_id, std::string const & long_id)
735  {
736  auto constexpr allowed = is_alnum || is_char<'_'> || is_char<'@'>;
737 
738  if (id_exists(short_id))
739  throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
740  if (id_exists(long_id))
741  throw design_error("Option Identifier '" + long_id + "' was already used before.");
742  if (long_id.length() == 1)
743  throw design_error("Long IDs must be either empty, or longer than one character.");
744  if (!allowed(short_id) && !is_char<'\0'>(short_id))
745  throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
746  if (long_id.size() > 0 && is_char<'-'>(long_id[0]))
747  throw design_error("First character of long ID cannot be '-'.");
748 
749  std::for_each(long_id.begin(), long_id.end(), [&allowed] (char c)
750  {
751  if (!(allowed(c) || is_char<'-'>(c)))
752  throw design_error("Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
753  });
754  if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
755  throw design_error("Option Identifiers cannot both be empty.");
756  }
757 
762  std::variant<detail::format_parse,
763  detail::format_help,
764  detail::format_short_help,
765  detail::format_version,
766  detail::format_html,
767  detail::format_man,
768  detail::format_copyright/*,
769  detail::format_ctd*/> format{detail::format_help{{}, false}}; // Will be overwritten in any case.
770 
772  std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
773 };
774 
775 } // namespace seqan3
seqan3::is_alnum
constexpr auto is_alnum
Checks whether c is a alphanumeric character.
Definition: predicate.hpp:220
sstream
seqan3::argument_parser::add_subsection
void add_subsection(std::string const &title, option_spec const spec=option_spec::DEFAULT)
Adds an help page subsection to the seqan3::argument_parser.
Definition: argument_parser.hpp:439
seqan3::argument_parser::argument_parser
argument_parser(argument_parser &&)=default
Defaulted.
seqan3::option_spec
option_spec
Used to further specify argument_parser options/flags.
Definition: auxiliary.hpp:233
std::for_each
T for_each(T... args)
regex
std::string
sequence_container
A more refined container concept than seqan3::container.
format_html.hpp
Provides the format_html struct and its helper functions.
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:408
seqan3::argument_parser
The SeqAn command line parser.
Definition: argument_parser.hpp:154
concept.hpp
Stream concepts.
seqan3::argument_parser::add_line
void add_line(std::string const &text, bool is_paragraph=false, option_spec const spec=option_spec::DEFAULT)
Adds an help page text line to the seqan3::argument_parser.
Definition: argument_parser.hpp:453
format_man.hpp
Provides the format_man struct and its helper functions.
vector
std::string::length
T length(T... args)
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::DEFAULT, validator_type option_validator=validator_type{})
Adds an option to the seqan3::argument_parser.
Definition: argument_parser.hpp:237
seqan3::argument_parser_meta_data::version
std::string version
The version information MAJOR.MINOR.PATH (e.g. 3.1.3)
Definition: auxiliary.hpp:271
std::promise
seqan3::pack_traits::find
constexpr ptrdiff_t find
Get the index of the first occurrence of a type in a pack.
Definition: traits.hpp:152
std::chrono::seconds
seqan3::DEFAULT
@ DEFAULT
The default were no checking or special displaying is happening.
Definition: auxiliary.hpp:234
std::regex_match
T regex_match(T... args)
seqan3::argument_parser_meta_data
Stores all parser related meta information of the seqan3::argument_parser.
Definition: auxiliary.hpp:263
std::promise::get_future
T get_future(T... args)
iostream
terminal.hpp
Checks if program is run interactively and retrieves dimensions of terminal (Transferred from seqan2)...
future
seqan3::argument_parser::argument_parser
argument_parser(argument_parser const &)=default
Defaulted.
seqan3::argument_parser::argument_parser
argument_parser()=delete
Deleted.
std::future::valid
T valid(T... args)
seqan3::views::move
auto const move
A view that turns lvalue-references into rvalue-references.
Definition: move.hpp:68
seqan3::argument_parser::add_section
void add_section(std::string const &title, option_spec const spec=option_spec::DEFAULT)
Adds an help page section to the seqan3::argument_parser.
Definition: argument_parser.hpp:428
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:300
format_help.hpp
Provides the format_help struct that print the help page to the command line and the two child format...
seqan3
The main SeqAn3 namespace.
Definition: aligned_sequence_concept.hpp:29
seqan3::argument_parser::add_list_item
void add_list_item(std::string const &key, std::string const &desc, option_spec const spec=option_spec::DEFAULT)
Adds an help page list item (key-value) to the seqan3::argument_parser.
Definition: argument_parser.hpp:476
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::DEFAULT)
Adds a flag to the seqan3::argument_parser.
Definition: argument_parser.hpp:262
seqan3::argument_parser_meta_data::url
std::string url
A link to your github/gitlab project with the newest release.
Definition: auxiliary.hpp:283
seqan3::argument_parser::operator=
argument_parser & operator=(argument_parser const &)=default
Defaulted.
std::regex
std::future::wait_for
T wait_for(T... args)
seqan3::argument_parser::argument_parser
argument_parser(std::string const app_name, int const argc, char const *const *const argv, bool version_check=true, std::vector< std::string > subcommands={})
Initializes an seqan3::argument_parser object from the command line arguments.
Definition: argument_parser.hpp:181
std::string::substr
T substr(T... args)
argument_parser_compatible_option
Checks whether the the type can be used in an add_(positional_)option call on the argument parser.
seqan3::argument_parser::~argument_parser
~argument_parser()
The destructor.
Definition: argument_parser.hpp:200
std::string::begin
T begin(T... args)
predicate.hpp
Provides character predicates for tokenisation.
std::set::insert
T insert(T... args)
std::visit
T visit(T... args)
std::vector::empty
T empty(T... args)
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:530
std::optional< bool >
to_string.hpp
Auxiliary for pretty printing of exception messages.
std::vector::end
T end(T... args)
seqan3::argument_parser::parse
void parse()
Initiates the actual command line parsing.
Definition: argument_parser.hpp:387
std::unique_ptr
seqan3::argument_parser::operator=
argument_parser & operator=(argument_parser &&)=default
Defaulted.
set
seqan3::argument_parser_meta_data::app_name
std::string app_name
The application name that will be displayed on the help page.
Definition: auxiliary.hpp:269
seqan3::design_error
Argument parser exception that is thrown whenever there is an design error directed at the developer ...
Definition: exceptions.hpp:171
test_accessor.hpp
Forward declares seqan3::detail::test_accessor.
format_parse.hpp
Provides the format_parse class.
version_check.hpp
Provides the version check functionality.
variant
string