99
1010#pragma once
1111
12- #include < set >
12+ #include < unordered_set >
1313#include < variant>
1414
1515#include < sharg/config.hpp>
@@ -426,9 +426,6 @@ class parser
426426 // Determine the format and subcommand.
427427 determine_format_and_subcommand ();
428428
429- // If a subcommand was provided, check that it is valid.
430- verify_subcommand ();
431-
432429 // Apply all defered operations to the parser, e.g., `add_option`, `add_flag`, `add_positional_option`.
433430 for (auto & operation : operations)
434431 operation ();
@@ -512,7 +509,7 @@ class parser
512509 }
513510 }
514511
515- if (std::find (used_option_ids. begin (), used_option_ids. end (), std::string{id}) == used_option_ids. end ( ))
512+ if (!used_ids. contains ( std::string{id}))
516513 throw design_error{" You can only ask for option identifiers that you added with add_option() before." };
517514
518515 // we only need to search for an option before the `option_end_identifier` (`--`)
@@ -744,8 +741,8 @@ class parser
744741 detail::format_copyright>
745742 format{detail::format_short_help{}};
746743
747- // !\brief List of option/flag identifiers that are already used.
748- std::set <std::string> used_option_ids {" h" , " hh" , " help" , " advanced-help" , " export-help" , " version" , " copyright" };
744+ // !\brief List of option/flag identifiers (excluding -/--) that are already used.
745+ std::unordered_set <std::string> used_ids {" h" , " hh" , " help" , " advanced-help" , " export-help" , " version" , " copyright" };
749746
750747 // !\brief The command line arguments that will be passed to the format.
751748 std::vector<std::string> format_arguments{};
@@ -756,14 +753,18 @@ class parser
756753 // !\brief The command that lead to calling this parser, e.g. [./build/bin/raptor, build]
757754 std::vector<std::string> executable_name{};
758755
756+ // !\brief Set of option identifiers (including -/--) that have been added via `add_option`.
757+ std::unordered_set<std::string> options{};
758+
759759 // !\brief Vector of functions that stores all calls.
760760 std::vector<std::function<void ()>> operations;
761761
762- /* !\brief Initializes the sharg::parser class on construction .
762+ /* !\brief Handles format and subcommand detection .
763763 * \throws sharg::too_few_arguments if option --export-help was specified without a value
764764 * \throws sharg::too_few_arguments if option --version-check was specified without a value
765765 * \throws sharg::validation_error if the value passed to option --export-help was invalid.
766766 * \throws sharg::validation_error if the value passed to option --version-check was invalid.
767+ * \throws sharg::user_input_error if the subcommand is unknown.
767768 * \details
768769 *
769770 * This function adds all command line parameters to the format_arguments member variable
@@ -788,23 +789,27 @@ class parser
788789 void determine_format_and_subcommand ()
789790 {
790791 assert (!arguments.empty ());
792+
791793 auto it = arguments.begin ();
794+ std::string_view arg{*it};
792795
793- executable_name.emplace_back (*it);
794- ++it;
796+ executable_name.emplace_back (arg);
795797
796- // Helper function for going to the next argument. This makes it more obvious that we are
798+ // Helper function for reading the next argument. This makes it more obvious that we are
797799 // incrementing `it` (version-check, and export-help).
798- auto go_to_next_arg = [this , &it](std::string_view message ) -> auto
800+ auto read_next_arg = [this , &it, &arg]( ) -> bool
799801 {
800802 assert (it != arguments.end ());
801803
802804 if (++it == arguments.end ())
803- throw too_few_arguments{message.data ()};
805+ return false ;
806+
807+ arg = *it;
808+ return true ;
804809 };
805810
806811 // Helper function for finding and processing subcommands.
807- auto found_and_processed_subcommand = [this , &it](std::string_view arg) -> bool
812+ auto found_subcommand = [this , &it, & arg]( ) -> bool
808813 {
809814 if (subcommands.empty ())
810815 return false ;
@@ -824,25 +829,46 @@ class parser
824829 }
825830 else
826831 {
827- // Positional options are forbidden by design. Todo: Allow options. Forbidden in check_option_config.
832+ // Positional options are forbidden by design.
828833 // Flags and options, which both start with '-', are allowed for the top-level parser.
829- // Otherwise, this is a wrongly spelled subcommand. The error will be thrown in parse() .
834+ // Otherwise, this is an unknown subcommand.
830835 if (!arg.starts_with (' -' ))
831836 {
832- format_arguments.emplace_back (arg);
833- return true ;
837+ std::string message = " You specified an unknown subcommand! Available subcommands are: [" ;
838+ for (std::string const & command : subcommands)
839+ message += command + " , " ;
840+ message.replace (message.size () - 2 , 2 , " ]. Use -h/--help for more information." );
841+
842+ throw user_input_error{message};
834843 }
835844 }
836845
837846 return false ;
838847 };
839848
840849 // Process the arguments.
841- for (; it != arguments. end (); ++it )
850+ for (; read_next_arg ();)
842851 {
843- std::string_view arg{*it};
852+ // The argument is a known option.
853+ if (options.contains (std::string{arg}))
854+ {
855+ // No futher checks are needed.
856+ format_arguments.emplace_back (arg);
857+
858+ // Consume the next argument (the option value) if possible.
859+ if (read_next_arg ())
860+ {
861+ format_arguments.emplace_back (arg);
862+ continue ;
863+ }
864+ else // Too few arguments. This is handled by format_parse.
865+ {
866+ break ;
867+ }
868+ }
844869
845- if (found_and_processed_subcommand (arg))
870+ // If we have a subcommand, all further arguments are passed to the subparser.
871+ if (found_subcommand ())
846872 break ;
847873
848874 if (arg == " -h" || arg == " --help" )
@@ -868,8 +894,8 @@ class parser
868894 // --export-help man
869895 if (arg.empty ())
870896 {
871- go_to_next_arg ( " Option --export-help must be followed by a value. " );
872- arg = *it ;
897+ if (! read_next_arg ())
898+ throw too_few_arguments{ " Option --export-help must be followed by a value. " } ;
873899 }
874900 else // --export-help=man
875901 {
@@ -891,8 +917,8 @@ class parser
891917 }
892918 else if (arg == " --version-check" )
893919 {
894- go_to_next_arg ( " Option --version-check must be followed by a value. " );
895- arg = *it ;
920+ if (! read_next_arg ())
921+ throw too_few_arguments{ " Option --version-check must be followed by a value. " } ;
896922
897923 if (arg == " 1" || arg == " true" )
898924 version_check_user_decision = true ;
@@ -903,6 +929,7 @@ class parser
903929 }
904930 else
905931 {
932+ // Flags, positional options, options using an alternative syntax (--optionValue, --option=value), etc.
906933 format_arguments.emplace_back (arg);
907934 }
908935 }
@@ -927,7 +954,7 @@ class parser
927954 {
928955 if (detail::format_parse::is_empty_id (id))
929956 return false ;
930- return (!(used_option_ids .insert (std::string ({id}))).second );
957+ return (!(used_ids .insert (std::string ({id}))).second );
931958 }
932959
933960 /* !\brief Verifies that the short and the long identifiers are correctly formatted.
@@ -941,44 +968,47 @@ class parser
941968 */
942969 void verify_identifiers (char const short_id, std::string const & long_id)
943970 {
944- constexpr std::string_view valid_chars{" @_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" };
945- auto is_valid = [&valid_chars](char const c)
971+ auto is_valid = [](char const c) -> bool
946972 {
947- return valid_chars.find (c) != std::string::npos;
973+ return (c >= ' a' && c <= ' z' ) || (c >= ' A' && c <= ' Z' ) || (c >= ' 0' && c <= ' 9' ) // alphanumeric
974+ || c == ' @' || c == ' _' || c == ' -' ; // additional characters
948975 };
949976
950- if (id_exists (short_id))
951- throw design_error (" Option Identifier '" + std::string (1 , short_id) + " ' was already used before." );
952- if (id_exists (long_id))
953- throw design_error (" Option Identifier '" + long_id + " ' was already used before." );
954- if (long_id.length () == 1 )
955- throw design_error (" Long IDs must be either empty, or longer than one character." );
956- if ((short_id != ' \0 ' ) && !is_valid (short_id))
957- throw design_error (" Option identifiers may only contain alphanumeric characters, '_', or '@'." );
958- if (long_id.size () > 0 && (long_id[0 ] == ' -' ))
959- throw design_error (" First character of long ID cannot be '-'." );
960-
961- std::for_each (long_id.begin (),
962- long_id.end (),
963- [&is_valid](char c)
964- {
965- if (!((c == ' -' ) || is_valid (c)))
966- throw design_error (
967- " Long identifiers may only contain alphanumeric characters, '_', '-', or '@'." );
968- });
969- if (detail::format_parse::is_empty_id (short_id) && detail::format_parse::is_empty_id (long_id))
970- throw design_error (" Option Identifiers cannot both be empty." );
977+ if (short_id == ' \0 ' && long_id.empty ())
978+ throw design_error{" Short and long identifiers may not both be empty." };
979+
980+ if (short_id != ' \0 ' )
981+ {
982+ if (short_id == ' -' || !is_valid (short_id))
983+ throw design_error{" Short identifiers may only contain alphanumeric characters, '_', or '@'." };
984+ if (id_exists (short_id))
985+ throw design_error{" Short identifier '" + std::string (1 , short_id) + " ' was already used before." };
986+ }
987+
988+ if (!long_id.empty ())
989+ {
990+ if (long_id.size () == 1 )
991+ throw design_error{" Long identifiers must be either empty or longer than one character." };
992+ if (long_id[0 ] == ' -' )
993+ throw design_error{" Long identifiers may not use '-' as first character." };
994+ if (!std::ranges::all_of (long_id, is_valid))
995+ throw design_error{" Long identifiers may only contain alphanumeric characters, '_', '-', or '@'." };
996+ if (id_exists (long_id))
997+ throw design_error{" Long identifier '" + long_id + " ' was already used before." };
998+ }
971999 }
9721000
9731001 // !brief Verify the configuration given to a sharg::parser::add_option call.
9741002 template <typename validator_t >
9751003 void verify_option_config (config<validator_t > const & config)
9761004 {
977- if (!subcommands.empty ())
978- throw design_error{" You may only specify flags for the top-level parser." };
979-
9801005 verify_identifiers (config.short_id , config.long_id );
9811006
1007+ if (config.short_id != ' \0 ' )
1008+ options.emplace (std::string{" -" } + config.short_id );
1009+ if (!config.long_id .empty ())
1010+ options.emplace (std::string{" --" } + config.long_id );
1011+
9821012 if (config.required && !config.default_message .empty ())
9831013 throw design_error{" A required option cannot have a default message." };
9841014 }
@@ -1005,7 +1035,7 @@ class parser
10051035 throw design_error{" Positional options are always required and therefore cannot be advanced nor hidden!" };
10061036
10071037 if (!subcommands.empty ())
1008- throw design_error{" You may only specify flags for the top-level parser." };
1038+ throw design_error{" You may only specify flags and options for the top-level parser." };
10091039
10101040 if (has_positional_list_option)
10111041 throw design_error{" You added a positional option with a list value before so you cannot add "
@@ -1075,27 +1105,6 @@ class parser
10751105 }
10761106 }
10771107
1078- /* !\brief Verifies that the subcommand was correctly specified.
1079- * \throws sharg::too_few_arguments if a subparser was configured at construction but a subcommand is missing.
1080- */
1081- inline void verify_subcommand ()
1082- {
1083- if (std::holds_alternative<detail::format_parse>(format) && !subcommands.empty () && sub_parser == nullptr )
1084- {
1085- assert (!subcommands.empty ());
1086- std::string subcommands_str{" [" };
1087- for (std::string const & command : subcommands)
1088- subcommands_str += command + " , " ;
1089- subcommands_str.replace (subcommands_str.size () - 2 , 2 , " ]" ); // replace last ", " by "]"
1090-
1091- throw too_few_arguments{" You misspelled the subcommand! Please specify which sub-program "
1092- " you want to use: one of "
1093- + subcommands_str
1094- + " . Use -h/--help for more "
1095- " information." };
1096- }
1097- }
1098-
10991108 /* !\brief Parses the command line arguments according to the format.
11001109 * \throws sharg::option_declared_multiple_times if an option that is not a list was declared multiple times.
11011110 * \throws sharg::user_input_error if an incorrect argument is given as (positional) option value.
0 commit comments