Skip to content

Commit dac147c

Browse files
author
Roeland Schoukens
committed
Allow specifying an option terminator ("--" on POSIX)
Add an option terminator to option styles. If not empty, this changes the parsing in the following ways: - Initially, arguments are not allowed to start with an option prefix; - unknown options will trigger a parse error; - after the specified terminator is encountered, all remaining tokens will be plain arguments; - the parser still tolerates "--" prefixes as option values. It has this in common with Python's argparse, but POSIX parsers tend to also error out with a "no option value provided" error. Having this terminator is not entirely backwards compatible, in particular it becomes cumbersome to pass in negative numbers as arguments. This is a known problem for POSIX style parsers. This commit therefore defines a new style for POSIX with terminator.
1 parent 56998c3 commit dac147c

4 files changed

Lines changed: 142 additions & 7 deletions

File tree

include/lyra/arg.hpp

100644100755
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ class arg : public bound_parser<arg>
8080

8181
auto const & token = tokens.argument();
8282

83+
// argument not allowed at this point
84+
if (token.type == detail::token_type::unknown)
85+
{
86+
// Nothing to match against.
87+
return parse_result::ok(
88+
detail::parse_state(parser_result_type::no_match, tokens));
89+
}
90+
8391
auto valueRef = static_cast<detail::BoundValueRefBase *>(m_ref.get());
8492

8593
if (value_choices)

include/lyra/detail/tokens.hpp

100644100755
Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,11 @@ class token_iterator
124124
: style(opt_style)
125125
, args_i(args.begin())
126126
, args_e(args.end())
127-
, args_i_sub(opt_style.short_option_size)
128-
{}
127+
, args_i_sub(0)
128+
, had_terminator(false)
129+
{
130+
handle_next_arg_i();
131+
}
129132

130133
explicit operator bool() const noexcept { return args_i != args_e; }
131134

@@ -138,14 +141,14 @@ class token_iterator
138141
if (++args_i_sub >= args_i->size())
139142
{
140143
++args_i;
141-
args_i_sub = style.short_option_size;
144+
handle_next_arg_i();
142145
}
143146
}
144147
else
145148
{
146149
// Regular arg or long option, just advance to the next arg.
147150
++args_i;
148-
args_i_sub = style.short_option_size;
151+
handle_next_arg_i();
149152
}
150153
return *this;
151154
}
@@ -158,7 +161,7 @@ class token_iterator
158161
args_i += 2;
159162
else
160163
++args_i;
161-
args_i_sub = style.short_option_size;
164+
handle_next_arg_i();
162165
return *this;
163166
}
164167

@@ -195,6 +198,10 @@ class token_iterator
195198
// Extract the current option token.
196199
token option() const
197200
{
201+
if (had_terminator)
202+
{
203+
return token();
204+
}
198205
if (has_long_option_prefix())
199206
{
200207
if (has_value_delimiter())
@@ -249,7 +256,25 @@ class token_iterator
249256
return token();
250257
}
251258

252-
token argument() const { return token(token_type::argument, *args_i); }
259+
token argument() const
260+
{
261+
if (args_i_sub != style.short_option_size)
262+
{
263+
// can't have value argument in the middle of a string of
264+
// short options. Still return the argument string so it
265+
// shows up in error messages.
266+
return token(token_type::unknown, *args_i);
267+
}
268+
if (!style.option_terminator.empty()
269+
&& has_option_prefix() && !had_terminator)
270+
{
271+
// can't have arguments starting with option prefix unless
272+
// we had an explicit terminator for arguments. Still return
273+
// the argument string so it shows up in error messages.
274+
return token(token_type::unknown, *args_i);
275+
}
276+
return token(token_type::argument, *args_i);
277+
}
253278

254279
static bool is_prefixed(
255280
const std::string & prefix, std::size_t size, const std::string & s)
@@ -272,6 +297,7 @@ class token_iterator
272297
std::vector<std::string>::const_iterator args_i;
273298
std::vector<std::string>::const_iterator args_e;
274299
std::string::size_type args_i_sub;
300+
bool had_terminator;
275301

276302
inline bool is_opt_prefix(char c) const noexcept
277303
{
@@ -295,6 +321,17 @@ class token_iterator
295321
return std::string(
296322
static_cast<typename std::string::size_type>(size), prefix[0]);
297323
}
324+
325+
inline void handle_next_arg_i()
326+
{
327+
args_i_sub = style.short_option_size;
328+
if (!style.option_terminator.empty()
329+
&& *args_i == style.option_terminator)
330+
{
331+
had_terminator = true;
332+
++args_i;
333+
}
334+
}
298335
};
299336

300337
}} // namespace lyra::detail

include/lyra/option_style.hpp

100644100755
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ struct option_style
5959
std::size_t long_option_size = 0;
6060
std::string short_option_prefix;
6161
std::size_t short_option_size = 0;
62+
std::string option_terminator = "";
6263
opt_print_order options_print_order = opt_print_order::per_declaration;
6364
std::size_t indent_size = 2;
6465

@@ -70,12 +71,14 @@ struct option_style
7071
std::string && short_option_prefix_chars = {},
7172
std::size_t short_option_prefix_size = 0,
7273
opt_print_order options_print_order_ = opt_print_order::per_declaration,
73-
std::size_t indent_size_ = 2)
74+
std::size_t indent_size_ = 2,
75+
std::string && option_terminator_ = "")
7476
: value_delimiters(std::move(value_delimiters_chars))
7577
, long_option_prefix(std::move(long_option_prefix_chars))
7678
, long_option_size(long_option_prefix_size)
7779
, short_option_prefix(std::move(short_option_prefix_chars))
7880
, short_option_size(short_option_prefix_size)
81+
, option_terminator(std::move(option_terminator_))
7982
, options_print_order(options_print_order_)
8083
, indent_size(indent_size_)
8184
{}
@@ -87,6 +90,7 @@ struct option_style
8790

8891
// Styles..
8992

93+
static const option_style & posix_with_terminator();
9094
static const option_style & posix();
9195
static const option_style & posix_brief();
9296
static const option_style & windows();
@@ -189,6 +193,12 @@ These provide definitions for common syntax of option styles:
189193
190194
end::reference[] */
191195

196+
inline const option_style & option_style::posix_with_terminator()
197+
{
198+
static const option_style style("= ", "-", 2, "-", 1, opt_print_order::per_declaration, 2, "--");
199+
return style;
200+
}
201+
192202
inline const option_style & option_style::posix()
193203
{
194204
static const option_style style("= ", "-", 2, "-", 1);

tests/terminator_run_test.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#include "mini_test.hpp"
2+
#include <lyra/lyra.hpp>
3+
#include <iostream>
4+
5+
using ArgList = std::initializer_list<std::string>;
6+
7+
void test_terminator1(bfg::mini_test::scope& test, std::string test_value)
8+
{
9+
using namespace lyra;
10+
{
11+
std::string default_value = "default1";
12+
std::string arg_value_s = default_value;
13+
std::string opt_value_s = default_value;
14+
15+
auto cli = lyra::cli().style(lyra::option_style::posix_with_terminator())
16+
| arg(arg_value_s, "value")
17+
| opt(opt_value_s, "OPTION").name("--option");
18+
ArgList args = { "TestApp", "--", test_value.c_str() };
19+
auto result = cli.parse(args);
20+
if (!result) std::cerr << result.message() << '\n';
21+
test(REQUIRE(result));
22+
test(opt_value_s == default_value, "opt_value_s == " + default_value, CONTEXT);
23+
test(arg_value_s == test_value, "arg_value_s == " + test_value, CONTEXT);
24+
}
25+
}
26+
27+
28+
void test_terminator_no_terminator(bfg::mini_test::scope& test, std::string test_value)
29+
{
30+
using namespace lyra;
31+
{
32+
std::string default_value = "default1";
33+
std::string arg_value_s = default_value;
34+
std::string opt_value_s = default_value;
35+
36+
auto cli = lyra::cli().style(lyra::option_style::posix_with_terminator())
37+
| arg(arg_value_s, "value")
38+
| opt(opt_value_s, "OPTION").name("--option");
39+
ArgList args = { "TestApp", "--option", test_value.c_str() };
40+
auto result = cli.parse(args);
41+
if (!result) std::cerr << result.message() << '\n';
42+
test(REQUIRE(result));
43+
test(opt_value_s == test_value, "opt_value_s == " + test_value, CONTEXT);
44+
test(arg_value_s == default_value, "arg_value_s == " + default_value, CONTEXT);
45+
}
46+
}
47+
48+
49+
void test_terminator_unknown_option(bfg::mini_test::scope& test)
50+
{
51+
using namespace lyra;
52+
{
53+
std::string default_value = "default1";
54+
std::string arg_value_s = default_value;
55+
std::string opt_value_s = default_value;
56+
57+
auto cli = lyra::cli().style(lyra::option_style::posix_with_terminator())
58+
| arg(arg_value_s, "value")
59+
| opt(opt_value_s, "OPTION").name("--option");
60+
ArgList args = { "TestApp", "--unknown" };
61+
auto result = cli.parse(args);
62+
test(REQUIRE(!result));
63+
}
64+
}
65+
66+
67+
int main()
68+
{
69+
using namespace lyra;
70+
bfg::mini_test::scope test;
71+
72+
test_terminator1(test, "test1");
73+
test_terminator1(test, "--test1");
74+
test_terminator1(test, "--option");
75+
test_terminator_no_terminator(test, "test1");
76+
test_terminator_no_terminator(test, "--test1"); // Q: should this also be an error?
77+
test_terminator_unknown_option(test); // unknown option must raise an error
78+
79+
return test;
80+
}

0 commit comments

Comments
 (0)