1010#pragma once
1111
1212#include < set>
13+ #include < unordered_set>
1314#include < variant>
1415
1516#include < sharg/config.hpp>
@@ -426,9 +427,6 @@ class parser
426427 // Determine the format and subcommand.
427428 determine_format_and_subcommand ();
428429
429- // If a subcommand was provided, check that it is valid.
430- verify_subcommand ();
431-
432430 // Apply all defered operations to the parser, e.g., `add_option`, `add_flag`, `add_positional_option`.
433431 for (auto & operation : operations)
434432 operation ();
@@ -512,7 +510,7 @@ class parser
512510 }
513511 }
514512
515- if (std::find (used_option_ids. begin (), used_option_ids. end (), std::string{id}) == used_option_ids. end ( ))
513+ if (!used_ids. contains ( std::string{id}))
516514 throw design_error{" You can only ask for option identifiers that you added with add_option() before." };
517515
518516 // we only need to search for an option before the `option_end_identifier` (`--`)
@@ -744,8 +742,8 @@ class parser
744742 detail::format_copyright>
745743 format{detail::format_short_help{}};
746744
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" };
745+ // !\brief List of option/flag identifiers (excluding -/--) that are already used.
746+ std::unordered_set <std::string> used_ids {" h" , " hh" , " help" , " advanced-help" , " export-help" , " version" , " copyright" };
749747
750748 // !\brief The command line arguments that will be passed to the format.
751749 std::vector<std::string> format_arguments{};
@@ -756,14 +754,18 @@ class parser
756754 // !\brief The command that lead to calling this parser, e.g. [./build/bin/raptor, build]
757755 std::vector<std::string> executable_name{};
758756
757+ // !\brief Set of option identifiers (including -/--) that have been added via `add_option`.
758+ std::unordered_set<std::string> options{};
759+
759760 // !\brief Vector of functions that stores all calls.
760761 std::vector<std::function<void ()>> operations;
761762
762- /* !\brief Initializes the sharg::parser class on construction .
763+ /* !\brief Handles format and subcommand detection .
763764 * \throws sharg::too_few_arguments if option --export-help was specified without a value
764765 * \throws sharg::too_few_arguments if option --version-check was specified without a value
765766 * \throws sharg::validation_error if the value passed to option --export-help was invalid.
766767 * \throws sharg::validation_error if the value passed to option --version-check was invalid.
768+ * \throws sharg::user_input_error if the subcommand is unknown.
767769 * \details
768770 *
769771 * This function adds all command line parameters to the format_arguments member variable
@@ -788,23 +790,27 @@ class parser
788790 void determine_format_and_subcommand ()
789791 {
790792 assert (!arguments.empty ());
793+
791794 auto it = arguments.begin ();
795+ std::string_view arg{*it};
792796
793- executable_name.emplace_back (*it);
794- ++it;
797+ executable_name.emplace_back (arg);
795798
796- // Helper function for going to the next argument. This makes it more obvious that we are
799+ // Helper function for reading the next argument. This makes it more obvious that we are
797800 // incrementing `it` (version-check, and export-help).
798- auto go_to_next_arg = [this , &it](std::string_view message ) -> auto
801+ auto read_next_arg = [this , &it, &arg]( ) -> bool
799802 {
800803 assert (it != arguments.end ());
801804
802805 if (++it == arguments.end ())
803- throw too_few_arguments{message.data ()};
806+ return false ;
807+
808+ arg = *it;
809+ return true ;
804810 };
805811
806812 // Helper function for finding and processing subcommands.
807- auto found_and_processed_subcommand = [this , &it](std::string_view arg) -> bool
813+ auto found_subcommand = [this , &it, & arg]( ) -> bool
808814 {
809815 if (subcommands.empty ())
810816 return false ;
@@ -824,25 +830,46 @@ class parser
824830 }
825831 else
826832 {
827- // Positional options are forbidden by design. Todo: Allow options. Forbidden in check_option_config.
833+ // Positional options are forbidden by design.
828834 // 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() .
835+ // Otherwise, this is an unknown subcommand.
830836 if (!arg.starts_with (' -' ))
831837 {
832- format_arguments.emplace_back (arg);
833- return true ;
838+ std::string message = " You specified an unknown subcommand! Available subcommands are: [" ;
839+ for (std::string const & command : subcommands)
840+ message += command + " , " ;
841+ message.replace (message.size () - 2 , 2 , " ]. Use -h/--help for more information." );
842+
843+ throw user_input_error{message};
834844 }
835845 }
836846
837847 return false ;
838848 };
839849
840850 // Process the arguments.
841- for (; it != arguments. end (); ++it )
851+ for (; read_next_arg ();)
842852 {
843- std::string_view arg{*it};
853+ // The argument is a known option.
854+ if (options.contains (std::string{arg}))
855+ {
856+ // No futher checks are needed.
857+ format_arguments.emplace_back (arg);
858+
859+ // Consume the next argument (the option value) if possible.
860+ if (read_next_arg ())
861+ {
862+ format_arguments.emplace_back (arg);
863+ continue ;
864+ }
865+ else // Too few arguments. This is handled by format_parse.
866+ {
867+ break ;
868+ }
869+ }
844870
845- if (found_and_processed_subcommand (arg))
871+ // If we have a subcommand, all further arguments are passed to the subparser.
872+ if (found_subcommand ())
846873 break ;
847874
848875 if (arg == " -h" || arg == " --help" )
@@ -868,8 +895,8 @@ class parser
868895 // --export-help man
869896 if (arg.empty ())
870897 {
871- go_to_next_arg ( " Option --export-help must be followed by a value. " );
872- arg = *it ;
898+ if (! read_next_arg ())
899+ throw too_few_arguments{ " Option --export-help must be followed by a value. " } ;
873900 }
874901 else // --export-help=man
875902 {
@@ -891,8 +918,8 @@ class parser
891918 }
892919 else if (arg == " --version-check" )
893920 {
894- go_to_next_arg ( " Option --version-check must be followed by a value. " );
895- arg = *it ;
921+ if (! read_next_arg ())
922+ throw too_few_arguments{ " Option --version-check must be followed by a value. " } ;
896923
897924 if (arg == " 1" || arg == " true" )
898925 version_check_user_decision = true ;
@@ -903,6 +930,7 @@ class parser
903930 }
904931 else
905932 {
933+ // Flags, positional options, options using an alternative syntax (--optionValue, --option=value), etc.
906934 format_arguments.emplace_back (arg);
907935 }
908936 }
@@ -927,7 +955,7 @@ class parser
927955 {
928956 if (detail::format_parse::is_empty_id (id))
929957 return false ;
930- return (!(used_option_ids .insert (std::string ({id}))).second );
958+ return (!(used_ids .insert (std::string ({id}))).second );
931959 }
932960
933961 /* !\brief Verifies that the short and the long identifiers are correctly formatted.
@@ -941,44 +969,47 @@ class parser
941969 */
942970 void verify_identifiers (char const short_id, std::string const & long_id)
943971 {
944- constexpr std::string_view valid_chars{" @_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" };
945- auto is_valid = [&valid_chars](char const c)
972+ auto is_valid = [](char const c) -> bool
946973 {
947- return valid_chars.find (c) != std::string::npos;
974+ return (c >= ' a' && c <= ' z' ) || (c >= ' A' && c <= ' Z' ) || (c >= ' 0' && c <= ' 9' ) // alphanumeric
975+ || c == ' @' || c == ' _' || c == ' -' ; // additional characters
948976 };
949977
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." );
978+ if (short_id == ' \0 ' && long_id.empty ())
979+ throw design_error{" Short and long identifiers may not both be empty." };
980+
981+ if (short_id != ' \0 ' )
982+ {
983+ if (short_id == ' -' || !is_valid (short_id))
984+ throw design_error{" Short identifiers may only contain alphanumeric characters, '_', or '@'." };
985+ if (id_exists (short_id))
986+ throw design_error{" Short identifier '" + std::string (1 , short_id) + " ' was already used before." };
987+ }
988+
989+ if (!long_id.empty ())
990+ {
991+ if (long_id.size () == 1 )
992+ throw design_error{" Long identifiers must be either empty or longer than one character." };
993+ if (long_id[0 ] == ' -' )
994+ throw design_error{" Long identifiers may not use '-' as first character." };
995+ if (!std::ranges::all_of (long_id, is_valid))
996+ throw design_error{" Long identifiers may only contain alphanumeric characters, '_', '-', or '@'." };
997+ if (id_exists (long_id))
998+ throw design_error{" Long identifier '" + long_id + " ' was already used before." };
999+ }
9711000 }
9721001
9731002 // !brief Verify the configuration given to a sharg::parser::add_option call.
9741003 template <typename validator_t >
9751004 void verify_option_config (config<validator_t > const & config)
9761005 {
977- if (!subcommands.empty ())
978- throw design_error{" You may only specify flags for the top-level parser." };
979-
9801006 verify_identifiers (config.short_id , config.long_id );
9811007
1008+ if (config.short_id != ' \0 ' )
1009+ options.emplace (std::string{" -" } + config.short_id );
1010+ if (!config.long_id .empty ())
1011+ options.emplace (std::string{" --" } + config.long_id );
1012+
9821013 if (config.required && !config.default_message .empty ())
9831014 throw design_error{" A required option cannot have a default message." };
9841015 }
@@ -1005,7 +1036,7 @@ class parser
10051036 throw design_error{" Positional options are always required and therefore cannot be advanced nor hidden!" };
10061037
10071038 if (!subcommands.empty ())
1008- throw design_error{" You may only specify flags for the top-level parser." };
1039+ throw design_error{" You may only specify flags and options for the top-level parser." };
10091040
10101041 if (has_positional_list_option)
10111042 throw design_error{" You added a positional option with a list value before so you cannot add "
@@ -1075,27 +1106,6 @@ class parser
10751106 }
10761107 }
10771108
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-
10991109 /* !\brief Parses the command line arguments according to the format.
11001110 * \throws sharg::option_declared_multiple_times if an option that is not a list was declared multiple times.
11011111 * \throws sharg::user_input_error if an incorrect argument is given as (positional) option value.
0 commit comments