Skip to content

Commit 96b9349

Browse files
committed
[FIX] Prohibit long-option value pairs wihtout a separation.
For example, given the long option name "--opt", then "--optValue" was possible. This caused ambiguities if one long option name is the prefix of another. We now only allow "--opt Value" and "--opt=value".
1 parent dbe8fcb commit 96b9349

File tree

3 files changed

+78
-18
lines changed

3 files changed

+78
-18
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ Note that 3.1.0 will be the first API stable release and interfaces in this rele
7575

7676
## Notable Bug-fixes
7777

78+
### Argument Parser
79+
80+
* Long option identifiers and their value must be separated by a space or equal sign `=`.
81+
Handling this restriction resolves the ambiguity if one long option identifier is the prefix of
82+
another ([\#1792](https://github.com/seqan/seqan3/pull/1792)).
83+
84+
Valid short id value pairs: `-iValue`, `-i=Value`, `-i Value`
85+
Valid long id value pairs: `--id=Value`, `--id Value` (prohibited now: `--idValue`)
86+
7887
### I/O
7988

8089
* The `seqan3::field::cigar` was added to the default fields for reading and writing alignment files

include/seqan3/argument_parser/detail/format_parse.hpp

+26-8
Original file line numberDiff line numberDiff line change
@@ -202,16 +202,25 @@ class format_parse : public format_base
202202
}
203203

204204
/*!\brief Finds the position of a short/long identifier in format_parse::argv.
205-
*
205+
* \tparam id_type The identifier type; must be either of type `char` if it denotes a short identifier or
206+
* std::string if it denotes a long identifier.
206207
* \param[in] begin_it The iterator where to start the search of the identifier.
207208
* Note that the end iterator is kept as a member variable.
208209
* \param[in] id The identifier to search for (must not contain dashes).
209210
* \returns An iterator pointing to the first occurrence of the identifier in
210211
* the list pointed to by begin_t. If the list does not contain the
211212
* identifier `id`, the member variable `end_of_options_it` is returned.
212213
*
213-
* Note: The `id` is compared to the prefix of each value in the list, such
214-
* that "-idValue" arguments are correctly identified.
214+
* \details
215+
*
216+
* **Valid short-id value pairs are: `-iValue`, `-i=Value`, or `-i Value`**
217+
* If the `id` passed to this function is of type `char`, it is assumed to be a short identifier.
218+
* The `id` is found by comparing the prefix of every argument in argv to the `id` prepended with a single `-`.
219+
*
220+
* **Valid long id value pairs are: `--id=Value`, `--id Value`**.
221+
* If the `id` passed to this function is of type `std::string`, it is assumed to be a long identifier.
222+
* The `id` is found by comparing every argument in argv to `id` prepended with two dashes (`--`)
223+
* or a prefix of such followed by the equal sign `=`.
215224
*/
216225
template <typename id_type>
217226
std::vector<std::string>::iterator find_option_id(std::vector<std::string>::iterator const begin_it, id_type const & id)
@@ -220,13 +229,22 @@ class format_parse : public format_base
220229
return end_of_options_it;
221230

222231
return (std::find_if(begin_it, end_of_options_it,
223-
[&] (const std::string & v)
232+
[&] (std::string const & current_arg)
224233
{
225-
size_t id_size{(prepend_dash(id)).size()};
226-
if (v.size() < id_size)
227-
return false; // cannot be the correct identifier
234+
std::string full_id = prepend_dash(id);
228235

229-
return v.substr(0, id_size) == prepend_dash(id); // check if prefix of v is the same
236+
if constexpr (std::same_as<id_type, char>) // short id
237+
{
238+
// check if current_arg starts with "-o", i.e. it correctly identifies all short notations:
239+
// "-ovalue", "-o=value", and "-o value".
240+
return current_arg.substr(0, full_id.size()) == full_id;
241+
}
242+
else
243+
{
244+
// only "--opt Value" or "--opt=Value" are valid
245+
return current_arg.substr(0, full_id.size()) == full_id && // prefix is the same
246+
(current_arg.size() == full_id.size() || current_arg[full_id.size()] == '='); // space or `=`
247+
}
230248
}));
231249
}
232250

test/unit/argument_parser/format_parse_test.cpp

+43-10
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,6 @@ TEST(parse_type_test, add_option_long_id)
6060
EXPECT_TRUE((testing::internal::GetCapturedStderr()).empty());
6161
EXPECT_EQ(option_value, "option_string");
6262

63-
// add with no space
64-
const char * argv2[] = {"./argument_parser_test", "--string-optionoption_string"};
65-
seqan3::argument_parser parser2{"test_parser", 2, argv2, false};
66-
parser2.add_option(option_value, 'S', "string-option", "this is a string option.");
67-
68-
testing::internal::CaptureStderr();
69-
EXPECT_NO_THROW(parser2.parse());
70-
EXPECT_TRUE((testing::internal::GetCapturedStderr()).empty());
71-
EXPECT_EQ(option_value, "option_string");
72-
7363
const char * argv3[] = {"./argument_parser_test", "--string-option=option_string"};
7464
seqan3::argument_parser parser3{"test_parser", 2, argv3, false};
7565
parser3.add_option(option_value, 's', "string-option", "this is a string option.");
@@ -936,3 +926,46 @@ TEST(parse_test, subcommand_argument_parser_success)
936926
seqan3::argument_parser_error);
937927
}
938928
}
929+
930+
TEST(parse_test, issue1544)
931+
{
932+
{ // wrong separation of long value:
933+
std::string option_value;
934+
const char * argv[] = {"./argument_parser_test", "--foohallo"};
935+
seqan3::argument_parser parser{"test_parser", 2, argv, false};
936+
parser.add_option(option_value, 'f', "foo", "this is a string option.");
937+
938+
EXPECT_THROW(parser.parse(), seqan3::unknown_option);
939+
}
940+
941+
{ // unknown option (`--foo-bar`) that has a prefix of a known option (`--foo`)
942+
std::string option_value;
943+
const char * argv[] = {"./argument_parser_test", "--foo", "hallo", "--foo-bar", "ballo"};
944+
seqan3::argument_parser parser{"test_parser", 5, argv, false};
945+
parser.add_option(option_value, 'f', "foo", "this is a string option.");
946+
947+
EXPECT_THROW(parser.parse(), seqan3::unknown_option);
948+
}
949+
950+
{ // known option (`--foo-bar`) that has a prefix of a unknown option (`--foo`)
951+
std::string option_value;
952+
const char * argv[] = {"./argument_parser_test", "--foo", "hallo", "--foo-bar", "ballo"};
953+
seqan3::argument_parser parser{"test_parser", 5, argv, false};
954+
parser.add_option(option_value, 'f', "foo-bar", "this is a string option.");
955+
956+
EXPECT_THROW(parser.parse(), seqan3::unknown_option);
957+
}
958+
959+
{ // known option (`--foo`) is a prefix of another known option (`--foo-bar`)
960+
std::string foo_option_value;
961+
std::string foobar_option_value;
962+
const char * argv[] = {"./argument_parser_test", "--foo", "hallo", "--foo-bar", "ballo"};
963+
seqan3::argument_parser parser{"test_parser", 5, argv, false};
964+
parser.add_option(foo_option_value, 'f', "foo", "this is a prefix of foobar.");
965+
parser.add_option(foobar_option_value, 'b', "foo-bar", "this has prefix foo.");
966+
967+
EXPECT_NO_THROW(parser.parse());
968+
EXPECT_EQ(foo_option_value, "hallo");
969+
EXPECT_EQ(foobar_option_value, "ballo");
970+
}
971+
}

0 commit comments

Comments
 (0)