Skip to content

Commit 77fa6f2

Browse files
committed
[MISC] Allow options for subcommands
1 parent 01f848e commit 77fa6f2

File tree

1 file changed

+84
-75
lines changed

1 file changed

+84
-75
lines changed

include/sharg/parser.hpp

Lines changed: 84 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
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

Comments
 (0)