Skip to content

Commit 89ffa7b

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. Other change: - Unnamed arguments must be separated from a group of short options: the "abc" token in "-vabc" cannot be interpreted as an unnamed argument, unless -a, -b and -c options are defined this will now result in an 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 89ffa7b

4 files changed

Lines changed: 143 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: 44 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,18 @@ 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 (args_i != args_e
329+
&& !style.option_terminator.empty()
330+
&& *args_i == style.option_terminator)
331+
{
332+
had_terminator = true;
333+
++args_i;
334+
}
335+
}
298336
};
299337

300338
}} // 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)