Skip to content

Commit a035bed

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

File tree

1 file changed

+84
-74
lines changed

1 file changed

+84
-74
lines changed

include/sharg/parser.hpp

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

Comments
 (0)