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
34namespace seqan3
35{
36
152{
153public:
157 argument_parser() = delete;
158 argument_parser(argument_parser const &) = default;
162
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>>
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,
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.
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,
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.
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>>
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.
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
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
519 {
521 [&](auto & f)
522 {
523 f.add_section(title, spec);
524 },
525 format);
526 }
527
535 {
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 {
556 [&](auto & f)
557 {
558 f.add_line(text, is_paragraph, spec);
559 },
560 format);
561 }
562
581 void
583 {
585 [&](auto & f)
586 {
587 f.add_list_item(key, desc, spec);
588 },
589 format);
590 }
592
642
643private:
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_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:319
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:275
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:481
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:243
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 & operator=(argument_parser &&)=default
Defaulted.
~argument_parser()
The destructor.
Definition: argument_parser.hpp:208
argument_parser_meta_data info
Aggregates all parser related meta data (see seqan3::argument_parser_meta_data struct).
Definition: argument_parser.hpp:641
argument_parser(argument_parser &&)=default
Defaulted.
void parse()
Initiates the actual command line parsing.
Definition: argument_parser.hpp:409
argument_parser()=delete
Deleted.
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:553
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:582
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:518
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:534
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:443
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
Argument parser exception thrown when too few arguments are provided.
Definition: exceptions.hpp:79
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
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)
Stores all parser related meta information of the seqan3::argument_parser.
Definition: auxiliary.hpp:286
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)