SeqAn3  3.2.0
The Modern C++ library for sequence analysis.
argument_parser.hpp
Go to the documentation of this file.
1 // -----------------------------------------------------------------------------------------------------
2 // Copyright (c) 2006-2022, Knut Reinert & Freie Universität Berlin
3 // Copyright (c) 2016-2022, 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 <regex>
18 #include <set>
19 #include <sstream>
20 #include <string>
21 #include <variant>
22 #include <vector>
23 
24 // #include <seqan3/argument_parser/detail/format_ctd.hpp>
33 
34 namespace seqan3
35 {
36 
152 {
153 public:
157  argument_parser() = delete;
158  argument_parser(argument_parser const &) = default;
162 
179  argument_parser(std::string const app_name,
180  int const argc,
181  char const * const * const argv,
183  std::vector<std::string> subcommands = {}) :
184  version_check_dev_decision{version_updates},
185  subcommands{std::move(subcommands)}
186  {
187  if (!std::regex_match(app_name, app_name_regex))
188  {
189  throw design_error{("The application name must only contain alpha-numeric characters or '_' and '-' "
190  "(regex: \"^[a-zA-Z0-9_-]+$\").")};
191  }
192 
193  for (auto & sub : this->subcommands)
194  {
195  if (!std::regex_match(sub, app_name_regex))
196  {
197  throw design_error{"The subcommand name must only contain alpha-numeric characters or '_' and '-' "
198  "(regex: \"^[a-zA-Z0-9_-]+$\")."};
199  }
200  }
201 
202  info.app_name = std::move(app_name);
203 
204  init(argc, argv);
205  }
206 
209  {
210  // wait for another 3 seconds
211  if (version_check_future.valid())
212  version_check_future.wait_for(std::chrono::seconds(3));
213  }
215 
239  template <typename option_type, validator validator_type = detail::default_validator<option_type>>
241  || argument_parser_compatible_option<std::ranges::range_value_t<option_type>>)
242  && std::invocable<validator_type, option_type>
243  void add_option(option_type & value,
244  char const short_id,
245  std::string const & long_id,
246  std::string const & desc,
247  option_spec const spec = option_spec::standard,
248  validator_type option_validator = validator_type{}) // copy to bind rvalues
249  {
250  if (sub_parser != nullptr)
251  throw design_error{"You may only specify flags for the top-level parser."};
252 
253  verify_identifiers(short_id, long_id);
254  // copy variables into the lambda because the calls are pushed to a stack
255  // and the references would go out of scope.
256  std::visit(
257  [=, &value](auto & f)
258  {
259  f.add_option(value, short_id, long_id, desc, spec, option_validator);
260  },
261  format);
262  }
263 
275  void add_flag(bool & value,
276  char const short_id,
277  std::string const & long_id,
278  std::string const & desc,
279  option_spec const spec = option_spec::standard)
280  {
281  if (value)
282  throw design_error("A flag's default value must be false.");
283 
284  verify_identifiers(short_id, long_id);
285  // copy variables into the lambda because the calls are pushed to a stack
286  // and the references would go out of scope.
287  std::visit(
288  [=, &value](auto & f)
289  {
290  f.add_flag(value, short_id, long_id, desc, spec);
291  },
292  format);
293  }
294 
315  template <typename option_type, validator validator_type = detail::default_validator<option_type>>
317  || argument_parser_compatible_option<std::ranges::range_value_t<option_type>>)
318  && std::invocable<validator_type, option_type>
319  void add_positional_option(option_type & value,
320  std::string const & desc,
321  validator_type option_validator = validator_type{}) // copy to bind rvalues
322  {
323  if (sub_parser != nullptr)
324  throw design_error{"You may only specify flags for the top-level parser."};
325 
326  if (has_positional_list_option)
327  throw design_error{"You added a positional option with a list value before so you cannot add "
328  "any other positional options."};
329 
330  if constexpr (detail::is_container_option<option_type>)
331  has_positional_list_option = true; // keep track of a list option because there must be only one!
332 
333  // copy variables into the lambda because the calls are pushed to a stack
334  // and the references would go out of scope.
335  std::visit(
336  [=, &value](auto & f)
337  {
338  f.add_positional_option(value, desc, option_validator);
339  },
340  format);
341  }
343 
409  void parse()
410  {
411  if (parse_was_called)
412  throw design_error("The function parse() must only be called once!");
413 
414  detail::version_checker app_version{info.app_name, info.version, info.url};
415 
416  if (std::holds_alternative<detail::format_parse>(format) && !subcommands.empty() && sub_parser == nullptr)
417  {
418  throw too_few_arguments{detail::to_string("You either forgot or misspelled the subcommand! Please specify"
419  " which sub-program you want to use: one of ",
420  subcommands,
421  ". Use -h/--help for more information.")};
422  }
423 
424  if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
425  {
426  // must be done before calling parse on the format because this might std::exit
427  std::promise<bool> app_version_prom;
428  version_check_future = app_version_prom.get_future();
429  app_version(std::move(app_version_prom));
430  }
431 
432  std::visit(
433  [this](auto & f)
434  {
435  f.parse(info);
436  },
437  format);
438  parse_was_called = true;
439  }
440 
444  {
445  if (sub_parser == nullptr)
446  {
447  throw design_error("No subcommand was provided at the construction of the argument parser!");
448  }
449 
450  return *sub_parser;
451  }
452 
479  template <typename id_type>
480  requires std::same_as<id_type, char> || std::constructible_from<std::string, id_type> bool
481  is_option_set(id_type const & id) const
482  {
483  if (!parse_was_called)
484  throw design_error{"You can only ask which options have been set after calling the function `parse()`."};
485 
486  // the detail::format_parse::find_option_id call in the end expects either a char or std::string
487  using char_or_string_t = std::conditional_t<std::same_as<id_type, char>, char, std::string>;
488  char_or_string_t short_or_long_id = {id}; // e.g. convert char * to string here if necessary
489 
490  if constexpr (!std::same_as<id_type, char>) // long id was given
491  {
492  if (short_or_long_id.size() == 1)
493  {
494  throw design_error{"Long option identifiers must be longer than one character! If " + short_or_long_id
495  + "' was meant to be a short identifier, please pass it as a char ('') not a string"
496  " (\"\")!"};
497  }
498  }
499 
500  if (std::find(used_option_ids.begin(), used_option_ids.end(), std::string{id}) == used_option_ids.end())
501  throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
502 
503  // we only need to search for an option before the `end_of_options_indentifier` (`--`)
504  auto end_of_options = std::find(cmd_arguments.begin(), cmd_arguments.end(), end_of_options_indentifier);
505  auto option_it = detail::format_parse::find_option_id(cmd_arguments.begin(), end_of_options, short_or_long_id);
506  return option_it != end_of_options;
507  }
508 
511 
518  void add_section(std::string const & title, option_spec const spec = option_spec::standard)
519  {
520  std::visit(
521  [&](auto & f)
522  {
523  f.add_section(title, spec);
524  },
525  format);
526  }
527 
534  void add_subsection(std::string const & title, option_spec const spec = option_spec::standard)
535  {
536  std::visit(
537  [&](auto & f)
538  {
539  f.add_subsection(title, spec);
540  },
541  format);
542  }
543 
553  void add_line(std::string const & text, bool is_paragraph = false, option_spec const spec = option_spec::standard)
554  {
555  std::visit(
556  [&](auto & f)
557  {
558  f.add_line(text, is_paragraph, spec);
559  },
560  format);
561  }
562 
581  void
582  add_list_item(std::string const & key, std::string const & desc, option_spec const spec = option_spec::standard)
583  {
584  std::visit(
585  [&](auto & f)
586  {
587  f.add_list_item(key, desc, spec);
588  },
589  format);
590  }
592 
641  argument_parser_meta_data info;
642 
643 private:
645  bool parse_was_called{false};
646 
648  bool has_positional_list_option{false};
649 
651  update_notifications version_check_dev_decision{};
652 
654  std::optional<bool> version_check_user_decision;
655 
657  friend struct ::seqan3::detail::test_accessor;
658 
660  std::future<bool> version_check_future;
661 
663  std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"};
664 
666  static constexpr std::string_view const end_of_options_indentifier{"--"};
667 
669  std::unique_ptr<argument_parser> sub_parser{nullptr};
670 
672  std::vector<std::string> subcommands{};
673 
681  std::variant<detail::format_parse,
682  detail::format_help,
683  detail::format_short_help,
684  detail::format_version,
685  detail::format_html,
686  detail::format_man,
687  detail::format_copyright/*,
688  detail::format_ctd*/> format{detail::format_help{{}, false}}; // Will be overwritten in any case.
689 
691  std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
692 
694  std::vector<std::string> cmd_arguments{};
695 
728  void init(int argc, char const * const * const argv)
729  {
730  if (argc <= 1) // no arguments provided
731  {
732  format = detail::format_short_help{};
733  return;
734  }
735 
736  bool special_format_was_set{false};
737 
738  for (int i = 1, argv_len = argc; i < argv_len; ++i) // start at 1 to skip binary name
739  {
740  std::string arg{argv[i]};
741 
742  if (std::ranges::find(subcommands, arg) != subcommands.end())
743  {
744  sub_parser = std::make_unique<argument_parser>(info.app_name + "-" + arg,
745  argc - i,
746  argv + i,
748  break;
749  }
750 
751  if (arg == "-h" || arg == "--help")
752  {
753  format = detail::format_help{subcommands, false};
754  init_standard_options();
755  special_format_was_set = true;
756  }
757  else if (arg == "-hh" || arg == "--advanced-help")
758  {
759  format = detail::format_help{subcommands, true};
760  init_standard_options();
761  special_format_was_set = true;
762  }
763  else if (arg == "--version")
764  {
765  format = detail::format_version{};
766  special_format_was_set = true;
767  }
768  else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed
769  {
770  std::string export_format;
771 
772  if (arg.size() > 13)
773  {
774  export_format = arg.substr(14);
775  }
776  else
777  {
778  if (argv_len <= i + 1)
779  throw too_few_arguments{"Option --export-help must be followed by a value."};
780  export_format = {argv[i + 1]};
781  }
782 
783  if (export_format == "html")
784  format = detail::format_html{subcommands};
785  else if (export_format == "man")
786  format = detail::format_man{subcommands};
787  // TODO (smehringer) use when CTD support is available
788  // else if (export_format == "ctd")
789  // format = detail::format_ctd{};
790  else
791  throw validation_error{"Validation failed for option --export-help: "
792  "Value must be one of [html, man]"};
793  init_standard_options();
794  special_format_was_set = true;
795  }
796  else if (arg == "--copyright")
797  {
798  format = detail::format_copyright{};
799  special_format_was_set = true;
800  }
801  else if (arg == "--version-check")
802  {
803  if (++i >= argv_len)
804  throw too_few_arguments{"Option --version-check must be followed by a value."};
805 
806  arg = argv[i];
807 
808  if (arg == "1" || arg == "true")
809  version_check_user_decision = true;
810  else if (arg == "0" || arg == "false")
811  version_check_user_decision = false;
812  else
813  throw validation_error{"Value for option --version-check must be true (1) or false (0)."};
814 
815  // in case --version-check is specified it shall not be passed to format_parse()
816  argc -= 2;
817  }
818  else
819  {
820  cmd_arguments.push_back(std::move(arg));
821  }
822  }
823 
824  if (!special_format_was_set)
825  format = detail::format_parse(argc, cmd_arguments);
826  }
827 
829  void init_standard_options()
830  {
831  add_subsection("Basic options:");
832  add_list_item("\\fB-h\\fP, \\fB--help\\fP", "Prints the help page.");
833  add_list_item("\\fB-hh\\fP, \\fB--advanced-help\\fP", "Prints the help page including advanced options.");
834  add_list_item("\\fB--version\\fP", "Prints the version information.");
835  add_list_item("\\fB--copyright\\fP", "Prints the copyright/license information.");
836  add_list_item("\\fB--export-help\\fP (std::string)",
837  "Export the help page information. Value must be one of [html, man].");
838  if (version_check_dev_decision == update_notifications::on)
839  add_list_item("\\fB--version-check\\fP (bool)",
840  "Whether to check for the newest app version. Default: true.");
841  }
842 
848  template <typename id_type>
849  bool id_exists(id_type const & id)
850  {
851  if (detail::format_parse::is_empty_id(id))
852  return false;
853  return (!(used_option_ids.insert(std::string({id}))).second);
854  }
855 
865  void verify_identifiers(char const short_id, std::string const & long_id)
866  {
867  constexpr auto allowed = is_alnum || is_char<'_'> || is_char<'@'>;
868 
869  if (id_exists(short_id))
870  throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
871  if (id_exists(long_id))
872  throw design_error("Option Identifier '" + long_id + "' was already used before.");
873  if (long_id.length() == 1)
874  throw design_error("Long IDs must be either empty, or longer than one character.");
875  if (!allowed(short_id) && !is_char<'\0'>(short_id))
876  throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
877  if (long_id.size() > 0 && is_char<'-'>(long_id[0]))
878  throw design_error("First character of long ID cannot be '-'.");
879 
880  std::for_each(long_id.begin(),
881  long_id.end(),
882  [&allowed](char c)
883  {
884  if (!(allowed(c) || is_char<'-'>(c)))
885  throw design_error(
886  "Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
887  });
888  if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
889  throw design_error("Option Identifiers cannot both be empty.");
890  }
891 };
892 
893 } // namespace seqan3
T begin(T... args)
The SeqAn command line parser.
Definition: argument_parser.hpp:152
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:274
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:179
~argument_parser()
The destructor.
Definition: argument_parser.hpp:208
requires std::same_as< id_type, char > std::constructible_from< std::string, id_type > 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:479
argument_parser_meta_data info
Aggregates all parser related meta data (see seqan3::argument_parser_meta_data struct).
Definition: argument_parser.hpp:639
argument_parser(argument_parser &&)=default
Defaulted.
void parse()
Initiates the actual command line parsing.
Definition: argument_parser.hpp:407
argument_parser()=delete
Deleted.
requires(argument_parser_compatible_option< option_type >||argument_parser_compatible_option< std::ranges::range_value_t< option_type >>) &&std
Adds an option to the seqan3::argument_parser.
Definition: argument_parser.hpp:240
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:551
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:441
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:580
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:516
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:532
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:151
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:248
@ standard
The default were no checking or special displaying is happening.
Definition: auxiliary.hpp:249
constexpr auto is_alnum
Checks whether c is a alphanumeric character.
Definition: predicate.hpp:197
constexpr ptrdiff_t find
Get the index of the first occurrence of a type in a pack.
Definition: type_pack/traits.hpp:182
T insert(T... args)
Checks whether the the type can be used in an add_(positional_)option call on the argument parser.
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:267
@ off
Automatic update notifications should be disabled.
@ on
Automatic update notifications should be enabled.
T push_back(T... args)
T regex_match(T... args)
T length(T... args)
std::string version
The version information MAJOR.MINOR.PATH (e.g. 3.1.3)
Definition: auxiliary.hpp:294
std::string app_name
The application name that will be displayed on the help page.
Definition: auxiliary.hpp:292
std::string url
A link to your github/gitlab project with the newest release.
Definition: auxiliary.hpp:306
T substr(T... args)
Checks if program is run interactively and retrieves dimensions of terminal (Transferred from seqan2)...
Forward declares seqan3::detail::test_accessor.
Auxiliary for pretty printing of exception messages.
T valid(T... args)
Provides the version check functionality.
T visit(T... args)
T wait_for(T... args)