sharg 1.0.0
THE argument parser for bio-c++ tools.
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/sharg-parser/blob/main/LICENSE.md
6// --------------------------------------------------------------------------------------------------------
7
13#pragma once
14
15#include <set>
16#include <variant>
17
18#include <sharg/config.hpp>
24
25namespace sharg
26{
27
156{
157public:
161 parser() = delete;
162 parser(parser const &) = default;
163 parser & operator=(parser const &) = default;
164 parser(parser &&) = default;
165 parser & operator=(parser &&) = default;
166
186 parser(std::string const & app_name,
187 int const argc,
188 char const * const * const argv,
190 std::vector<std::string> subcommands = {}) :
191 version_check_dev_decision{version_updates},
192 subcommands{std::move(subcommands)}
193 {
194 if (!std::regex_match(app_name, app_name_regex))
195 {
196 throw design_error{("The application name must only contain alpha-numeric characters or '_' and '-' "
197 "(regex: \"^[a-zA-Z0-9_-]+$\").")};
198 }
199
200 for (auto & sub : this->subcommands)
201 {
202 if (!std::regex_match(sub, app_name_regex))
203 {
204 throw design_error{"The subcommand name must only contain alpha-numeric characters or '_' and '-' "
205 "(regex: \"^[a-zA-Z0-9_-]+$\")."};
206 }
207 }
208
209 info.app_name = app_name;
210
211 init(argc, argv);
212 }
213
216 {
217 // wait for another 3 seconds
218 if (version_check_future.valid())
219 version_check_future.wait_for(std::chrono::seconds(3));
220 }
222
248 template <typename option_type, typename validator_type>
250 && std::invocable<validator_type, option_type>
251 void add_option(option_type & value, config<validator_type> const & config)
252 {
253 verify_option_config(config);
254
255 // copy variables into the lambda because the calls are pushed to a stack
256 // and the references would go out of scope.
257 std::visit(
258 [&value, &config](auto & f)
259 {
260 f.add_option(value, config);
261 },
262 format);
263 }
264
275 template <typename validator_type>
276 requires std::invocable<validator_type, bool>
277 void add_flag(bool & value, config<validator_type> const & config)
278 {
279 verify_flag_config(config);
280
281 if (value)
282 throw design_error("A flag's default value must be false.");
283
284 // copy variables into the lambda because the calls are pushed to a stack
285 // and the references would go out of scope.
286 std::visit(
287 [&value, &config](auto & f)
288 {
289 f.add_flag(value, config);
290 },
291 format);
292 }
293
316 template <typename option_type, typename validator_type>
318 && std::invocable<validator_type, option_type>
319 void add_positional_option(option_type & value, config<validator_type> const & config)
320 {
321 verify_positional_option_config(config);
322
323 if constexpr (detail::is_container_option<option_type>)
324 has_positional_list_option = true; // keep track of a list option because there must be only one!
325
326 // copy variables into the lambda because the calls are pushed to a stack
327 // and the references would go out of scope.
328 std::visit(
329 [&value, &config](auto & f)
330 {
331 f.add_positional_option(value, config);
332 },
333 format);
334 }
336
404 void parse()
405 {
406 if (parse_was_called)
407 throw design_error("The function parse() must only be called once!");
408
409 detail::version_checker app_version{info.app_name, info.version, info.url};
410
411 if (std::holds_alternative<detail::format_parse>(format) && !subcommands.empty() && sub_parser == nullptr)
412 {
413 assert(!subcommands.empty());
414 std::string subcommands_str{"["};
415 for (std::string const & command : subcommands)
416 subcommands_str += command + ", ";
417 subcommands_str.replace(subcommands_str.size() - 2, 2, "]"); // replace last ", " by "]"
418
419 throw too_few_arguments{"You either forgot or misspelled the subcommand! Please specify which sub-program "
420 "you want to use: one of "
421 + subcommands_str
422 + ". Use -h/--help for more "
423 "information."};
424 }
425
426 if (app_version.decide_if_check_is_performed(version_check_dev_decision, version_check_user_decision))
427 {
428 // must be done before calling parse on the format because this might std::exit
429 std::promise<bool> app_version_prom;
430 version_check_future = app_version_prom.get_future();
431 app_version(std::move(app_version_prom));
432 }
433
434 std::visit(
435 [this](auto & f)
436 {
437 f.parse(info);
438 },
439 format);
440 parse_was_called = true;
441 }
442
450 {
451 if (sub_parser == nullptr)
452 {
453 throw design_error("No subcommand was provided at the construction of the argument parser!");
454 }
455
456 return *sub_parser;
457 }
458
488 // clang-format off
489 template <typename id_type>
490 requires std::same_as<id_type, char> || std::constructible_from<std::string, id_type>
491 bool is_option_set(id_type const & id) const
492 // clang-format on
493 {
494 if (!parse_was_called)
495 throw design_error{"You can only ask which options have been set after calling the function `parse()`."};
496
497 // the detail::format_parse::find_option_id call in the end expects either a char or std::string
498 using char_or_string_t = std::conditional_t<std::same_as<id_type, char>, char, std::string>;
499 char_or_string_t short_or_long_id = {id}; // e.g. convert char * to string here if necessary
500
501 if constexpr (!std::same_as<id_type, char>) // long id was given
502 {
503 if (short_or_long_id.size() == 1)
504 {
505 throw design_error{"Long option identifiers must be longer than one character! If " + short_or_long_id
506 + "' was meant to be a short identifier, please pass it as a char ('') not a string"
507 " (\"\")!"};
508 }
509 }
510
511 if (std::find(used_option_ids.begin(), used_option_ids.end(), std::string{id}) == used_option_ids.end())
512 throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
513
514 // we only need to search for an option before the `end_of_options_indentifier` (`--`)
515 auto end_of_options = std::find(cmd_arguments.begin(), cmd_arguments.end(), end_of_options_indentifier);
516 auto option_it = detail::format_parse::find_option_id(cmd_arguments.begin(), end_of_options, short_or_long_id);
517 return option_it != end_of_options;
518 }
519
522
532 void add_section(std::string const & title, bool const advanced_only = false)
533 {
534 std::visit(
535 [&title, advanced_only](auto & f)
536 {
537 f.add_section(title, advanced_only);
538 },
539 format);
540 }
541
551 void add_subsection(std::string const & title, bool const advanced_only = false)
552 {
553 std::visit(
554 [&title, advanced_only](auto & f)
555 {
556 f.add_subsection(title, advanced_only);
557 },
558 format);
559 }
560
571 void add_line(std::string const & text, bool is_paragraph = false, bool const advanced_only = false)
572 {
573 std::visit(
574 [&text, is_paragraph, advanced_only](auto & f)
575 {
576 f.add_line(text, is_paragraph, advanced_only);
577 },
578 format);
579 }
580
600 void add_list_item(std::string const & key, std::string const & desc, bool const advanced_only = false)
601 {
602 std::visit(
603 [&key, &desc, advanced_only](auto & f)
604 {
605 f.add_list_item(key, desc, advanced_only);
606 },
607 format);
608 }
610
662
663private:
665 bool parse_was_called{false};
666
668 bool has_positional_list_option{false};
669
671 update_notifications version_check_dev_decision{};
672
674 std::optional<bool> version_check_user_decision;
675
677 friend struct ::sharg::detail::test_accessor;
678
680 std::future<bool> version_check_future;
681
683 std::regex app_name_regex{"^[a-zA-Z0-9_-]+$"};
684
686 static constexpr std::string_view const end_of_options_indentifier{"--"};
687
689 std::unique_ptr<parser> sub_parser{nullptr};
690
692 std::vector<std::string> subcommands{};
693
701 std::variant<detail::format_parse,
702 detail::format_help,
703 detail::format_short_help,
704 detail::format_version,
705 detail::format_html,
706 detail::format_man,
707 detail::format_copyright/*,
708 detail::format_ctd*/> format{detail::format_help{{}, false}}; // Will be overwritten in any case.
709
711 std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
712
714 std::vector<std::string> cmd_arguments{};
715
748 void init(int argc, char const * const * const argv)
749 {
750 bool special_format_was_set{false};
751
752 for (int i = 1, argv_len = argc; i < argv_len; ++i) // start at 1 to skip binary name
753 {
754 std::string arg{argv[i]};
755
756 if (std::find(subcommands.begin(), subcommands.end(), arg) != subcommands.end())
757 {
758 sub_parser =
759 std::make_unique<parser>(info.app_name + "-" + arg, argc - i, argv + i, update_notifications::off);
760 break;
761 }
762
763 if (arg == "-h" || arg == "--help")
764 {
765 format = detail::format_help{subcommands, false};
766 init_standard_options();
767 special_format_was_set = true;
768 }
769 else if (arg == "-hh" || arg == "--advanced-help")
770 {
771 format = detail::format_help{subcommands, true};
772 init_standard_options();
773 special_format_was_set = true;
774 }
775 else if (arg == "--version")
776 {
777 format = detail::format_version{};
778 special_format_was_set = true;
779 }
780 else if (arg.substr(0, 13) == "--export-help") // --export-help=man is also allowed
781 {
782 std::string export_format;
783
784 if (arg.size() > 13)
785 {
786 export_format = arg.substr(14);
787 }
788 else
789 {
790 if (argv_len <= i + 1)
791 throw too_few_arguments{"Option --export-help must be followed by a value."};
792 export_format = {argv[i + 1]};
793 }
794
795 if (export_format == "html")
796 format = detail::format_html{subcommands};
797 else if (export_format == "man")
798 format = detail::format_man{subcommands};
799 // TODO (smehringer) use when CTD support is available
800 // else if (export_format == "ctd")
801 // format = detail::format_ctd{};
802 else
803 throw validation_error{"Validation failed for option --export-help: "
804 "Value must be one of [html, man]"};
805 init_standard_options();
806 special_format_was_set = true;
807 }
808 else if (arg == "--copyright")
809 {
810 format = detail::format_copyright{};
811 special_format_was_set = true;
812 }
813 else if (arg == "--version-check")
814 {
815 if (++i >= argv_len)
816 throw too_few_arguments{"Option --version-check must be followed by a value."};
817
818 arg = argv[i];
819
820 if (arg == "1" || arg == "true")
821 version_check_user_decision = true;
822 else if (arg == "0" || arg == "false")
823 version_check_user_decision = false;
824 else
825 throw validation_error{"Value for option --version-check must be true (1) or false (0)."};
826
827 // in case --version-check is specified it shall not be passed to format_parse()
828 argc -= 2;
829 }
830 else
831 {
832 cmd_arguments.push_back(std::move(arg));
833 }
834 }
835
836 // all special options have been identified, which might involve deleting them from argv (e.g. version-check)
837 // check if no actual options remain and then call the short help page.
838 if (argc <= 1) // no arguments provided
839 {
840 format = detail::format_short_help{};
841 return;
842 }
843
844 if (!special_format_was_set)
845 format = detail::format_parse(argc, cmd_arguments);
846 }
847
849 void init_standard_options()
850 {
851 add_subsection("Basic options:");
852 add_list_item("\\fB-h\\fP, \\fB--help\\fP", "Prints the help page.");
853 add_list_item("\\fB-hh\\fP, \\fB--advanced-help\\fP", "Prints the help page including advanced options.");
854 add_list_item("\\fB--version\\fP", "Prints the version information.");
855 add_list_item("\\fB--copyright\\fP", "Prints the copyright/license information.");
856 add_list_item("\\fB--export-help\\fP (std::string)",
857 "Export the help page information. Value must be one of [html, man].");
858 if (version_check_dev_decision == update_notifications::on)
859 add_list_item("\\fB--version-check\\fP (bool)",
860 "Whether to check for the newest app version. Default: true.");
861 }
862
868 template <typename id_type>
869 bool id_exists(id_type const & id)
870 {
871 if (detail::format_parse::is_empty_id(id))
872 return false;
873 return (!(used_option_ids.insert(std::string({id}))).second);
874 }
875
885 void verify_identifiers(char const short_id, std::string const & long_id)
886 {
887 constexpr std::string_view valid_chars{"@_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"};
888 auto is_valid = [&valid_chars](char const c)
889 {
890 return valid_chars.find(c) != std::string::npos;
891 };
892
893 if (id_exists(short_id))
894 throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
895 if (id_exists(long_id))
896 throw design_error("Option Identifier '" + long_id + "' was already used before.");
897 if (long_id.length() == 1)
898 throw design_error("Long IDs must be either empty, or longer than one character.");
899 if ((short_id != '\0') && !is_valid(short_id))
900 throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
901 if (long_id.size() > 0 && (long_id[0] == '-'))
902 throw design_error("First character of long ID cannot be '-'.");
903
904 std::for_each(long_id.begin(),
905 long_id.end(),
906 [&is_valid](char c)
907 {
908 if (!((c == '-') || is_valid(c)))
909 throw design_error(
910 "Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
911 });
912 if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
913 throw design_error("Option Identifiers cannot both be empty.");
914 }
915
917 void verify_option_config(config<auto> const & config)
918 {
919 if (sub_parser != nullptr)
920 throw design_error{"You may only specify flags for the top-level parser."};
921
922 verify_identifiers(config.short_id, config.long_id);
923
924 if (config.required && !config.default_message.empty())
925 throw design_error{"A required option cannot have a default message."};
926 }
927
929 void verify_flag_config(config<auto> const & config)
930 {
931 verify_identifiers(config.short_id, config.long_id);
932
933 if (!config.default_message.empty())
934 throw design_error{"A flag may not have a default message because the default is always `false`."};
935 }
936
938 void verify_positional_option_config(config<auto> const & config) const
939 {
940 if (config.short_id != '\0' || config.long_id != "")
941 throw design_error{"Positional options are identified by their position on the command line. "
942 "Short or long ids are not permitted!"};
943
944 if (config.advanced || config.hidden)
945 throw design_error{"Positional options are always required and therefore cannot be advanced nor hidden!"};
946
947 if (sub_parser != nullptr)
948 throw design_error{"You may only specify flags for the top-level parser."};
949
950 if (has_positional_list_option)
951 throw design_error{"You added a positional option with a list value before so you cannot add "
952 "any other positional options."};
953
954 if (!config.default_message.empty())
955 throw design_error{"A positional option may not have a default message because it is always required."};
956 }
957};
958
959} // 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:156
void add_option(option_type &value, config< validator_type > const &config)
Adds an option to the sharg::parser.
Definition: parser.hpp:251
void add_flag(bool &value, config< validator_type > const &config)
Adds a flag to the sharg::parser.
Definition: parser.hpp:277
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:551
parser & operator=(parser const &)=default
Defaulted.
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:491
void add_positional_option(option_type &value, config< validator_type > const &config)
Adds a positional option to the sharg::parser.
Definition: parser.hpp:319
parser_meta_data info
Aggregates all parser related meta data (see sharg::parser_meta_data struct).
Definition: parser.hpp:661
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:186
parser(parser &&)=default
Defaulted.
void parse()
Initiates the actual command line parsing.
Definition: parser.hpp:404
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:600
parser & operator=(parser &&)=default
Defaulted.
parser(parser const &)=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:215
parser & get_sub_parser()
Returns a reference to the sub-parser instance if subcommand parsing was enabled.
Definition: parser.hpp:449
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:571
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 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)
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 push_back(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)