Skip to content

Commit 1e9e8a4

Browse files
committed
[FIX] is_option_set: match both long and short ids
1 parent fc521fe commit 1e9e8a4

File tree

4 files changed

+128
-57
lines changed

4 files changed

+128
-57
lines changed

include/sharg/config.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414
namespace sharg
1515
{
1616

17+
/*!\brief A simple struct to store a short and a long identifier for an option.
18+
* \ingroup parser
19+
*/
20+
struct id_pair
21+
{
22+
std::string short_id; //!< The short identifier for the option.
23+
std::string long_id; //!< The long identifier for the option.
24+
25+
//!\brief Compares two id_pairs for equality.
26+
friend auto operator<=>(id_pair const &, id_pair const &) = default;
27+
};
28+
1729
/*!\brief Option struct that is passed to the `sharg::parser::add_option()` function.
1830
* \ingroup parser
1931
*

include/sharg/detail/format_parse.hpp

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,10 @@ class format_parse : public format_base
145145
template <typename id_type>
146146
static bool is_empty_id(id_type const & id)
147147
{
148-
if constexpr (std::same_as<std::remove_cvref_t<id_type>, std::string>)
149-
return id.empty();
150-
else // char
148+
if constexpr (std::same_as<id_type, char>)
151149
return id == '\0';
150+
else
151+
return id.empty();
152152
}
153153

154154
/*!\brief Finds the position of a short/long identifier in format_parse::argv.
@@ -157,7 +157,7 @@ class format_parse : public format_base
157157
* std::string if it denotes a long identifier.
158158
* \param[in] begin_it The iterator where to start the search of the identifier.
159159
* \param[in] end_it The iterator one past the end of where to search the identifier.
160-
* \param[in] id The identifier to search for (must not contain dashes).
160+
* \param[in] ids The identifier to search for (must not contain dashes).
161161
* \returns An iterator pointing to the first occurrence of `id` in the list pointed to by `begin_it`
162162
* or `end_it` if it is not contained.
163163
*
@@ -172,32 +172,47 @@ class format_parse : public format_base
172172
* The `id` is found by comparing every argument in argv to `id` prepended with two dashes (`--`)
173173
* or a prefix of such followed by the equal sign `=`.
174174
*/
175-
template <typename iterator_type, typename id_type>
176-
static iterator_type find_option_id(iterator_type begin_it, iterator_type end_it, id_type const & id)
175+
template <typename iterator_type>
176+
static iterator_type find_option_id(iterator_type begin_it, iterator_type end_it, id_pair const & ids)
177177
{
178-
if (is_empty_id(id))
178+
bool short_id_empty{ids.short_id.empty()};
179+
bool long_id_empty{ids.long_id.empty()};
180+
181+
if (short_id_empty && long_id_empty)
179182
return end_it;
180183

181-
return (std::find_if(begin_it,
182-
end_it,
183-
[&](std::string const & current_arg)
184-
{
185-
std::string full_id = prepend_dash(id);
186-
187-
if constexpr (std::same_as<id_type, char>) // short id
188-
{
189-
// check if current_arg starts with "-o", i.e. it correctly identifies all short notations:
190-
// "-ovalue", "-o=value", and "-o value".
191-
return current_arg.substr(0, full_id.size()) == full_id;
192-
}
193-
else
194-
{
195-
// only "--opt Value" or "--opt=Value" are valid
196-
return current_arg.substr(0, full_id.size()) == full_id && // prefix is the same
197-
(current_arg.size() == full_id.size()
198-
|| current_arg[full_id.size()] == '='); // space or `=`
199-
}
200-
}));
184+
auto cmp = [&](std::string const & current_arg)
185+
{
186+
// check if current_arg starts with "-o", i.e. it correctly identifies all short notations:
187+
// "-ovalue", "-o=value", and "-o value".
188+
bool const short_id_found = !short_id_empty && current_arg.starts_with("-")
189+
&& current_arg.substr(1, ids.short_id.size()) == ids.short_id;
190+
191+
// only "--opt Value" or "--opt=Value" are valid
192+
bool const long_id_found = !short_id_found // Don't need to check long_id if short_id was found
193+
&& !long_id_empty && current_arg.starts_with("--")
194+
&& current_arg.substr(2, ids.long_id.size()) == ids.long_id // prefix is the same
195+
&& (current_arg.size() == ids.long_id.size() + 2
196+
|| current_arg[ids.long_id.size() + 2] == '='); // space or `=`
197+
198+
return short_id_found || long_id_found;
199+
};
200+
201+
return std::find_if(begin_it, end_it, cmp);
202+
}
203+
204+
//!\overload
205+
template <typename iterator_type>
206+
static iterator_type find_option_id(iterator_type begin_it, iterator_type end_it, char const id)
207+
{
208+
return find_option_id(std::move(begin_it), std::move(end_it), id_pair{std::string(1, id), ""});
209+
}
210+
211+
//!\overload
212+
template <typename iterator_type>
213+
static iterator_type find_option_id(iterator_type begin_it, iterator_type end_it, std::string const & id)
214+
{
215+
return find_option_id(std::move(begin_it), std::move(end_it), id_pair{"", id});
201216
}
202217

203218
private:

include/sharg/parser.hpp

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -500,11 +500,13 @@ class parser
500500
if (!parse_was_called)
501501
throw design_error{"You can only ask which options have been set after calling the function `parse()`."};
502502

503+
constexpr bool id_is_long = !std::same_as<id_type, char>;
504+
503505
// the detail::format_parse::find_option_id call in the end expects either a char or std::string
504-
using char_or_string_t = std::conditional_t<std::same_as<id_type, char>, char, std::string>;
505-
char_or_string_t short_or_long_id = {id}; // e.g. convert char * to string here if necessary
506+
using char_or_string_t = std::conditional_t<id_is_long, std::string, char>;
507+
char_or_string_t const short_or_long_id{id}; // e.g. convert char * to string here if necessary
506508

507-
if constexpr (!std::same_as<id_type, char>) // long id was given
509+
if constexpr (id_is_long) // long id was given
508510
{
509511
if (short_or_long_id.size() == 1)
510512
{
@@ -514,12 +516,17 @@ class parser
514516
}
515517
}
516518

517-
if (std::find(used_option_ids.begin(), used_option_ids.end(), std::string{id}) == used_option_ids.end())
518-
throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
519+
id_pair const & ids = [this, &short_or_long_id]()
520+
{
521+
auto it = find_used_id(short_or_long_id);
522+
if (!it)
523+
throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
524+
return *it;
525+
}();
519526

520527
// we only need to search for an option before the `end_of_options_indentifier` (`--`)
521528
auto end_of_options = std::find(cmd_arguments.begin(), cmd_arguments.end(), end_of_options_indentifier);
522-
auto option_it = detail::format_parse::find_option_id(cmd_arguments.begin(), end_of_options, short_or_long_id);
529+
auto option_it = detail::format_parse::find_option_id(cmd_arguments.begin(), end_of_options, ids);
523530
return option_it != end_of_options;
524531
}
525532

@@ -715,7 +722,11 @@ class parser
715722
format{detail::format_help{{}, {}, false}}; // Will be overwritten in any case.
716723

717724
//!\brief List of option/flag identifiers that are already used.
718-
std::set<std::string> used_option_ids{"h", "hh", "help", "advanced-help", "export-help", "version", "copyright"};
725+
std::set<id_pair> used_option_ids{{"h", "help"},
726+
{"hh", "advanced-help"},
727+
{"", "export-help"},
728+
{"", "version"},
729+
{"", "copyright"}};
719730

720731
//!\brief The command line arguments.
721732
std::vector<std::string> cmd_arguments{};
@@ -884,11 +895,34 @@ class parser
884895
* otherwise.
885896
*/
886897
template <typename id_type>
887-
bool id_exists(id_type const & id)
898+
std::optional<id_pair> find_used_id(id_type const & id) const
888899
{
889900
if (detail::format_parse::is_empty_id(id))
890-
return false;
891-
return (!(used_option_ids.insert(std::string({id}))).second);
901+
return std::nullopt;
902+
903+
if constexpr (std::same_as<id_type, char>)
904+
{
905+
std::string const id_str{std::string(1, id)};
906+
auto it = std::ranges::find_if(used_option_ids,
907+
[&id_str](id_pair const & element)
908+
{
909+
return element.short_id == id_str;
910+
});
911+
if (it != used_option_ids.end())
912+
return *it;
913+
}
914+
else
915+
{
916+
auto it = std::ranges::find_if(used_option_ids,
917+
[&id](id_pair const & element)
918+
{
919+
return element.long_id == id;
920+
});
921+
if (it != used_option_ids.end())
922+
return *it;
923+
}
924+
925+
return std::nullopt;
892926
}
893927

894928
/*!\brief Verifies that the short and the long identifiers are correctly formatted.
@@ -908,27 +942,33 @@ class parser
908942
return valid_chars.find(c) != std::string::npos;
909943
};
910944

911-
if (id_exists(short_id))
912-
throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
913-
if (id_exists(long_id))
914-
throw design_error("Option Identifier '" + long_id + "' was already used before.");
945+
bool const short_id_empty = detail::format_parse::is_empty_id(short_id);
946+
bool const long_id_empty = detail::format_parse::is_empty_id(long_id);
947+
bool const long_id_valid = std::ranges::all_of(long_id,
948+
[&is_valid](char c)
949+
{
950+
return ((c == '-') || is_valid(c));
951+
});
952+
953+
// Validity
954+
if (short_id_empty && long_id_empty)
955+
throw design_error("Option Identifiers cannot both be empty.");
956+
if (!short_id_empty && !is_valid(short_id))
957+
throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
915958
if (long_id.length() == 1)
916959
throw design_error("Long IDs must be either empty, or longer than one character.");
917-
if ((short_id != '\0') && !is_valid(short_id))
918-
throw design_error("Option identifiers may only contain alphanumeric characters, '_', or '@'.");
919-
if (long_id.size() > 0 && (long_id[0] == '-'))
960+
if (!long_id_empty && (long_id[0] == '-'))
920961
throw design_error("First character of long ID cannot be '-'.");
962+
if (!long_id_empty && !long_id_valid)
963+
throw design_error("Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
921964

922-
std::for_each(long_id.begin(),
923-
long_id.end(),
924-
[&is_valid](char c)
925-
{
926-
if (!((c == '-') || is_valid(c)))
927-
throw design_error(
928-
"Long identifiers may only contain alphanumeric characters, '_', '-', or '@'.");
929-
});
930-
if (detail::format_parse::is_empty_id(short_id) && detail::format_parse::is_empty_id(long_id))
931-
throw design_error("Option Identifiers cannot both be empty.");
965+
// Availability
966+
if (find_used_id(short_id))
967+
throw design_error("Option Identifier '" + std::string(1, short_id) + "' was already used before.");
968+
if (find_used_id(long_id))
969+
throw design_error("Option Identifier '" + long_id + "' was already used before.");
970+
971+
used_option_ids.insert({std::string(1, short_id), long_id});
932972
}
933973

934974
//!brief Verify the configuration given to a sharg::parser::add_option call.

test/unit/parser/format_parse_test.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -960,8 +960,10 @@ TEST(parse_test, issue1544)
960960
TEST(parse_test, is_option_set)
961961
{
962962
std::string option_value{};
963-
char const * argv[] = {"./parser_test", "-l", "hallo", "--foobar", "ballo", "--", "--loo"};
963+
// `--` signals the end of options!
964+
char const * argv[] = {"./parser_test", "-l", "hallo", "--foobar", "ballo", "--", "--loo", "--bar"};
964965
sharg::parser parser{"test_parser", 5, argv, sharg::update_notifications::off};
966+
parser.add_option(option_value, sharg::config{.short_id = 'b', .long_id = "bar"});
965967
parser.add_option(option_value, sharg::config{.short_id = 'l', .long_id = "loo"});
966968
parser.add_option(option_value, sharg::config{.short_id = 'f', .long_id = "foobar"});
967969

@@ -970,10 +972,12 @@ TEST(parse_test, is_option_set)
970972
EXPECT_NO_THROW(parser.parse());
971973

972974
EXPECT_TRUE(parser.is_option_set('l'));
975+
EXPECT_TRUE(parser.is_option_set("loo")); // --loo is behind the `--`, but -l is before
976+
EXPECT_TRUE(parser.is_option_set('f'));
973977
EXPECT_TRUE(parser.is_option_set("foobar"));
974978

975-
EXPECT_FALSE(parser.is_option_set('f'));
976-
EXPECT_FALSE(parser.is_option_set("loo")); // --loo is behind the `--` which signals the end of options!
979+
EXPECT_FALSE(parser.is_option_set('b'));
980+
EXPECT_FALSE(parser.is_option_set("bar")); // --bar is behind the `--`
977981

978982
// errors:
979983
EXPECT_THROW(parser.is_option_set("l"), sharg::design_error); // short identifiers are passed as chars not strings

0 commit comments

Comments
 (0)