SeqAn3  3.0.1
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;
161  argument_parser & operator=(argument_parser const &) = default;
162  argument_parser(argument_parser &&) = default;
163  argument_parser & operator=(argument_parser &&) = 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 parser_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 parser_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>>) &&
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 validator = validator_type{}) // copy to bind rvalues
243  {
244  if (sub_parser != nullptr)
245  throw parser_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, validator); }, format);
251  }
252 
261  void add_flag(bool & value,
262  char const short_id,
263  std::string const & long_id,
264  std::string const & desc,
265  option_spec const & spec = option_spec::DEFAULT)
266  {
267  verify_identifiers(short_id, long_id);
268  // copy variables into the lambda because the calls are pushed to a stack
269  // and the references would go out of scope.
270  std::visit([=, &value] (auto & f) { f.add_flag(value, short_id, long_id, desc, spec); }, format);
271  }
272 
293  template <typename option_type, validator validator_type = detail::default_validator<option_type>>
296  argument_parser_compatible_option<std::ranges::range_value_t<option_type>>) &&
299  void add_positional_option(option_type & value,
300  std::string const & desc,
301  validator_type validator = validator_type{}) // copy to bind rvalues
302  {
303  if (sub_parser != nullptr)
304  throw parser_design_error{"You may only specify flags for the top-level parser."};
305 
306  if (has_positional_list_option)
307  throw parser_design_error{"You added a positional option with a list value before so you cannot add "
308  "any other positional options."};
309 
311  has_positional_list_option = true; // keep track of a list option because there must be only one!
312 
313  // copy variables into the lambda because the calls are pushed to a stack
314  // and the references would go out of scope.
315  std::visit([=, &value] (auto & f) { f.add_positional_option(value, desc, validator); }, format);
316  }
318 
389  void parse()
390  {
391  if (parse_was_called)
392  throw parser_design_error("The function parse() must only be called once!");
393 
394  detail::version_checker app_version{info.app_name, info.version, info.url};
395 
396  if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
397  {
398  // must be done before calling parse on the format because this might std::exit
399  std::promise<bool> app_version_prom;
400  version_check_future = app_version_prom.get_future();
401  app_version(std::move(app_version_prom));
402  }
403 
404  std::visit([this] (auto & f) { f.parse(info); }, format);
405  parse_was_called = true;
406  }
407 
411  {
412  if (sub_parser == nullptr)
413  {
414  throw parser_design_error("You did not enable subcommand parsing on construction "
415  "so you cannot access the sub-parser!");
416  }
417 
418  return *sub_parser;
419  }
420 
423 
428  void add_section(std::string const & title)
429  {
430  std::visit([&] (auto & f) { f.add_section(title); }, format);
431  }
432 
437  void add_subsection(std::string const & title)
438  {
439  std::visit([&] (auto & f) { f.add_subsection(title); }, format);
440  }
441 
450  void add_line(std::string const & text, bool line_is_paragraph = false)
451  {
452  std::visit([&] (auto & f) { f.add_line(text, line_is_paragraph); }, format);
453  }
454 
471  void add_list_item(std::string const & key, std::string const & desc)
472  {
473  std::visit([&] (auto & f) { f.add_list_item(key, desc); }, format);
474  }
476 
526 
527 private:
529  bool parse_was_called{false};
530 
532  bool has_positional_list_option{false};
533 
535  bool version_check_dev_decision{};
536 
538  std::optional<bool> version_check_user_decision;
539 
541  friend struct ::seqan3::detail::test_accessor;
542 
544  std::future<bool> version_check_future;
545 
547  std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"};
548 
550  std::unique_ptr<argument_parser> sub_parser{nullptr};
551 
581  void init(int argc, char const * const * const argv, std::vector<std::string> const & subcommands)
582  {
583  // cash command line input, in case --version-check is specified but shall not be passed to format_parse()
584  std::vector<std::string> argv_new{};
585 
586  if (argc <= 1) // no arguments provided
587  {
588  format = detail::format_short_help{};
589  return;
590  }
591 
592  bool special_format_was_set{false};
593 
594  for(int i = 1, argv_len = argc; i < argv_len; ++i) // start at 1 to skip binary name
595  {
596  std::string arg{argv[i]};
597 
598  if (std::ranges::find(subcommands, arg) != subcommands.end())
599  {
600  sub_parser = std::make_unique<argument_parser>(info.app_name + "-" + arg, argc - i, argv + i, false);
601  break;
602  }
603  if (arg == "-h" || arg == "--help")
604  {
605  format = detail::format_help{subcommands, false};
606  init_standard_options();
607  special_format_was_set = true;
608  }
609  else if (arg == "-hh" || arg == "--advanced-help")
610  {
611  format = detail::format_help{subcommands, true};
612  init_standard_options();
613  special_format_was_set = true;
614  }
615  else if (arg == "--version")
616  {
617  format = detail::format_version{};
618  special_format_was_set = true;
619  }
620  else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed
621  {
622  std::string export_format;
623 
624  if (arg.size() > 13)
625  {
626  export_format = arg.substr(14);
627  }
628  else
629  {
630  if (argv_len <= i + 1)
631  throw parser_invalid_argument{"Option --export-help must be followed by a value."};
632  export_format = {argv[i+1]};
633  }
634 
635  if (export_format == "html")
636  format = detail::format_html{subcommands};
637  else if (export_format == "man")
638  format = detail::format_man{subcommands};
639  // TODO (smehringer) use when CTD support is available
640  // else if (export_format == "ctd")
641  // format = detail::format_ctd{};
642  else
643  throw validation_failed{"Validation failed for option --export-help: "
644  "Value must be one of [html, man]"};
645  init_standard_options();
646  special_format_was_set = true;
647  }
648  else if (arg == "--copyright")
649  {
650  format = detail::format_copyright{};
651  special_format_was_set = true;
652  }
653  else if (arg == "--version-check")
654  {
655  if (++i >= argv_len)
656  throw parser_invalid_argument{"Option --version-check must be followed by a value."};
657 
658  arg = argv[i];
659 
660  if (arg == "1")
661  version_check_user_decision = true;
662  else if (arg == "0")
663  version_check_user_decision = false;
664  else
665  throw parser_invalid_argument{"Value for option --version-check must be 1 or 0."};
666 
667  argc -= 2;
668  }
669  else
670  {
671  argv_new.push_back(std::move(arg));
672  }
673  }
674 
675  if (!special_format_was_set)
676  {
677  if (!subcommands.empty() && sub_parser == nullptr)
678  {
679  throw parser_invalid_argument{detail::to_string("Please specify which sub program you want to use ",
680  "(one of ", subcommands, "). Use -h/--help for more information.")};
681  }
682 
683  format = detail::format_parse(argc, std::move(argv_new));
684  }
685  }
686 
688  void init_standard_options()
689  {
690  add_subsection("Basic options:");
691  add_list_item("\\fB-h\\fP, \\fB--help\\fP", "Prints the help page.");
692  add_list_item("\\fB-hh\\fP, \\fB--advanced-help\\fP",
693  "Prints the help page including advanced options.");
694  add_list_item("\\fB--version\\fP", "Prints the version information.");
695  add_list_item("\\fB--copyright\\fP", "Prints the copyright/license information.");
696  add_list_item("\\fB--export-help\\fP (std::string)",
697  "Export the help page information. Value must be one of [html, man].");
698  if (version_check_dev_decision)
699  add_list_item("\\fB--version-check\\fP (bool)", "Whether to to check for the newest app version. Default: 1.");
700  add_subsection(""); // add a new line (todo smehringer) add a add_newline() function
701  }
702 
708  template <typename id_type>
709  bool id_exists(id_type const & id)
710  {
711  if (detail::format_parse::is_empty_id(id))
712  return false;
713  return (!(used_option_ids.insert(std::string({id}))).second);
714  }
715 
725  void verify_identifiers(char const short_id, std::string const & long_id)
726  {
727  auto constexpr allowed = is_alnum || is_char<'_'> || is_char<'@'>;
728 
729  if (id_exists(short_id))
730  throw parser_design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
731  if (id_exists(long_id))
732  throw parser_design_error("Option Identifier '" + long_id + "' was already used before.");
733  if (long_id.length() == 1)
734  throw parser_design_error("Long IDs must be either empty, or longer than one character.");
735  if (!allowed(short_id) && !is_char<'\0'>(short_id))
736  throw parser_design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
737  if (long_id.size() > 0 && is_char<'-'>(long_id[0]))
738  throw parser_design_error("First character of long ID cannot be '-'.");
739 
740  std::for_each(long_id.begin(), long_id.end(), [&allowed] (char c)
741  {
742  if (!(allowed(c) || is_char<'-'>(c)))
743  throw parser_design_error("Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
744  });
745  if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
746  throw parser_design_error("Option Identifiers cannot both be empty.");
747  }
748 
753  std::variant<detail::format_parse,
754  detail::format_help,
755  detail::format_short_help,
756  detail::format_version,
757  detail::format_html,
758  detail::format_man,
759  detail::format_copyright/*,
760  detail::format_ctd*/> format{detail::format_help{{}, false}}; // Will be overwritten in any case.
761 
763  std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
764 };
765 
766 } // namespace seqan3
void add_line(std::string const &text, bool line_is_paragraph=false)
Adds an help page text line to the seqan3::argument_parser.
Definition: argument_parser.hpp:450
Checks whether the the type can be used in an add_(positional_)option call on the argument parser...
std::string url
A link to your github/gitlab project with the newest release.
Definition: auxiliary.hpp:282
constexpr auto is_char
Checks whether a given letter is the same as the template non-type argument.
Definition: predicate.hpp:83
Argument parser exception thrown when an argument could not be casted to the according type...
Definition: exceptions.hpp:117
T visit(T... args)
T empty(T... args)
T regex_match(T... args)
void add_subsection(std::string const &title)
Adds an help page subsection to the seqan3::argument_parser.
Definition: argument_parser.hpp:437
std::string version
The version information MAJOR.MINOR.PATH (e.g. 3.1.3)
Definition: auxiliary.hpp:270
void add_section(std::string const &title)
Adds an help page section to the seqan3::argument_parser.
Definition: argument_parser.hpp:428
Specifies whether the given callable is invocable with the given arguments.
T end(T... args)
The SeqAn command line parser.
Definition: argument_parser.hpp:153
The main SeqAn3 namespace.
Auxiliary for pretty printing of exception messages.
Provides the format_man struct and its helper functions.
Checks if program is run interactively and retrieves dimensions of terminal (Transferred from seqan2)...
Argument parser exception that is thrown whenever there is an design error directed at the developer ...
Definition: exceptions.hpp:136
The concept for option validators passed to add_option/positional_option.
Stores all parser related meta information of the seqan3::argument_parser.
Definition: auxiliary.hpp:261
constexpr ptrdiff_t find
Get the index of the first occurrence of a type in a pack.
Definition: traits.hpp:152
T get_future(T... args)
void add_positional_option(option_type &value, std::string const &desc, validator_type validator=validator_type{})
Adds a positional option to the seqan3::argument_parser.
Definition: argument_parser.hpp:299
argument_parser & operator=(argument_parser const &)=default
Defaulted.
argument_parser & get_sub_parser()
Returns a reference to the sub-parser instance if subcommand parsing was enabled.
Definition: argument_parser.hpp:410
T valid(T... args)
argument_parser()=delete
Deleted.
std::string app_name
The application name that will be displayed on the help page.
Definition: auxiliary.hpp:268
void add_list_item(std::string const &key, std::string const &desc)
Adds an help page list item (key-value) to the seqan3::argument_parser.
Definition: argument_parser.hpp:471
Argument parser exception that is thrown whenever there is an error while parsing the command line ar...
Definition: exceptions.hpp:37
argument_parser_meta_data info
Aggregates all parser related meta data (see seqan3::argument_parser_meta_data struct).
Definition: argument_parser.hpp:525
~argument_parser()
The destructor.
Definition: argument_parser.hpp:200
T insert(T... args)
T length(T... args)
Provides character predicates for tokenisation.
Stream concepts.
auto const move
A view that turns lvalue-references into rvalue-references.
Definition: move.hpp:68
T begin(T... args)
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 argument_parser object from the command line arguments.
Definition: argument_parser.hpp:181
Provides the version check functionality.
T wait_for(T... args)
T substr(T... args)
option_spec
Used to further specify argument_parser options/flags.
Definition: auxiliary.hpp:231
Provides the format_html struct and its helper functions.
Provides the format_parse class.
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 validator=validator_type{})
Adds an option to the seqan3::argument_parser.
Definition: argument_parser.hpp:237
T for_each(T... args)
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:261
Forward declares seqan3::detail::test_accessor.
The concept std::same_as<T, U> is satisfied if and only if T and U denote the same type...
A more refined container concept than seqan3::container.
void parse()
Initiates the actual command line parsing.
Definition: argument_parser.hpp:389
auto constexpr is_alnum
Checks whether c is a alphanumeric character.
Definition: predicate.hpp:220
Provides the format_help struct that print the help page to the command line and the two child format...
The default were no checking or special displaying is happening.
Definition: auxiliary.hpp:233