Sharg 1.1.2-rc.1
The argument parser for bio-c++ tools.
Loading...
Searching...
No Matches
parser.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2006-2024, Knut Reinert & Freie Universität Berlin
2// SPDX-FileCopyrightText: 2016-2024, Knut Reinert & MPI für molekulare Genetik
3// SPDX-License-Identifier: BSD-3-Clause
4
10#pragma once
11
12#include <unordered_set>
13#include <variant>
14
15#include <sharg/config.hpp>
22
23namespace sharg
24{
25
154{
155public:
159 parser() = delete;
160 parser(parser const &) = delete;
161 parser & operator=(parser const &) = delete;
162 parser(parser &&) = default;
163 parser & operator=(parser &&) = default;
164
181 parser(std::string const & app_name,
182 std::vector<std::string> const & arguments,
184 std::vector<std::string> subcommands = {}) :
185 version_check_dev_decision{version_updates},
186 arguments{arguments}
187 {
188 add_subcommands(subcommands);
189 info.app_name = app_name;
190 }
191
193 parser(std::string const & app_name,
194 int const argc,
195 char const * const * const argv,
197 std::vector<std::string> subcommands = {}) :
198 parser{app_name, std::vector<std::string>{argv, argv + argc}, version_updates, std::move(subcommands)}
199 {}
200
203 {
204 // wait for another 3 seconds
205 if (version_check_future.valid())
206 version_check_future.wait_for(std::chrono::seconds(3));
207 }
209
238 template <typename option_type, typename validator_type>
241 void add_option(option_type & value, config<validator_type> const & config)
242 {
243 check_parse_not_called("add_option");
244 verify_option_config(config);
245
246 auto operation = [this, &value, config]()
247 {
248 auto visit_fn = [&value, &config](auto & f)
249 {
250 f.add_option(value, config);
251 };
252
253 std::visit(std::move(visit_fn), format);
254 };
255
256 operations.push_back(std::move(operation));
257 }
258
272 template <typename validator_type>
274 void add_flag(bool & value, config<validator_type> const & config)
275 {
276 check_parse_not_called("add_flag");
277 verify_flag_config(config);
278
279 if (value)
280 throw design_error("A flag's default value must be false.");
281
282 auto operation = [this, &value, config]()
283 {
284 auto visit_fn = [&value, &config](auto & f)
285 {
286 f.add_flag(value, config);
287 };
288
289 std::visit(std::move(visit_fn), format);
290 };
291
292 operations.push_back(std::move(operation));
293 }
294
322 template <typename option_type, typename validator_type>
325 void add_positional_option(option_type & value, config<validator_type> const & config)
326 {
327 check_parse_not_called("add_positional_option");
328 verify_positional_option_config(config);
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 auto operation = [this, &value, config]()
334 {
335 auto visit_fn = [&value, &config](auto & f)
336 {
337 f.add_positional_option(value, config);
338 };
339
340 std::visit(std::move(visit_fn), format);
341 };
342
343 operations.push_back(std::move(operation));
344 }
346
414 void parse()
415 {
416 if (parse_was_called)
417 throw design_error("The function parse() must only be called once!");
418
419 parse_was_called = true;
420
421 // User input sanitization must happen before version check!
422 verify_app_and_subcommand_names();
423
424 // Determine the format and subcommand.
425 determine_format_and_subcommand();
426
427 // Apply all defered operations to the parser, e.g., `add_option`, `add_flag`, `add_positional_option`.
428 for (auto & operation : operations)
429 operation();
430
431 // The version check, which might exit the program, must be called before calling parse on the format.
432 run_version_check();
433
434 // Parse the command line arguments.
435 parse_format();
436
437 // Exit after parsing any special format.
439 std::exit(EXIT_SUCCESS);
440 }
441
449 {
450 if (sub_parser == nullptr)
451 {
452 throw design_error("No subcommand was provided at the construction of the argument parser!");
453 }
454
455 return *sub_parser;
456 }
457
487 // clang-format off
488 template <typename id_type>
490 bool is_option_set(id_type const & id) const
491 // clang-format on
492 {
493 if (!parse_was_called)
494 throw design_error{"You can only ask which options have been set after calling the function `parse()`."};
495
496 // the detail::format_parse::find_option_id call in the end expects either a char or std::string
497 using char_or_string_t = std::conditional_t<std::same_as<id_type, char>, char, std::string>;
498 char_or_string_t short_or_long_id = {id}; // e.g. convert char * to string here if necessary
499
500 if constexpr (!std::same_as<id_type, char>) // long id was given
501 {
502 if (short_or_long_id.size() == 1)
503 {
504 throw design_error{"Long option identifiers must be longer than one character! If " + short_or_long_id
505 + "' was meant to be a short identifier, please pass it as a char ('') not a string"
506 " (\"\")!"};
507 }
508 }
509
510 if (!used_ids.contains(std::string{id}))
511 throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
512
513 // we only need to search for an option before the `option_end_identifier` (`--`)
514 auto option_end = std::find(format_arguments.begin(), format_arguments.end(), option_end_identifier);
515 auto option_it = detail::format_parse::find_option_id(format_arguments.begin(), option_end, short_or_long_id);
516 return option_it != option_end;
517 }
518
521
532 void add_section(std::string const & title, bool const advanced_only = false)
533 {
534 check_parse_not_called("add_section");
535
536 auto operation = [this, title, advanced_only]()
537 {
538 auto visit_fn = [&title, advanced_only](auto & f)
539 {
540 f.add_section(title, advanced_only);
541 };
542
543 std::visit(std::move(visit_fn), format);
544 };
545
546 operations.push_back(std::move(operation));
547 }
548
559 void add_subsection(std::string const & title, bool const advanced_only = false)
560 {
561 check_parse_not_called("add_subsection");
562
563 auto operation = [this, title, advanced_only]()
564 {
565 auto visit_fn = [&title, advanced_only](auto & f)
566 {
567 f.add_subsection(title, advanced_only);
568 };
569
570 std::visit(std::move(visit_fn), format);
571 };
572
573 operations.push_back(std::move(operation));
574 }
575
587 void add_line(std::string const & text, bool is_paragraph = false, bool const advanced_only = false)
588 {
589 check_parse_not_called("add_line");
590
591 auto operation = [this, text, is_paragraph, advanced_only]()
592 {
593 auto visit_fn = [&text, is_paragraph, advanced_only](auto & f)
594 {
595 f.add_line(text, is_paragraph, advanced_only);
596 };
597
598 std::visit(std::move(visit_fn), format);
599 };
600
601 operations.push_back(std::move(operation));
602 }
603
624 void add_list_item(std::string const & key, std::string const & desc, bool const advanced_only = false)
625 {
626 check_parse_not_called("add_list_item");
627
628 auto operation = [this, key, desc, advanced_only]()
629 {
630 auto visit_fn = [&key, &desc, advanced_only](auto & f)
631 {
632 f.add_list_item(key, desc, advanced_only);
633 };
634
635 std::visit(std::move(visit_fn), format);
636 };
637
638 operations.push_back(std::move(operation));
639 }
640
653 {
654 auto & parser_subcommands = this->subcommands;
655 parser_subcommands.insert(parser_subcommands.end(), subcommands.cbegin(), subcommands.cend());
656
657 std::ranges::sort(parser_subcommands);
658 auto const [first, last] = std::ranges::unique(parser_subcommands);
659 parser_subcommands.erase(first, last);
660 }
662
714
715private:
717 bool parse_was_called{false};
718
720 bool has_positional_list_option{false};
721
723 update_notifications version_check_dev_decision{};
724
726 std::optional<bool> version_check_user_decision;
727
729 friend struct ::sharg::detail::test_accessor;
730
732 std::future<bool> version_check_future;
733
735 std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"};
736
738 static constexpr std::string_view const option_end_identifier{"--"};
739
741 std::unique_ptr<parser> sub_parser{nullptr};
742
744 std::vector<std::string> subcommands{};
745
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_tdl,
760 detail::format_copyright>
761 format{detail::format_short_help{}};
762
764 std::unordered_set<std::string> used_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
765
767 std::vector<std::string> format_arguments{};
768
770 std::vector<std::string> arguments{};
771
773 std::vector<std::string> executable_name{};
774
777
779 std::vector<std::function<void()>> operations;
780
808 void determine_format_and_subcommand()
809 {
810 assert(!arguments.empty());
811
812 auto it = arguments.begin();
813 std::string_view arg{*it};
814
815 executable_name.emplace_back(arg);
816
817 // Helper function for reading the next argument. This makes it more obvious that we are
818 // incrementing `it` (version-check, and export-help).
819 auto read_next_arg = [this, &it, &arg]() -> bool
820 {
821 assert(it != arguments.end());
822
823 if (++it == arguments.end())
824 return false;
825
826 arg = *it;
827 return true;
828 };
829
830 // Helper function for finding and processing subcommands.
831 auto found_subcommand = [this, &it, &arg]() -> bool
832 {
833 if (subcommands.empty())
834 return false;
835
836 if (std::ranges::find(subcommands, arg) != subcommands.end())
837 {
838 sub_parser = std::make_unique<parser>(info.app_name + "-" + arg.data(),
839 std::vector<std::string>{it, arguments.end()},
841
842 // Add the original calls to the front, e.g. ["raptor"],
843 // s.t. ["raptor", "build"] will be the list after constructing the subparser
844 sub_parser->executable_name.insert(sub_parser->executable_name.begin(),
845 executable_name.begin(),
846 executable_name.end());
847 return true;
848 }
849 else
850 {
851 // Positional options are forbidden by design.
852 // Flags and options, which both start with '-', are allowed for the top-level parser.
853 // Otherwise, this is an unknown subcommand.
854 if (!arg.starts_with('-'))
855 {
856 std::string message = "You specified an unknown subcommand! Available subcommands are: [";
857 for (std::string const & command : subcommands)
858 message += command + ", ";
859 message.replace(message.size() - 2, 2, "]. Use -h/--help for more information.");
860
861 throw user_input_error{message};
862 }
863 }
864
865 return false;
866 };
867
868 // Process the arguments.
869 for (; read_next_arg();)
870 {
871 // The argument is a known option.
872 if (options.contains(std::string{arg}))
873 {
874 // No futher checks are needed.
875 format_arguments.emplace_back(arg);
876
877 // Consume the next argument (the option value) if possible.
878 if (read_next_arg())
879 {
880 format_arguments.emplace_back(arg);
881 continue;
882 }
883 else // Too few arguments. This is handled by format_parse.
884 {
885 break;
886 }
887 }
888
889 // If we have a subcommand, all further arguments are passed to the subparser.
890 if (found_subcommand())
891 break;
892
893 if (arg == "-h" || arg == "--help")
894 {
895 format = detail::format_help{subcommands, version_check_dev_decision, false};
896 }
897 else if (arg == "-hh" || arg == "--advanced-help")
898 {
899 format = detail::format_help{subcommands, version_check_dev_decision, true};
900 }
901 else if (arg == "--version")
902 {
903 format = detail::format_version{};
904 }
905 else if (arg == "--copyright")
906 {
907 format = detail::format_copyright{};
908 }
909 else if (arg == "--export-help" || arg.starts_with("--export-help="))
910 {
911 arg.remove_prefix(std::string_view{"--export-help"}.size());
912
913 // --export-help man
914 if (arg.empty())
915 {
916 if (!read_next_arg())
917 throw too_few_arguments{"Option --export-help must be followed by a value."};
918 }
919 else // --export-help=man
920 {
921 arg.remove_prefix(1u);
922 }
923
924 if (arg == "html")
925 format = detail::format_html{subcommands, version_check_dev_decision};
926 else if (arg == "man")
927 format = detail::format_man{subcommands, version_check_dev_decision};
928 else if (arg == "ctd")
929 format = detail::format_tdl{detail::format_tdl::FileFormat::CTD};
930 else if (arg == "cwl")
931 format = detail::format_tdl{detail::format_tdl::FileFormat::CWL};
932 else
933 throw validation_error{"Validation failed for option --export-help: "
934 "Value must be one of "
935 + detail::supported_exports + "."};
936 }
937 else if (arg == "--version-check")
938 {
939 if (!read_next_arg())
940 throw too_few_arguments{"Option --version-check must be followed by a value."};
941
942 if (arg == "1" || arg == "true")
943 version_check_user_decision = true;
944 else if (arg == "0" || arg == "false")
945 version_check_user_decision = false;
946 else
947 throw validation_error{"Value for option --version-check must be true (1) or false (0)."};
948 }
949 else
950 {
951 // Flags, positional options, options using an alternative syntax (--optionValue, --option=value), etc.
952 format_arguments.emplace_back(arg);
953 }
954 }
955
956 // A special format was set. We do not need to parse the format_arguments.
958 return;
959
960 // All special options have been handled. If there are arguments left or we have a subparser,
961 // we call format_parse. Oterhwise, we print the short help (default variant).
962 if (!format_arguments.empty() || sub_parser)
963 format = detail::format_parse(format_arguments);
964 }
965
971 template <typename id_type>
972 bool id_exists(id_type const & id)
973 {
974 if (detail::format_parse::is_empty_id(id))
975 return false;
976 return (!(used_ids.insert(std::string({id}))).second);
977 }
978
988 void verify_identifiers(char const short_id, std::string const & long_id)
989 {
990 auto is_valid = [](char const c) -> bool
991 {
992 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') // alphanumeric
993 || c == '@' || c == '_' || c == '-'; // additional characters
994 };
995
996 if (short_id == '\0' && long_id.empty())
997 throw design_error{"Short and long identifiers may not both be empty."};
998
999 if (short_id != '\0')
1000 {
1001 if (short_id == '-' || !is_valid(short_id))
1002 throw design_error{"Short identifiers may only contain alphanumeric characters, '_', or '@'."};
1003 if (id_exists(short_id))
1004 throw design_error{"Short identifier '" + std::string(1, short_id) + "' was already used before."};
1005 }
1006
1007 if (!long_id.empty())
1008 {
1009 if (long_id.size() == 1)
1010 throw design_error{"Long identifiers must be either empty or longer than one character."};
1011 if (long_id[0] == '-')
1012 throw design_error{"Long identifiers may not use '-' as first character."};
1013 if (!std::ranges::all_of(long_id, is_valid))
1014 throw design_error{"Long identifiers may only contain alphanumeric characters, '_', '-', or '@'."};
1015 if (id_exists(long_id))
1016 throw design_error{"Long identifier '" + long_id + "' was already used before."};
1017 }
1018 }
1019
1021 template <typename validator_t>
1022 void verify_option_config(config<validator_t> const & config)
1023 {
1024 verify_identifiers(config.short_id, config.long_id);
1025
1026 if (config.short_id != '\0')
1027 options.emplace(std::string{"-"} + config.short_id);
1028 if (!config.long_id.empty())
1029 options.emplace(std::string{"--"} + config.long_id);
1030
1031 if (config.required && !config.default_message.empty())
1032 throw design_error{"A required option cannot have a default message."};
1033 }
1034
1036 template <typename validator_t>
1037 void verify_flag_config(config<validator_t> const & config)
1038 {
1039 verify_identifiers(config.short_id, config.long_id);
1040
1041 if (!config.default_message.empty())
1042 throw design_error{"A flag may not have a default message because the default is always `false`."};
1043 }
1044
1046 template <typename validator_t>
1047 void verify_positional_option_config(config<validator_t> const & config) const
1048 {
1049 if (config.short_id != '\0' || config.long_id != "")
1050 throw design_error{"Positional options are identified by their position on the command line. "
1051 "Short or long ids are not permitted!"};
1052
1053 if (config.advanced || config.hidden)
1054 throw design_error{"Positional options are always required and therefore cannot be advanced nor hidden!"};
1055
1056 if (!subcommands.empty())
1057 throw design_error{"You may only specify flags and options for the top-level parser."};
1058
1059 if (has_positional_list_option)
1060 throw design_error{"You added a positional option with a list value before so you cannot add "
1061 "any other positional options."};
1062
1063 if (!config.default_message.empty())
1064 throw design_error{"A positional option may not have a default message because it is always required."};
1065 }
1066
1076 inline void check_parse_not_called(std::string_view const function_name) const
1077 {
1078 if (parse_was_called)
1079 throw design_error{detail::to_string(function_name.data(), " may only be used before calling parse().")};
1080 }
1081
1089 inline void verify_app_and_subcommand_names() const
1090 {
1091 // Before creating the detail::version_checker, we have to make sure that
1092 // malicious code cannot be injected through the app name.
1093 if (!std::regex_match(info.app_name, app_name_regex))
1094 {
1095 throw design_error{("The application name must only contain alpha-numeric characters or '_' and '-' "
1096 "(regex: \"^[a-zA-Z0-9_-]+$\").")};
1097 }
1098
1099 for (auto & sub : this->subcommands)
1100 {
1101 if (!std::regex_match(sub, app_name_regex))
1102 {
1103 throw design_error{"The subcommand name must only contain alpha-numeric characters or '_' and '-' "
1104 "(regex: \"^[a-zA-Z0-9_-]+$\")."};
1105 }
1106 }
1107 }
1108
1114 inline void run_version_check()
1115 {
1116 detail::version_checker app_version{info.app_name, info.version, info.url};
1117
1118 if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
1119 {
1120 // must be done before calling parse on the format because this might std::exit
1121 std::promise<bool> app_version_prom;
1122 version_check_future = app_version_prom.get_future();
1123 app_version(std::move(app_version_prom));
1124 }
1125 }
1126
1137 inline void parse_format()
1138 {
1139 auto format_parse_fn = [this]<typename format_t>(format_t & f)
1140 {
1142 f.parse(info, executable_name);
1143 else
1144 f.parse(info);
1145 };
1146
1147 std::visit(std::move(format_parse_fn), format);
1148 }
1149};
1150
1151} // namespace sharg
T all_of(T... args)
T cbegin(T... args)
Parser exception that is thrown whenever there is an design error directed at the developer of the ap...
Definition exceptions.hpp:207
The Sharg command line parser.
Definition parser.hpp:154
void add_option(option_type &value, config< validator_type > const &config)
Adds an option to the sharg::parser.
Definition parser.hpp:241
void add_flag(bool &value, config< validator_type > const &config)
Adds a flag to the sharg::parser.
Definition parser.hpp:274
parser()=delete
Deleted.
void add_subsection(std::string const &title, bool const advanced_only=false)
Adds an help page subsection to the sharg::parser.
Definition parser.hpp:559
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 parser.hpp:490
void add_subcommands(std::vector< std::string > const &subcommands)
Adds subcommands to the parser.
Definition parser.hpp:652
void add_positional_option(option_type &value, config< validator_type > const &config)
Adds a positional option to the sharg::parser.
Definition parser.hpp:325
parser_meta_data info
Aggregates all parser related meta data (see sharg::parser_meta_data struct).
Definition parser.hpp:713
parser(std::string const &app_name, std::vector< std::string > const &arguments, update_notifications version_updates=update_notifications::on, std::vector< std::string > subcommands={})
Initializes an sharg::parser object from the command line arguments.
Definition parser.hpp:181
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={})
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition parser.hpp:193
parser(parser &&)=default
Defaulted.
void parse()
Initiates the actual command line parsing.
Definition parser.hpp:414
parser & operator=(parser const &)=delete
Deleted.
parser(parser const &)=delete
Deleted.
void add_list_item(std::string const &key, std::string const &desc, bool const advanced_only=false)
Adds an help page list item (key-value) to the sharg::parser.
Definition parser.hpp:624
parser & operator=(parser &&)=default
Defaulted.
void add_section(std::string const &title, bool const advanced_only=false)
Adds an help page section to the sharg::parser.
Definition parser.hpp:532
~parser()
The destructor.
Definition parser.hpp:202
parser & get_sub_parser()
Returns a reference to the sub-parser instance if subcommand parsing was enabled.
Definition parser.hpp:448
void add_line(std::string const &text, bool is_paragraph=false, bool const advanced_only=false)
Adds an help page text line to the sharg::parser.
Definition parser.hpp:587
Checks whether the the type can be used in an add_(positional_)option call on the parser.
Definition concept.hpp:91
Provides sharg::config class.
T data(T... args)
T empty(T... args)
T cend(T... args)
T exit(T... args)
T find(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.
Provides the format_tdl struct and its helper functions.
T format(T... args)
T get_future(T... args)
update_notifications
Indicates whether application allows automatic update notifications by the sharg::parser.
Definition auxiliary.hpp:26
@ off
Automatic update notifications should be disabled.
@ on
Automatic update notifications should be enabled.
T insert(T... args)
T is_same_v
T regex_match(T... args)
T replace(T... args)
T size(T... args)
T sort(T... args)
Option struct that is passed to the sharg::parser::add_option() function.
Definition config.hpp:43
Stores all parser related meta information of the sharg::parser.
Definition auxiliary.hpp:45
std::string app_name
The application name that will be displayed on the help page.
Definition auxiliary.hpp:51
std::string version
The version information MAJOR.MINOR.PATH (e.g. 3.1.3)
Definition auxiliary.hpp:54
std::string url
A link to your github/gitlab project with the newest release.
Definition auxiliary.hpp:71
T unique(T... args)
Provides the version check functionality.
T visit(T... args)
Hide me