Skip to content

Commit e58a7f4

Browse files
committed
[FIX] is_option_set: match both long and short ids
1 parent 39f65a4 commit e58a7f4

File tree

4 files changed

+180
-54
lines changed

4 files changed

+180
-54
lines changed

include/sharg/detail/format_parse.hpp

Lines changed: 28 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,33 @@ 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.short_id.empty()};
177+
bool const long_id_empty{id.long_id.empty()};
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+
auto cmp = [&](std::string const & current_arg)
183+
{
184+
// check if current_arg starts with "-o", i.e. it correctly identifies all short notations:
185+
// "-ovalue", "-o=value", and "-o value".
186+
bool const short_id_found = !short_id_empty && current_arg.starts_with("-")
187+
&& current_arg.substr(1, id.short_id.size()) == id.short_id;
188+
189+
// only "--opt Value" or "--opt=Value" are valid
190+
bool const long_id_found = !short_id_found // Don't need to check long_id if short_id was found
191+
&& !long_id_empty && current_arg.starts_with("--")
192+
&& current_arg.substr(2, id.long_id.size()) == id.long_id // prefix is the same
193+
&& (current_arg.size() == id.long_id.size() + 2
194+
|| current_arg[id.long_id.size() + 2] == '='); // space or `=`
195+
196+
return short_id_found || long_id_found;
197+
};
198+
199+
return std::ranges::find_if(begin_it, end_it, cmp);
198200
}
199201

200202
private:

include/sharg/detail/id_pair.hpp

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 <string>
13+
#include <unordered_set>
14+
15+
//!\cond
16+
17+
namespace sharg::detail
18+
{
19+
20+
/*!\brief A simple struct to store a short and a long identifier for an option.
21+
* \ingroup parser
22+
*/
23+
struct id_pair
24+
{
25+
std::string short_id{}; //!< The short identifier for the option.
26+
std::string long_id{}; //!< The long identifier for the option.
27+
28+
id_pair() = default;
29+
id_pair(id_pair const &) = default;
30+
id_pair(id_pair &&) = default;
31+
id_pair & operator=(id_pair const &) = default;
32+
id_pair & operator=(id_pair &&) = default;
33+
~id_pair() = default;
34+
35+
id_pair(std::string id_short, std::string id_long) : short_id{std::move(id_short)}, long_id{std::move(id_long)}
36+
{}
37+
38+
id_pair(char const id_short, std::string id_long) :
39+
short_id{std::string(id_short != '\0', id_short)},
40+
long_id{std::move(id_long)}
41+
{}
42+
43+
id_pair(char const id_short) : short_id{std::string(id_short != '\0', id_short)}
44+
{}
45+
46+
id_pair(std::string id_long) : long_id{std::move(id_long)}
47+
{}
48+
49+
//!\brief Compares two id_pairs for equality.
50+
friend auto operator<=>(id_pair const &, id_pair const &) = default;
51+
52+
static auto find(std::unordered_set<id_pair> const & used_ids, char const short_id);
53+
54+
static auto find(std::unordered_set<id_pair> const & used_ids, std::string const & long_id);
55+
56+
static bool contains(std::unordered_set<id_pair> const & used_ids, char const short_id);
57+
58+
static bool contains(std::unordered_set<id_pair> const & used_ids, std::string const & long_id);
59+
};
60+
61+
} // namespace sharg::detail
62+
63+
namespace std
64+
{
65+
66+
template <>
67+
struct hash<sharg::detail::id_pair>
68+
{
69+
size_t operator()(sharg::detail::id_pair const & value) const noexcept
70+
{
71+
size_t h1 = std::hash<std::string>{}(value.short_id);
72+
size_t h2 = std::hash<std::string>{}(value.long_id);
73+
return h1 ^ (h2 << 1);
74+
}
75+
};
76+
77+
} // namespace std
78+
79+
namespace sharg::detail
80+
{
81+
82+
inline auto id_pair::find(std::unordered_set<id_pair> const & used_ids, char const short_id)
83+
{
84+
std::string const id_str(1, short_id);
85+
auto it = std::ranges::find_if(used_ids,
86+
[&id_str](id_pair const & id)
87+
{
88+
return id.short_id == id_str;
89+
});
90+
return it;
91+
}
92+
93+
inline auto id_pair::find(std::unordered_set<id_pair> const & used_ids, std::string const & long_id)
94+
{
95+
auto it = std::ranges::find_if(used_ids,
96+
[&long_id](id_pair const & id)
97+
{
98+
return id.long_id == long_id;
99+
});
100+
return it;
101+
}
102+
103+
inline bool id_pair::contains(std::unordered_set<id_pair> const & used_ids, char const short_id)
104+
{
105+
return id_pair::find(used_ids, short_id) != used_ids.end();
106+
}
107+
108+
inline bool id_pair::contains(std::unordered_set<id_pair> const & used_ids, std::string const & long_id)
109+
{
110+
return id_pair::find(used_ids, long_id) != used_ids.end();
111+
}
112+
113+
} // namespace sharg::detail
114+
115+
//!\endcond

include/sharg/parser.hpp

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -495,11 +495,13 @@ 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+
constexpr bool id_is_long = !std::same_as<id_type, char>;
499+
498500
// 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
501+
using char_or_string_t = std::conditional_t<id_is_long, std::string, char>;
502+
char_or_string_t const short_or_long_id{id}; // e.g. convert char * to string here if necessary
501503

502-
if constexpr (!std::same_as<id_type, char>) // long id was given
504+
if constexpr (id_is_long) // long id was given
503505
{
504506
if (short_or_long_id.size() == 1)
505507
{
@@ -509,13 +511,18 @@ class parser
509511
}
510512
}
511513

512-
if (!used_ids.contains(std::string{id}))
513-
throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
514+
detail::id_pair const & ids = [this, &short_or_long_id]()
515+
{
516+
auto it = detail::id_pair::find(used_option_ids, short_or_long_id);
517+
if (it == used_option_ids.end())
518+
throw design_error{"You can only ask for option identifiers that you added with add_option() before."};
519+
return *it;
520+
}();
514521

515522
// 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);
518-
return option_it != option_end;
523+
auto end_of_options = std::find(format_arguments.begin(), format_arguments.end(), option_end_identifier);
524+
auto option_it = detail::format_parse::find_option_id(format_arguments.begin(), end_of_options, ids);
525+
return option_it != end_of_options;
519526
}
520527

521528
//!\name Structuring the Help Page
@@ -741,8 +748,12 @@ class parser
741748
detail::format_copyright>
742749
format{detail::format_short_help{}};
743750

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"};
751+
//!\brief List of option/flag identifiers that are already used.
752+
std::unordered_set<detail::id_pair> used_option_ids{{"h", "help"},
753+
{"hh", "advanced-help"},
754+
{"", "export-help"},
755+
{"", "version"},
756+
{"", "copyright"}};
746757

747758
//!\brief The command line arguments that will be passed to the format.
748759
std::vector<std::string> format_arguments{};
@@ -944,19 +955,6 @@ class parser
944955
format = detail::format_parse(format_arguments);
945956
}
946957

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-
960958
/*!\brief Verifies that the short and the long identifiers are correctly formatted.
961959
* \param[in] short_id The short identifier of the command line option/flag.
962960
* \param[in] long_id The long identifier of the command line option/flag.
@@ -981,7 +979,7 @@ class parser
981979
{
982980
if (short_id == '-' || !is_valid(short_id))
983981
throw design_error{"Short identifiers may only contain alphanumeric characters, '_', or '@'."};
984-
if (id_exists(short_id))
982+
if (detail::id_pair::contains(used_option_ids, short_id))
985983
throw design_error{"Short identifier '" + std::string(1, short_id) + "' was already used before."};
986984
}
987985

@@ -993,9 +991,11 @@ class parser
993991
throw design_error{"Long identifiers may not use '-' as first character."};
994992
if (!std::ranges::all_of(long_id, is_valid))
995993
throw design_error{"Long identifiers may only contain alphanumeric characters, '_', '-', or '@'."};
996-
if (id_exists(long_id))
994+
if (detail::id_pair::contains(used_option_ids, long_id))
997995
throw design_error{"Long identifier '" + long_id + "' was already used before."};
998996
}
997+
998+
used_option_ids.emplace(short_id, long_id);
999999
}
10001000

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

test/unit/parser/format_parse_test.cpp

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -732,21 +732,30 @@ TEST_F(format_parse_test, issue1544)
732732
TEST_F(format_parse_test, is_option_set)
733733
{
734734
std::string option_value{};
735+
std::vector<std::string> positional_options{};
735736

736-
auto parser = get_parser("-l", "hallo", "--foobar", "ballo", "--");
737+
auto parser = get_parser("-l", "hallo", "--foobar", "ballo", "--", "--loo", "--bar");
738+
// `--` signals the end of options!
739+
parser.add_positional_option(positional_options, sharg::config{});
740+
parser.add_option(option_value, sharg::config{.short_id = 'b', .long_id = "bar"});
737741
parser.add_option(option_value, sharg::config{.short_id = 'l', .long_id = "loo"});
738742
parser.add_option(option_value, sharg::config{.short_id = 'f', .long_id = "foobar"});
739743

740744
// you cannot call option_is_set before parse()
741745
EXPECT_THROW(parser.is_option_set("foo"), sharg::design_error);
742746

743747
EXPECT_NO_THROW(parser.parse());
748+
EXPECT_EQ(positional_options.size(), 2u);
749+
EXPECT_EQ(positional_options[0], "--loo");
750+
EXPECT_EQ(positional_options[1], "--bar");
744751

745752
EXPECT_TRUE(parser.is_option_set('l'));
753+
EXPECT_TRUE(parser.is_option_set("loo")); // --loo is behind the `--`, but -l is before
754+
EXPECT_TRUE(parser.is_option_set('f'));
746755
EXPECT_TRUE(parser.is_option_set("foobar"));
747756

748-
EXPECT_FALSE(parser.is_option_set('f'));
749-
EXPECT_FALSE(parser.is_option_set("loo"));
757+
EXPECT_FALSE(parser.is_option_set('b'));
758+
EXPECT_FALSE(parser.is_option_set("bar")); // --bar is behind the `--`
750759

751760
// errors:
752761
auto expect_design_error = [&](auto && option)

0 commit comments

Comments
 (0)