Skip to content

Commit c1d0aea

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

File tree

5 files changed

+244
-73
lines changed

5 files changed

+244
-73
lines changed

include/sharg/detail/format_parse.hpp

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <sharg/concept.hpp>
1515
#include <sharg/detail/format_base.hpp>
16+
#include <sharg/detail/id_pair.hpp>
1617

1718
namespace sharg::detail
1819
{
@@ -142,10 +143,10 @@ class format_parse : public format_base
142143
template <typename id_type>
143144
static bool is_empty_id(id_type const & id)
144145
{
145-
if constexpr (std::same_as<std::remove_cvref_t<id_type>, std::string>)
146-
return id.empty();
147-
else // char
146+
if constexpr (std::same_as<id_type, char>)
148147
return id == '\0';
148+
else
149+
return id.empty();
149150
}
150151

151152
/*!\brief Finds the position of a short/long identifier in format_parse::arguments.
@@ -169,32 +170,39 @@ class format_parse : public format_base
169170
* The `id` is found by comparing every argument in arguments to `id` prepended with two dashes (`--`)
170171
* or a prefix of such followed by the equal sign `=`.
171172
*/
172-
template <typename iterator_type, typename id_type>
173-
static iterator_type find_option_id(iterator_type begin_it, iterator_type end_it, id_type const & id)
173+
template <typename iterator_type>
174+
static iterator_type find_option_id(iterator_type begin_it, iterator_type end_it, detail::id_pair const & id)
174175
{
175-
if (is_empty_id(id))
176+
bool const short_id_empty{id.empty_short_id()};
177+
bool const long_id_empty{id.empty_long_id()};
178+
179+
if (short_id_empty && long_id_empty)
176180
return end_it;
177181

178-
return (std::find_if(begin_it,
179-
end_it,
180-
[&](std::string const & current_arg)
181-
{
182-
std::string full_id = prepend_dash(id);
183-
184-
if constexpr (std::same_as<id_type, char>) // short id
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-
return current_arg.substr(0, full_id.size()) == full_id;
189-
}
190-
else
191-
{
192-
// only "--opt Value" or "--opt=Value" are valid
193-
return current_arg.substr(0, full_id.size()) == full_id && // prefix is the same
194-
(current_arg.size() == full_id.size()
195-
|| current_arg[full_id.size()] == '='); // space or `=`
196-
}
197-
}));
182+
std::string const short_id = prepend_dash(id.short_id);
183+
std::string const long_id_equals = prepend_dash(id.long_id) + "=";
184+
std::string_view const long_id_space = [&long_id_equals]()
185+
{
186+
std::string_view tmp{long_id_equals};
187+
tmp.remove_suffix(1u);
188+
return tmp;
189+
}();
190+
191+
auto cmp = [&](std::string_view const current_arg)
192+
{
193+
// check if current_arg starts with "-o", i.e. it correctly identifies all short notations:
194+
// "-ovalue", "-o=value", and "-o value".
195+
if (!short_id_empty && current_arg.starts_with(short_id))
196+
return true;
197+
198+
// only "--opt Value" or "--opt=Value" are valid
199+
if (!long_id_empty && (current_arg == long_id_space || current_arg.starts_with(long_id_equals)))
200+
return true;
201+
202+
return false;
203+
};
204+
205+
return std::find_if(begin_it, end_it, cmp);
198206
}
199207

200208
private:

include/sharg/detail/id_pair.hpp

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// SPDX-FileCopyrightText: 2006-2024, Knut Reinert & Freie Universität Berlin
2+
// SPDX-FileCopyrightText: 2016-2024, Knut Reinert & MPI für molekulare Genetik
3+
// SPDX-License-Identifier: BSD-3-Clause
4+
5+
/*!\file
6+
* \author Enrico Seiler <enrico.seiler AT fu-berlin.de>
7+
* \brief Provides sharg::detail::id_pair.
8+
*/
9+
10+
#pragma once
11+
12+
#include <algorithm>
13+
#include <string>
14+
#include <unordered_set>
15+
16+
#include <sharg/platform.hpp>
17+
18+
namespace sharg::detail
19+
{
20+
21+
/*!\brief A simple struct to store a short and a long identifier for an option.
22+
* \ingroup parser
23+
*/
24+
struct id_pair
25+
{
26+
char short_id{}; //!< The short identifier for the option.
27+
std::string long_id{}; //!< The long identifier for the option.
28+
29+
id_pair() = default; //!< Defaulted.
30+
id_pair(id_pair const &) = default; //!< Defaulted.
31+
id_pair & operator=(id_pair const &) = default; //!< Defaulted.
32+
id_pair(id_pair &&) = default; //!< Defaulted.
33+
id_pair & operator=(id_pair &&) = default; //!< Defaulted.
34+
~id_pair() = default; //!< Defaulted.
35+
36+
//!\brief Construct an id_pair from a short ID.
37+
id_pair(char const short_id) : short_id{short_id}
38+
{}
39+
40+
//!\brief Construct an id_pair from a long ID.
41+
id_pair(std::string long_id) : long_id{std::move(long_id)}
42+
{}
43+
44+
//!\brief Construct an id_pair from a short and long ID.
45+
id_pair(char const short_id, std::string long_id) : short_id{short_id}, long_id{std::move(long_id)}
46+
{}
47+
48+
/*!\brief Two id_pairs are equal if their short **or** long ID is equal.
49+
* Empty IDs are not considered for equality. If both IDs are empty, the id_pairs are considered **not** equal.
50+
*/
51+
friend bool operator==(id_pair const & lhs, id_pair const & rhs)
52+
{
53+
return (!lhs.empty_short_id() && lhs.short_id == rhs.short_id)
54+
|| (!lhs.empty_long_id() && lhs.long_id == rhs.long_id);
55+
}
56+
57+
/*!\brief Compares the given short ID with the short ID of the id_pair.
58+
* Returns false if the id_pair's short ID is empty.
59+
*/
60+
friend bool operator==(id_pair const & lhs, char const & rhs)
61+
{
62+
return !lhs.empty_short_id() && lhs.short_id == rhs;
63+
}
64+
65+
/*!\brief Compares the given long ID with the long ID of the id_pair.
66+
* Returns false if the id_pair's long ID is empty.
67+
*/
68+
friend bool operator==(id_pair const & lhs, std::string const & rhs)
69+
{
70+
return !lhs.empty_long_id() && lhs.long_id == rhs;
71+
}
72+
73+
//!\brief Returns true if the short ID is empty.
74+
bool empty_short_id() const noexcept
75+
{
76+
return empty(short_id);
77+
}
78+
79+
//!\brief Returns true if the long ID is empty.
80+
bool empty_long_id() const noexcept
81+
{
82+
return empty(long_id);
83+
}
84+
85+
//!\brief Returns true if both IDs are empty.
86+
bool empty() const noexcept
87+
{
88+
return empty_short_id() && empty_long_id();
89+
}
90+
91+
//!\brief Checks whether id is empty.
92+
template <typename id_type>
93+
static bool empty(id_type const & id) noexcept;
94+
95+
// Note: The following two functions are declared, but not defined.
96+
// We first need to specialise std::hash<id_pair> in the std namespace.
97+
// After that, we can define the functions.
98+
// Defining them here would generate std::hash<id_pair> before the specialisation.
99+
// 1.) Now there are two specialisations for std::hash<id_pair> (error)
100+
// 2.) The default-generated std::hash<id_pair> does actually not work
101+
102+
//!\brief Finds an id_pair in a set of used ids.
103+
template <typename id_type>
104+
static auto find(std::unordered_set<id_pair> const & used_ids, id_type const & id);
105+
106+
//!\brief Checks whether an id is already contained in a set of used ids.
107+
template <typename id_type>
108+
static bool contains(std::unordered_set<id_pair> const & used_ids, id_type const & id);
109+
};
110+
111+
} // namespace sharg::detail
112+
113+
namespace std
114+
{
115+
116+
/*!\brief Hash specialization for sharg::detail::id_pair.
117+
* \ingroup parser
118+
*/
119+
template <>
120+
struct hash<sharg::detail::id_pair>
121+
{
122+
//!\brief Computes the hash value for a given id_pair.
123+
size_t operator()(sharg::detail::id_pair const & value) const noexcept
124+
{
125+
size_t const h1 = std::hash<char>{}(value.short_id);
126+
size_t const h2 = std::hash<std::string>{}(value.long_id);
127+
return h1 ^ (h2 << 1);
128+
}
129+
};
130+
131+
} // namespace std
132+
133+
namespace sharg::detail
134+
{
135+
136+
template <typename id_type>
137+
inline bool id_pair::empty(id_type const & id) noexcept
138+
{
139+
if constexpr (std::same_as<id_type, id_pair>)
140+
return id.empty();
141+
else if constexpr (std::same_as<id_type, char>)
142+
return id == '\0';
143+
else
144+
return id.empty();
145+
}
146+
147+
template <typename id_type>
148+
inline auto id_pair::find(std::unordered_set<id_pair> const & used_ids, id_type const & id)
149+
{
150+
if (empty(id))
151+
return used_ids.end();
152+
153+
return std::ranges::find_if(used_ids,
154+
[&id](id_pair const & pair)
155+
{
156+
return pair == id;
157+
});
158+
}
159+
160+
template <typename id_type>
161+
inline bool id_pair::contains(std::unordered_set<id_pair> const & used_ids, id_type const & id)
162+
{
163+
return find(used_ids, id) != used_ids.end();
164+
}
165+
166+
} // namespace sharg::detail

include/sharg/parser.hpp

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -495,26 +495,22 @@ class parser
495495
if (!parse_was_called)
496496
throw design_error{"You can only ask which options have been set after calling the function `parse()`."};
497497

498-
// the detail::format_parse::find_option_id call in the end expects either a char or std::string
499-
using char_or_string_t = std::conditional_t<std::same_as<id_type, char>, char, std::string>;
500-
char_or_string_t short_or_long_id = {id}; // e.g. convert char * to string here if necessary
498+
detail::id_pair const id_pair{id};
501499

502-
if constexpr (!std::same_as<id_type, char>) // long id was given
500+
if (id_pair.long_id.size() == 1u)
503501
{
504-
if (short_or_long_id.size() == 1)
505-
{
506-
throw design_error{"Long option identifiers must be longer than one character! If " + short_or_long_id
507-
+ "' was meant to be a short identifier, please pass it as a char ('') not a string"
508-
" (\"\")!"};
509-
}
502+
throw design_error{"Long option identifiers must be longer than one character! If " + id_pair.long_id
503+
+ "' was meant to be a short identifier, please pass it as a char ('') not a string"
504+
" (\"\")!"};
510505
}
511506

512-
if (!used_ids.contains(std::string{id}))
507+
auto const it = detail::id_pair::find(used_option_ids, id_pair);
508+
if (it == used_option_ids.end())
513509
throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
514510

515511
// we only need to search for an option before the `option_end_identifier` (`--`)
516-
auto option_end = std::find(format_arguments.begin(), format_arguments.end(), option_end_identifier);
517-
auto option_it = detail::format_parse::find_option_id(format_arguments.begin(), option_end, short_or_long_id);
512+
auto option_end = std::ranges::find(format_arguments, option_end_identifier);
513+
auto option_it = detail::format_parse::find_option_id(format_arguments.begin(), option_end, *it);
518514
return option_it != option_end;
519515
}
520516

@@ -741,8 +737,13 @@ class parser
741737
detail::format_copyright>
742738
format{detail::format_short_help{}};
743739

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"};
740+
//!\brief List of option/flag identifiers that are already used.
741+
std::unordered_set<detail::id_pair> used_option_ids{{'h', "help"},
742+
{'\0' /*hh*/, "advanced-help"},
743+
{'\0', "hh"},
744+
{'\0', "export-help"},
745+
{'\0', "version"},
746+
{'\0', "copyright"}};
746747

747748
//!\brief The command line arguments that will be passed to the format.
748749
std::vector<std::string> format_arguments{};
@@ -944,19 +945,6 @@ class parser
944945
format = detail::format_parse(format_arguments);
945946
}
946947

947-
/*!\brief Checks whether the long identifier has already been used before.
948-
* \param[in] id The long identifier of the command line option/flag.
949-
* \returns `true` if an option or flag with the long identifier exists or `false`
950-
* otherwise.
951-
*/
952-
template <typename id_type>
953-
bool id_exists(id_type const & id)
954-
{
955-
if (detail::format_parse::is_empty_id(id))
956-
return false;
957-
return (!(used_ids.insert(std::string({id}))).second);
958-
}
959-
960948
/*!\brief Verifies that the short and the long identifiers are correctly formatted.
961949
* \param[in] short_id The short identifier of the command line option/flag.
962950
* \param[in] long_id The long identifier of the command line option/flag.
@@ -981,7 +969,7 @@ class parser
981969
{
982970
if (short_id == '-' || !is_valid(short_id))
983971
throw design_error{"Short identifiers may only contain alphanumeric characters, '_', or '@'."};
984-
if (id_exists(short_id))
972+
if (detail::id_pair::contains(used_option_ids, short_id))
985973
throw design_error{"Short identifier '" + std::string(1, short_id) + "' was already used before."};
986974
}
987975

@@ -993,9 +981,11 @@ class parser
993981
throw design_error{"Long identifiers may not use '-' as first character."};
994982
if (!std::ranges::all_of(long_id, is_valid))
995983
throw design_error{"Long identifiers may only contain alphanumeric characters, '_', '-', or '@'."};
996-
if (id_exists(long_id))
984+
if (detail::id_pair::contains(used_option_ids, long_id))
997985
throw design_error{"Long identifier '" + long_id + "' was already used before."};
998986
}
987+
988+
used_option_ids.emplace(short_id, long_id);
999989
}
1000990

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

0 commit comments

Comments
 (0)