SeqAn3  3.0.3
The Modern C++ library for sequence analysis.
argument_parser.hpp
Go to the documentation of this file.
1 // -----------------------------------------------------------------------------------------------------
2 // Copyright (c) 2006-2021, Knut Reinert & Freie Universität Berlin
3 // Copyright (c) 2016-2021, 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 
165 #ifdef SEQAN3_DEPRECATED_310
168  argument_parser(std::string const app_name,
169  int const argc,
170  char const * const * const argv,
171  bool version_updates,
172  std::vector<std::string> subcommands = {}) :
174  {
175  app_name,
176  argc,
177  argv,
179  subcommands
180  }
181  {}
182 #endif // SEQAN3_DEPRECATED_310
183 
200  argument_parser(std::string const app_name,
201  int const argc,
202  char const * const * const argv,
204  std::vector<std::string> subcommands = {}) :
205  version_check_dev_decision{version_updates},
206  subcommands{std::move(subcommands)}
207  {
208  if (!std::regex_match(app_name, app_name_regex))
209  {
210  throw design_error{("The application name must only contain alpha-numeric characters or '_' and '-' "
211  "(regex: \"^[a-zA-Z0-9_-]+$\").")};
212  }
213 
214  for (auto & sub : this->subcommands)
215  {
216  if (!std::regex_match(sub, app_name_regex))
217  {
218  throw design_error{"The subcommand name must only contain alpha-numeric characters or '_' and '-' "
219  "(regex: \"^[a-zA-Z0-9_-]+$\")."};
220  }
221  }
222 
223 
224  info.app_name = std::move(app_name);
225 
226  init(argc, argv);
227  }
228 
231  {
232  // wait for another 3 seconds
233  if (version_check_future.valid())
234  version_check_future.wait_for(std::chrono::seconds(3));
235  }
237 
261  template <typename option_type, validator validator_type = detail::default_validator<option_type>>
264  argument_parser_compatible_option<std::ranges::range_value_t<option_type>>) &&
265  std::invocable<validator_type, option_type>
267  void add_option(option_type & value,
268  char const short_id,
269  std::string const & long_id,
270  std::string const & desc,
271  option_spec const spec = option_spec::standard,
272  validator_type option_validator = validator_type{}) // copy to bind rvalues
273  {
274  if (sub_parser != nullptr)
275  throw design_error{"You may only specify flags for the top-level parser."};
276 
277  verify_identifiers(short_id, long_id);
278  // copy variables into the lambda because the calls are pushed to a stack
279  // and the references would go out of scope.
280  std::visit([=, &value] (auto & f) { f.add_option(value, short_id, long_id, desc, spec, option_validator); },
281  format);
282  }
283 
292  void add_flag(bool & value,
293  char const short_id,
294  std::string const & long_id,
295  std::string const & desc,
296  option_spec const spec = option_spec::standard)
297  {
298  verify_identifiers(short_id, long_id);
299  // copy variables into the lambda because the calls are pushed to a stack
300  // and the references would go out of scope.
301  std::visit([=, &value] (auto & f) { f.add_flag(value, short_id, long_id, desc, spec); }, format);
302  }
303 
324  template <typename option_type, validator validator_type = detail::default_validator<option_type>>
327  argument_parser_compatible_option<std::ranges::range_value_t<option_type>>) &&
328  std::invocable<validator_type, option_type>
330  void add_positional_option(option_type & value,
331  std::string const & desc,
332  validator_type option_validator = validator_type{}) // copy to bind rvalues
333  {
334  if (sub_parser != nullptr)
335  throw design_error{"You may only specify flags for the top-level parser."};
336 
337  if (has_positional_list_option)
338  throw design_error{"You added a positional option with a list value before so you cannot add "
339  "any other positional options."};
340 
341  if constexpr (sequence_container<option_type> && !std::same_as<option_type, std::string>)
342  has_positional_list_option = true; // keep track of a list option because there must be only one!
343 
344  // copy variables into the lambda because the calls are pushed to a stack
345  // and the references would go out of scope.
346  std::visit([=, &value] (auto & f) { f.add_positional_option(value, desc, option_validator); }, format);
347  }
349 
417  void parse()
418  {
419  if (parse_was_called)
420  throw design_error("The function parse() must only be called once!");
421 
422  detail::version_checker app_version{info.app_name, info.version, info.url};
423 
424  if (std::holds_alternative<detail::format_parse>(format) && !subcommands.empty() && sub_parser == nullptr)
425  {
426  throw too_few_arguments{detail::to_string("You either forgot or misspelled the subcommand! Please specify"
427  " which sub-program you want to use: one of ", subcommands,
428  ". Use -h/--help for more information.")};
429  }
430 
431  if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
432  {
433  // must be done before calling parse on the format because this might std::exit
434  std::promise<bool> app_version_prom;
435  version_check_future = app_version_prom.get_future();
436  app_version(std::move(app_version_prom));
437  }
438 
439  std::visit([this] (auto & f) { f.parse(info); }, format);
440  parse_was_called = true;
441  }
442 
446  {
447  if (sub_parser == nullptr)
448  {
449  throw design_error("No subcommand was provided at the construction of the argument parser!");
450  }
451 
452  return *sub_parser;
453  }
454 
481  template <typename id_type>
483  requires std::same_as<id_type, char> || std::constructible_from<std::string, id_type>
485  bool is_option_set(id_type const & id) const
486  {
487  if (!parse_was_called)
488  throw design_error{"You can only ask which options have been set after calling the function `parse()`."};
489 
490  // the detail::format_parse::find_option_id call in the end expects either a char or std::string
491  using char_or_string_t = std::conditional_t<std::same_as<id_type, char>, char, std::string>;
492  char_or_string_t short_or_long_id = {id}; // e.g. convert char * to string here if necessary
493 
494  if constexpr (!std::same_as<id_type, char>) // long id was given
495  {
496  if (short_or_long_id.size() == 1)
497  {
498  throw design_error{"Long option identifiers must be longer than one character! If " + short_or_long_id +
499  "' was meant to be a short identifier, please pass it as a char ('') not a string"
500  " (\"\")!"};
501  }
502  }
503 
504  if (std::find(used_option_ids.begin(), used_option_ids.end(), std::string{id}) == used_option_ids.end())
505  throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
506 
507  // we only need to search for an option before the `end_of_options_indentifier` (`--`)
508  auto end_of_options = std::find(cmd_arguments.begin(), cmd_arguments.end(), end_of_options_indentifier);
509  auto option_it = detail::format_parse::find_option_id(cmd_arguments.begin(), end_of_options, short_or_long_id);
510  return option_it != end_of_options;
511  }
512 
515 
522  void add_section(std::string const & title, option_spec const spec = option_spec::standard)
523  {
524  std::visit([&] (auto & f) { f.add_section(title, spec); }, format);
525  }
526 
534  {
535  std::visit([&] (auto & f) { f.add_subsection(title, spec); }, format);
536  }
537 
547  void add_line(std::string const & text, bool is_paragraph = false, option_spec const spec = option_spec::standard)
548  {
549  std::visit([&] (auto & f) { f.add_line(text, is_paragraph, spec); }, format);
550  }
551 
570  void add_list_item(std::string const & key,
571  std::string const & desc,
572  option_spec const spec = option_spec::standard)
573  {
574  std::visit([&] (auto & f) { f.add_list_item(key, desc, spec); }, format);
575  }
577 
627 
628 private:
630  bool parse_was_called{false};
631 
633  bool has_positional_list_option{false};
634 
636  update_notifications version_check_dev_decision{};
637 
639  std::optional<bool> version_check_user_decision;
640 
642  friend struct ::seqan3::detail::test_accessor;
643 
645  std::future<bool> version_check_future;
646 
648  std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"};
649 
651  static constexpr std::string_view const end_of_options_indentifier{"--"};
652 
654  std::unique_ptr<argument_parser> sub_parser{nullptr};
655 
657  std::vector<std::string> subcommands{};
658 
666  std::variant<detail::format_parse,
667  detail::format_help,
668  detail::format_short_help,
669  detail::format_version,
670  detail::format_html,
671  detail::format_man,
672  detail::format_copyright/*,
673  detail::format_ctd*/> format{detail::format_help{{}, false}}; // Will be overwritten in any case.
674 
676  std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
677 
679  std::vector<std::string> cmd_arguments{};
680 
713  void init(int argc, char const * const * const argv)
714  {
715  if (argc <= 1) // no arguments provided
716  {
717  format = detail::format_short_help{};
718  return;
719  }
720 
721  bool special_format_was_set{false};
722 
723 
724  for (int i = 1, argv_len = argc; i < argv_len; ++i) // start at 1 to skip binary name
725  {
726  std::string arg{argv[i]};
727 
728  if (std::ranges::find(subcommands, arg) != subcommands.end())
729  {
730  sub_parser = std::make_unique<argument_parser>(info.app_name + "-" + arg,
731  argc - i,
732  argv + i,
734  break;
735  }
736 
737  if (arg == "-h" || arg == "--help")
738  {
739  format = detail::format_help{subcommands, false};
740  init_standard_options();
741  special_format_was_set = true;
742  }
743  else if (arg == "-hh" || arg == "--advanced-help")
744  {
745  format = detail::format_help{subcommands, true};
746  init_standard_options();
747  special_format_was_set = true;
748  }
749  else if (arg == "--version")
750  {
751  format = detail::format_version{};
752  special_format_was_set = true;
753  }
754  else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed
755  {
756  std::string export_format;
757 
758  if (arg.size() > 13)
759  {
760  export_format = arg.substr(14);
761  }
762  else
763  {
764  if (argv_len <= i + 1)
765  throw too_few_arguments{"Option --export-help must be followed by a value."};
766  export_format = {argv[i+1]};
767  }
768 
769  if (export_format == "html")
770  format = detail::format_html{subcommands};
771  else if (export_format == "man")
772  format = detail::format_man{subcommands};
773  // TODO (smehringer) use when CTD support is available
774  // else if (export_format == "ctd")
775  // format = detail::format_ctd{};
776  else
777  throw validation_error{"Validation failed for option --export-help: "
778  "Value must be one of [html, man]"};
779  init_standard_options();
780  special_format_was_set = true;
781  }
782  else if (arg == "--copyright")
783  {
784  format = detail::format_copyright{};
785  special_format_was_set = true;
786  }
787  else if (arg == "--version-check")
788  {
789  if (++i >= argv_len)
790  throw too_few_arguments{"Option --version-check must be followed by a value."};
791 
792  arg = argv[i];
793 
794  if (arg == "1" || arg == "true")
795  version_check_user_decision = true;
796  else if (arg == "0" || arg == "false")
797  version_check_user_decision = false;
798  else
799  throw validation_error{"Value for option --version-check must be true (1) or false (0)."};
800 
801  // in case --version-check is specified it shall not be passed to format_parse()
802  argc -= 2;
803  }
804  else
805  {
806  cmd_arguments.push_back(std::move(arg));
807  }
808  }
809 
810  if (!special_format_was_set)
811  format = detail::format_parse(argc, cmd_arguments);
812  }
813 
815  void init_standard_options()
816  {
817  add_subsection("Basic options:");
818  add_list_item("\\fB-h\\fP, \\fB--help\\fP", "Prints the help page.");
819  add_list_item("\\fB-hh\\fP, \\fB--advanced-help\\fP",
820  "Prints the help page including advanced options.");
821  add_list_item("\\fB--version\\fP", "Prints the version information.");
822  add_list_item("\\fB--copyright\\fP", "Prints the copyright/license information.");
823  add_list_item("\\fB--export-help\\fP (std::string)",
824  "Export the help page information. Value must be one of [html, man].");
825  if (version_check_dev_decision == update_notifications::on)
826  add_list_item("\\fB--version-check\\fP (bool)", "Whether to check for the newest app version. Default: true.");
827  }
828 
834  template <typename id_type>
835  bool id_exists(id_type const & id)
836  {
837  if (detail::format_parse::is_empty_id(id))
838  return false;
839  return (!(used_option_ids.insert(std::string({id}))).second);
840  }
841 
851  void verify_identifiers(char const short_id, std::string const & long_id)
852  {
853  auto constexpr allowed = is_alnum || is_char<'_'> || is_char<'@'>;
854 
855  if (id_exists(short_id))
856  throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
857  if (id_exists(long_id))
858  throw design_error("Option Identifier '" + long_id + "' was already used before.");
859  if (long_id.length() == 1)
860  throw design_error("Long IDs must be either empty, or longer than one character.");
861  if (!allowed(short_id) && !is_char<'\0'>(short_id))
862  throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
863  if (long_id.size() > 0 && is_char<'-'>(long_id[0]))
864  throw design_error("First character of long ID cannot be '-'.");
865 
866  std::for_each(long_id.begin(), long_id.end(), [&allowed] (char c)
867  {
868  if (!(allowed(c) || is_char<'-'>(c)))
869  throw design_error("Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
870  });
871  if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
872  throw design_error("Option Identifiers cannot both be empty.");
873  }
874 };
875 
876 } // namespace seqan3
T begin(T... args)
The SeqAn command line parser.
Definition: argument_parser.hpp:154
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:292
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:330
argument_parser & operator=(argument_parser const &)=default
Defaulted.
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:200
~argument_parser()
The destructor.
Definition: argument_parser.hpp:230
argument_parser_meta_data info
Aggregates all parser related meta data (see seqan3::argument_parser_meta_data struct).
Definition: argument_parser.hpp:626
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:485
argument_parser(argument_parser &&)=default
Defaulted.
void parse()
Initiates the actual command line parsing.
Definition: argument_parser.hpp:417
argument_parser()=delete
Deleted.
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:267
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:547
argument_parser & operator=(argument_parser &&)=default
Defaulted.
argument_parser & get_sub_parser()
Returns a reference to the sub-parser instance if subcommand parsing was enabled.
Definition: argument_parser.hpp:445
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:570
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:522
argument_parser(std::string const app_name, int const argc, char const *const *const argv, bool version_updates, std::vector< std::string > subcommands={})
Definition: argument_parser.hpp:168
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:533
argument_parser(argument_parser const &)=default
Defaulted.
Argument parser exception that is thrown whenever there is an design error directed at the developer ...
Definition: exceptions.hpp:177
Argument parser exception thrown when too few arguments are provided.
Definition: exceptions.hpp:81
Auxiliary for pretty printing of exception messages.
T end(T... args)
T find(T... args)
T for_each(T... args)
Provides the format_help struct that print the help page to the command line and the two child format...
Provides the format_html struct and its helper functions.
Provides the format_man struct and its helper functions.
Provides the format_parse class.
T get_future(T... args)
option_spec
Used to further specify argument_parser options/flags.
Definition: auxiliary.hpp:238
@ standard
The default were no checking or special displaying is happening.
Definition: auxiliary.hpp:239
constexpr auto is_alnum
Checks whether c is a alphanumeric character.
Definition: predicate.hpp:220
constexpr ptrdiff_t find
Get the index of the first occurrence of a type in a pack.
Definition: traits.hpp:187
auto const move
A view that turns lvalue-references into rvalue-references.
Definition: move.hpp:74
T insert(T... args)
Checks whether the the type can be used in an add_(positional_)option call on the argument parser.
A more refined container concept than seqan3::container.
Stream concepts.
The main SeqAn3 namespace.
Definition: aligned_sequence_concept.hpp:29
update_notifications
Indicates whether application allows automatic update notifications by the seqan3::argument_parser.
Definition: auxiliary.hpp:265
@ off
Automatic update notifications should be disabled.
@ on
Automatic update notifications should be enabled.
#define SEQAN3_DEPRECATED_310
Deprecation message for SeqAn 3.1.0 release.
Definition: platform.hpp:203
T push_back(T... args)
T regex_match(T... args)
T length(T... args)
Stores all parser related meta information of the seqan3::argument_parser.
Definition: auxiliary.hpp:282
std::string version
The version information MAJOR.MINOR.PATH (e.g. 3.1.3)
Definition: auxiliary.hpp:290
std::string app_name
The application name that will be displayed on the help page.
Definition: auxiliary.hpp:288
std::string url
A link to your github/gitlab project with the newest release.
Definition: auxiliary.hpp:302
T substr(T... args)
Checks if program is run interactively and retrieves dimensions of terminal (Transferred from seqan2)...
Forward declares seqan3::detail::test_accessor.
Provides character predicates for tokenisation.
T valid(T... args)
Provides the version check functionality.
T visit(T... args)
T wait_for(T... args)