Sharg 1.1.0
The argument parser for bio-c++ tools.
 
Loading...
Searching...
No Matches
parser.hpp
Go to the documentation of this file.
1// --------------------------------------------------------------------------------------------------------
2// Copyright (c) 2006-2023, Knut Reinert & Freie Universität Berlin
3// Copyright (c) 2016-2023, 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/sharg-parser/blob/main/LICENSE.md
6// --------------------------------------------------------------------------------------------------------
7
13#pragma once
14
15#include <set>
16#include <variant>
17
18#include <sharg/config.hpp>
25
26namespace sharg
27{
28
157{
158public:
162 parser() = delete;
163 parser(parser const &) = delete;
164 parser & operator=(parser const &) = delete;
165 parser(parser &&) = default;
166 parser & operator=(parser &&) = default;
167
187 parser(std::string const & app_name,
188 int const argc,
189 char const * const * const argv,
191 std::vector<std::string> subcommands = {}) :
192 version_check_dev_decision{version_updates},
193 subcommands{std::move(subcommands)}
194 {
195 for (auto & sub : this->subcommands)
196 {
197 if (!std::regex_match(sub, app_name_regex))
198 {
199 throw design_error{"The subcommand name must only contain alpha-numeric characters or '_' and '-' "
200 "(regex: \"^[a-zA-Z0-9_-]+$\")."};
201 }
202 }
203
204 info.app_name = app_name;
205
206 init(argc, argv);
207 }
208
211 {
212 // wait for another 3 seconds
213 if (version_check_future.valid())
214 version_check_future.wait_for(std::chrono::seconds(3));
215 }
217
243 template <typename option_type, typename validator_type>
245 && std::invocable<validator_type, option_type>
246 void add_option(option_type & value, config<validator_type> const & config)
247 {
248 verify_option_config(config);
249
250 // copy variables into the lambda because the calls are pushed to a stack
251 // and the references would go out of scope.
252 std::visit(
253 [&value, &config](auto & f)
254 {
255 f.add_option(value, config);
256 },
257 format);
258 }
259
270 template <typename validator_type>
271 requires std::invocable<validator_type, bool>
272 void add_flag(bool & value, config<validator_type> const & config)
273 {
274 verify_flag_config(config);
275
276 if (value)
277 throw design_error("A flag's default value must be false.");
278
279 // copy variables into the lambda because the calls are pushed to a stack
280 // and the references would go out of scope.
281 std::visit(
282 [&value, &config](auto & f)
283 {
284 f.add_flag(value, config);
285 },
286 format);
287 }
288
311 template <typename option_type, typename validator_type>
313 && std::invocable<validator_type, option_type>
314 void add_positional_option(option_type & value, config<validator_type> const & config)
315 {
316 verify_positional_option_config(config);
317
318 if constexpr (detail::is_container_option<option_type>)
319 has_positional_list_option = true; // keep track of a list option because there must be only one!
320
321 // copy variables into the lambda because the calls are pushed to a stack
322 // and the references would go out of scope.
323 std::visit(
324 [&value, &config](auto & f)
325 {
326 f.add_positional_option(value, config);
327 },
328 format);
329 }
331
399 void parse()
400 {
401 if (parse_was_called)
402 throw design_error("The function parse() must only be called once!");
403
404 // Before creating the detail::version_checker, we have to make sure that
405 // malicious code cannot be injected through the app name.
406 if (!std::regex_match(info.app_name, app_name_regex))
407 {
408 throw design_error{("The application name must only contain alpha-numeric characters or '_' and '-' "
409 "(regex: \"^[a-zA-Z0-9_-]+$\").")};
410 }
411
412 detail::version_checker app_version{info.app_name, info.version, info.url};
413
414 if (std::holds_alternative<detail::format_parse>(format) && !subcommands.empty() && sub_parser == nullptr)
415 {
416 assert(!subcommands.empty());
417 std::string subcommands_str{"["};
418 for (std::string const & command : subcommands)
419 subcommands_str += command + ", ";
420 subcommands_str.replace(subcommands_str.size() - 2, 2, "]"); // replace last ", " by "]"
421
422 throw too_few_arguments{"You misspelled the subcommand! Please specify which sub-program "
423 "you want to use: one of "
424 + subcommands_str
425 + ". Use -h/--help for more "
426 "information."};
427 }
428
429 if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
430 {
431 // must be done before calling parse on the format because this might std::exit
432 std::promise<bool> app_version_prom;
433 version_check_future = app_version_prom.get_future();
434 app_version(std::move(app_version_prom));
435 }
436 std::visit(
437 [this]<typename T>(T & f)
438 {
439 if constexpr (std::same_as<T, detail::format_tdl>)
440 {
441 f.parse(info, executable_name);
442 }
443 else
444 {
445 f.parse(info);
446 }
447 },
448 format);
449 parse_was_called = true;
450
451 // Exit after parsing any special format.
452 if (!std::holds_alternative<detail::format_parse>(format))
453 std::exit(EXIT_SUCCESS);
454 }
455
463 {
464 if (sub_parser == nullptr)
465 {
466 throw design_error("No subcommand was provided at the construction of the argument parser!");
467 }
468
469 return *sub_parser;
470 }
471
501 // clang-format off
502 template <typename id_type>
503 requires std::same_as<id_type, char> || std::constructible_from<std::string, id_type>
504 bool is_option_set(id_type const & id) const
505 // clang-format on
506 {
507 if (!parse_was_called)
508 throw design_error{"You can only ask which options have been set after calling the function `parse()`."};
509
510 // the detail::format_parse::find_option_id call in the end expects either a char or std::string
511 using char_or_string_t = std::conditional_t<std::same_as<id_type, char>, char, std::string>;
512 char_or_string_t short_or_long_id = {id}; // e.g. convert char * to string here if necessary
513
514 if constexpr (!std::same_as<id_type, char>) // long id was given
515 {
516 if (short_or_long_id.size() == 1)
517 {
518 throw design_error{"Long option identifiers must be longer than one character! If " + short_or_long_id
519 + "' was meant to be a short identifier, please pass it as a char ('') not a string"
520 " (\"\")!"};
521 }
522 }
523
524 if (std::find(used_option_ids.begin(), used_option_ids.end(), std::string{id}) == used_option_ids.end())
525 throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
526
527 // we only need to search for an option before the `end_of_options_indentifier` (`--`)
528 auto end_of_options = std::find(cmd_arguments.begin(), cmd_arguments.end(), end_of_options_indentifier);
529 auto option_it = detail::format_parse::find_option_id(cmd_arguments.begin(), end_of_options, short_or_long_id);
530 return option_it != end_of_options;
531 }
532
535
545 void add_section(std::string const & title, bool const advanced_only = false)
546 {
547 std::visit(
548 [&title, advanced_only](auto & f)
549 {
550 f.add_section(title, advanced_only);
551 },
552 format);
553 }
554
564 void add_subsection(std::string const & title, bool const advanced_only = false)
565 {
566 std::visit(
567 [&title, advanced_only](auto & f)
568 {
569 f.add_subsection(title, advanced_only);
570 },
571 format);
572 }
573
584 void add_line(std::string const & text, bool is_paragraph = false, bool const advanced_only = false)
585 {
586 std::visit(
587 [&text, is_paragraph, advanced_only](auto & f)
588 {
589 f.add_line(text, is_paragraph, advanced_only);
590 },
591 format);
592 }
593
613 void add_list_item(std::string const & key, std::string const & desc, bool const advanced_only = false)
614 {
615 std::visit(
616 [&key, &desc, advanced_only](auto & f)
617 {
618 f.add_list_item(key, desc, advanced_only);
619 },
620 format);
621 }
623
675
676private:
678 bool parse_was_called{false};
679
681 bool has_positional_list_option{false};
682
684 update_notifications version_check_dev_decision{};
685
687 std::optional<bool> version_check_user_decision;
688
690 friend struct ::sharg::detail::test_accessor;
691
693 std::future<bool> version_check_future;
694
696 std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"};
697
699 static constexpr std::string_view const end_of_options_indentifier{"--"};
700
702 std::unique_ptr<parser> sub_parser{nullptr};
703
705 std::vector<std::string> subcommands{};
706
714 std::variant<detail::format_parse,
715 detail::format_help,
716 detail::format_short_help,
717 detail::format_version,
718 detail::format_html,
719 detail::format_man,
720 detail::format_tdl,
721 detail::format_copyright/*,
722 detail::format_ctd*/> format{detail::format_help{{}, {}, false}}; // Will be overwritten in any case.
723
725 std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
726
728 std::vector<std::string> cmd_arguments{};
729
731 std::vector<std::string> executable_name{};
732
765 void init(int argc, char const * const * const argv)
766 {
767 assert(argc > 0);
768 executable_name.emplace_back(argv[0]);
769
770 bool special_format_was_set{false};
771
772 for (int i = 1, argv_len = argc; i < argv_len; ++i) // start at 1 to skip binary name
773 {
774 std::string_view arg{argv[i]};
775
776 if (!subcommands.empty()) // this is a top_level parser
777 {
778 if (std::ranges::find(subcommands, arg) != subcommands.end()) // identified subparser
779 {
780 // LCOV_EXCL_START
781 sub_parser = std::make_unique<parser>(info.app_name + "-" + arg.data(),
782 argc - i,
783 argv + i,
785 // LCOV_EXCL_STOP
786
787 // Add the original calls to the front, e.g. ["raptor"],
788 // s.t. ["raptor", "build"] will be the list after constructing the subparser
789 sub_parser->executable_name.insert(sub_parser->executable_name.begin(),
790 executable_name.begin(),
791 executable_name.end());
792 break;
793 }
794 else
795 {
796 // Options and positional options are forbidden by design.
797 // Flags starting with '-' are allowed for the top-level parser.
798 // Otherwise, this is a wrongly spelled subcommand. The error will be thrown in parse().
799 if (!arg.empty() && arg[0] != '-')
800 break;
801 }
802 }
803
804 if (arg == "-h" || arg == "--help")
805 {
806 format = detail::format_help{subcommands, version_check_dev_decision, false};
807 special_format_was_set = true;
808 }
809 else if (arg == "-hh" || arg == "--advanced-help")
810 {
811 format = detail::format_help{subcommands, version_check_dev_decision, true};
812 special_format_was_set = true;
813 }
814 else if (arg == "--version")
815 {
816 format = detail::format_version{};
817 special_format_was_set = true;
818 }
819 else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed
820 {
821 std::string export_format;
822
823 if (arg.size() > 13)
824 {
825 export_format = arg.substr(14);
826 }
827 else
828 {
829 if (argv_len <= i + 1)
830 throw too_few_arguments{"Option --export-help must be followed by a value."};
831 export_format = {argv[i + 1]};
832 }
833
834 if (export_format == "html")
835 format = detail::format_html{subcommands, version_check_dev_decision};
836 else if (export_format == "man")
837 format = detail::format_man{subcommands, version_check_dev_decision};
838 else if (export_format == "ctd")
839 format = detail::format_tdl{detail::format_tdl::FileFormat::CTD};
840 else if (export_format == "cwl")
841 format = detail::format_tdl{detail::format_tdl::FileFormat::CWL};
842 else
843 throw validation_error{"Validation failed for option --export-help: "
844 "Value must be one of [html, man, ctd, cwl]."};
845 special_format_was_set = true;
846 }
847 else if (arg == "--copyright")
848 {
849 format = detail::format_copyright{};
850 special_format_was_set = true;
851 }
852 else if (arg == "--version-check")
853 {
854 if (++i >= argv_len)
855 throw too_few_arguments{"Option --version-check must be followed by a value."};
856
857 arg = argv[i];
858
859 if (arg == "1" || arg == "true")
860 version_check_user_decision = true;
861 else if (arg == "0" || arg == "false")
862 version_check_user_decision = false;
863 else
864 throw validation_error{"Value for option --version-check must be true (1) or false (0)."};
865
866 // in case --version-check is specified it shall not be passed to format_parse()
867 argc -= 2;
868 }
869 else
870 {
871 cmd_arguments.emplace_back(arg);
872 }
873 }
874
875 // all special options have been identified, which might involve deleting them from argv (e.g. version-check)
876 // check if no actual options remain and then call the short help page.
877 if (argc <= 1) // no arguments provided
878 {
879 format = detail::format_short_help{};
880 return;
881 }
882
883 if (!special_format_was_set)
884 format = detail::format_parse(argc, cmd_arguments);
885 }
886
892 template <typename id_type>
893 bool id_exists(id_type const & id)
894 {
895 if (detail::format_parse::is_empty_id(id))
896 return false;
897 return (!(used_option_ids.insert(std::string({id}))).second);
898 }
899
909 void verify_identifiers(char const short_id, std::string const & long_id)
910 {
911 constexpr std::string_view valid_chars{"@_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
912 auto is_valid = [&valid_chars](char const c)
913 {
914 return valid_chars.find(c) != std::string::npos;
915 };
916
917 if (id_exists(short_id))
918 throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
919 if (id_exists(long_id))
920 throw design_error("Option Identifier '" + long_id + "' was already used before.");
921 if (long_id.length() == 1)
922 throw design_error("Long IDs must be either empty, or longer than one character.");
923 if ((short_id != '\0') && !is_valid(short_id))
924 throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
925 if (long_id.size() > 0 && (long_id[0] == '-'))
926 throw design_error("First character of long ID cannot be '-'.");
927
928 std::for_each(long_id.begin(),
929 long_id.end(),
930 [&is_valid](char c)
931 {
932 if (!((c == '-') || is_valid(c)))
933 throw design_error(
934 "Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
935 });
936 if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
937 throw design_error("Option Identifiers cannot both be empty.");
938 }
939
941 void verify_option_config(config<auto> const & config)
942 {
943 if (sub_parser != nullptr)
944 throw design_error{"You may only specify flags for the top-level parser."};
945
946 verify_identifiers(config.short_id, config.long_id);
947
948 if (config.required && !config.default_message.empty())
949 throw design_error{"A required option cannot have a default message."};
950 }
951
953 void verify_flag_config(config<auto> const & config)
954 {
955 verify_identifiers(config.short_id, config.long_id);
956
957 if (!config.default_message.empty())
958 throw design_error{"A flag may not have a default message because the default is always `false`."};
959 }
960
962 void verify_positional_option_config(config<auto> const & config) const
963 {
964 if (config.short_id != '\0' || config.long_id != "")
965 throw design_error{"Positional options are identified by their position on the command line. "
966 "Short or long ids are not permitted!"};
967
968 if (config.advanced || config.hidden)
969 throw design_error{"Positional options are always required and therefore cannot be advanced nor hidden!"};
970
971 if (sub_parser != nullptr)
972 throw design_error{"You may only specify flags for the top-level parser."};
973
974 if (has_positional_list_option)
975 throw design_error{"You added a positional option with a list value before so you cannot add "
976 "any other positional options."};
977
978 if (!config.default_message.empty())
979 throw design_error{"A positional option may not have a default message because it is always required."};
980 }
981};
982
983} // namespace sharg
T begin(T... args)
Parser exception that is thrown whenever there is an design error directed at the developer of the ap...
Definition: exceptions.hpp:210
The Sharg command line parser.
Definition: parser.hpp:157
void add_option(option_type &value, config< validator_type > const &config)
Adds an option to the sharg::parser.
Definition: parser.hpp:246
void add_flag(bool &value, config< validator_type > const &config)
Adds a flag to the sharg::parser.
Definition: parser.hpp:272
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:564
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:504
void add_positional_option(option_type &value, config< validator_type > const &config)
Adds a positional option to the sharg::parser.
Definition: parser.hpp:314
parser_meta_data info
Aggregates all parser related meta data (see sharg::parser_meta_data struct).
Definition: parser.hpp:674
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 sharg::parser object from the command line arguments.
Definition: parser.hpp:187
parser(parser &&)=default
Defaulted.
void parse()
Initiates the actual command line parsing.
Definition: parser.hpp:399
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:613
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:545
~parser()
The destructor.
Definition: parser.hpp:210
parser & get_sub_parser()
Returns a reference to the sub-parser instance if subcommand parsing was enabled.
Definition: parser.hpp:462
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:584
Parser exception thrown when too few arguments are provided.
Definition: exceptions.hpp:103
Checks whether the the type can be used in an add_(positional_)option call on the parser.
Definition: concept.hpp:94
Provides sharg::config class.
T data(T... args)
T emplace_back(T... args)
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.
Provides the format_tdl struct and its helper functions.
T get_future(T... args)
update_notifications
Indicates whether application allows automatic update notifications by the sharg::parser.
Definition: auxiliary.hpp:29
@ off
Automatic update notifications should be disabled.
@ on
Automatic update notifications should be enabled.
T insert(T... args)
T replace(T... args)
T length(T... args)
Option struct that is passed to the sharg::parser::add_option() function.
Definition: config.hpp:46
Stores all parser related meta information of the sharg::parser.
Definition: auxiliary.hpp:48
std::string app_name
The application name that will be displayed on the help page.
Definition: auxiliary.hpp:54
std::string version
The version information MAJOR.MINOR.PATH (e.g. 3.1.3)
Definition: auxiliary.hpp:57
std::string url
A link to your github/gitlab project with the newest release.
Definition: auxiliary.hpp:74
T substr(T... args)
T valid(T... args)
Provides the version check functionality.
T wait_for(T... args)