Skip to content

Commit 1c4a424

Browse files
committed
feat: report unknown arguments
1 parent 11691be commit 1c4a424

File tree

2 files changed

+89
-31
lines changed

2 files changed

+89
-31
lines changed

include/opzioni/exceptions.hpp

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,22 @@
1212

1313
namespace opz {
1414

15-
// +-----------------+
16-
// | consumer errors |
17-
// +-----------------+
15+
// +-------------------+
16+
// | programmer errors |
17+
// +-------------------+
1818

1919
// Base class for exceptions thrown because of errors from the users of our library
20-
class ConsumerError : public std::logic_error {
20+
class ProgrammerError : public std::logic_error {
2121
public:
2222

2323
using std::logic_error::logic_error;
2424
};
2525

26-
class ArgumentNotFound : public ConsumerError {
26+
class ArgumentNotFound : public ProgrammerError {
2727
public:
2828

29-
explicit ArgumentNotFound(std::string_view name) : ConsumerError(fmt::format("Could not find argument `{}`", name)) {}
29+
explicit ArgumentNotFound(std::string_view name)
30+
: ProgrammerError(fmt::format("Could not find argument `{}`", name)) {}
3031
};
3132

3233
// +-------------+
@@ -63,11 +64,22 @@ class UnexpectedPositional : public UserError {
6364
) {}
6465
};
6566

66-
class UnknownArgument : public UserError {
67+
class UnknownArguments : public UserError {
6768
public:
6869

69-
UnknownArgument(std::string_view cmd_name, std::string_view name, CmdFmt const &formatter)
70-
: UserError(fmt::format("Unknown argument for `{}`: `{}`", cmd_name, name), formatter) {}
70+
UnknownArguments(
71+
std::string_view cmd_name, std::vector<std::string_view> const &unknown_args, CmdFmt const &formatter
72+
)
73+
: UserError(
74+
fmt::format("Unknown arguments for `{}` command: `{}`", cmd_name, fmt::join(unknown_args, "`, `")), formatter
75+
) {}
76+
};
77+
78+
class UnknownSubcommand : public UserError {
79+
public:
80+
81+
UnknownSubcommand(std::string_view cmd_name, std::string_view subcmd_name, CmdFmt const &formatter)
82+
: UserError(fmt::format("Unknown subcommand `{}` for command `{}`", subcmd_name, cmd_name), formatter) {}
7183
};
7284

7385
class WrongType : public UserError {

include/opzioni/parsing.hpp

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,7 @@ class CmdParser {
7575
auto scanner = Scanner(args);
7676
auto const tokens = scanner();
7777
auto const indices = index_tokens(tokens);
78-
// TODO: check we did not receive unknown arguments (throw UnknownArgument)
79-
auto map = this->get_args_map(tokens, indices, 0, -1);
78+
auto map = this->get_args_map(args, tokens, indices, 0, tokens.size() - 1);
8079
return map;
8180
}
8281

@@ -103,33 +102,46 @@ class CmdParser {
103102
auto get_cmd_fmt() const noexcept { return CmdFmt(this->cmd_ref.get(), this->extra_info); }
104103

105104
auto get_args_map(
105+
std::span<char const *> const args,
106106
std::span<Token const> const tokens,
107107
TokenIndices const &indices,
108108
std::size_t const recursion_start_idx,
109109
std::size_t recursion_end_idx
110110
) {
111-
constexpr auto args_size = std::tuple_size_v<decltype(this->cmd_ref.get().args)>;
112111
auto args_map = ArgsMap<Cmd const>();
113112
args_map.exec_path = *tokens[recursion_start_idx].value;
114-
if (this->cmd_ref.get().has_subcmds())
115-
this->parse_possible_subcmd(args_map, tokens, indices, recursion_start_idx, recursion_end_idx);
113+
if (this->cmd_ref.get().has_subcmds()) {
114+
this->parse_possible_subcmd(args, args_map, tokens, indices, recursion_start_idx, recursion_end_idx);
115+
}
116116
// further args have to be > recursion_start_idx and <= recursion_end_idx
117+
std::set<std::size_t> consumed_indices;
118+
consumed_indices.insert(recursion_start_idx);
119+
constexpr auto args_size = std::tuple_size_v<decltype(this->cmd_ref.get().args)>;
117120
this->process_tokens(
118-
args_map, tokens, indices, recursion_start_idx, recursion_end_idx, std::make_index_sequence<args_size>()
121+
args_map,
122+
tokens,
123+
indices,
124+
recursion_start_idx,
125+
recursion_end_idx,
126+
consumed_indices,
127+
std::make_index_sequence<args_size>()
119128
);
129+
this->check_unknown_args(args, tokens, recursion_start_idx, recursion_end_idx, consumed_indices);
120130
return args_map;
121131
}
122132

123133
void parse_possible_subcmd(
134+
std::span<char const *> const args,
124135
ArgsMap<Cmd const> &args_map,
125136
std::span<Token const> const tokens,
126137
TokenIndices const &indices,
127138
std::size_t const recursion_start_idx,
128139
std::size_t &recursion_end_idx
129140
) {
130141
// Note: a command can't have positionals if it has subcommands, so it suffices to check if first positional token
131-
// is a subcommand. If it isn't, everything is considered as positionals. Also, the command name is not present in
132-
// the indices, so 0 really is the first positional.
142+
// is a subcommand. If it's not, then the user provided an unknown subcommand, since this method is only called
143+
// if the command has any subcommand. Also, the command name is not present in the indices,
144+
// so 0 (or recursion_start_idx) really is the first positional.
133145
auto const tok_idx = indices.first_pos_idx_after(recursion_start_idx);
134146
if (!tok_idx.has_value()) return;
135147
auto const &tok = tokens[*tok_idx];
@@ -138,19 +150,21 @@ class CmdParser {
138150
int i = 0;
139151
// clang-format off
140152
std::apply(
141-
[this, &i, cmd_idx, &args_map, tokens, &indices, recursion_end_idx, tok_idx](auto&&... cmd) {
153+
[this, &i, cmd_idx, &args_map, &args, tokens, &indices, tok_idx, recursion_end_idx](auto&&... cmd) {
142154
(void)(( // cast to void to suppress unused warning
143155
i == cmd_idx
144156
? (args_map.submap = CmdParser<typename std::remove_reference_t<decltype(cmd)>::type>(
145-
cmd.get(), this->extra_info, this->cmd_ref.get().name).get_args_map(tokens, indices, *tok_idx, recursion_end_idx), true)
157+
cmd.get(), this->extra_info, this->cmd_ref.get().name).get_args_map(args, tokens, indices, *tok_idx, recursion_end_idx), true)
146158
: (++i, false)
147159
) || ...);
148160
},
149161
this->cmd_ref.get().subcmds
150162
);
151163
// clang-format on
152-
// when coming back from parsing a subcommand, limit the rest of the tokens to up to where we found the subcommand
164+
// when coming back from parsing a subcommand, limit the rest of the tokens to up to the token before the subcmd
153165
recursion_end_idx = *tok_idx - 1;
166+
} else {
167+
throw UnknownSubcommand(this->cmd_ref.get().name, *tok.value, this->get_cmd_fmt());
154168
}
155169
}
156170

@@ -161,16 +175,17 @@ class CmdParser {
161175
TokenIndices const &indices,
162176
std::size_t const recursion_start_idx,
163177
std::size_t const recursion_end_idx,
178+
std::set<std::size_t> &consumed_indices,
164179
std::index_sequence<Is...>
165180
) {
166181
try {
167-
(this->process_ith_arg<Is>(args_map, tokens, indices, recursion_start_idx, recursion_end_idx), ...);
182+
// clang-format off
183+
(this->process_ith_arg<Is>(args_map, tokens, indices, recursion_start_idx, recursion_end_idx, consumed_indices), ...);
168184
if (!args_map.has_submap()) { // commands can't have both positionals and subcommands
169185
std::size_t cur_pos_idx = 0;
170-
// clang-format off
171-
(this->process_ith_arg<Is>(args_map, tokens, indices, recursion_start_idx, recursion_end_idx, cur_pos_idx), ...);
172-
// clang-format on
186+
(this->process_ith_arg<Is>(args_map, tokens, indices, recursion_start_idx, recursion_end_idx, consumed_indices, cur_pos_idx), ...);
173187
}
188+
// clang-format on
174189
(this->check_ith_arg<Is>(args_map), ...);
175190
} catch (std::runtime_error const &e) {
176191
throw UserError(e.what(), get_cmd_fmt());
@@ -183,19 +198,26 @@ class CmdParser {
183198
std::span<Token const> const tokens,
184199
TokenIndices const &indices,
185200
std::size_t const recursion_start_idx,
186-
std::size_t const recursion_end_idx
201+
std::size_t const recursion_end_idx,
202+
std::set<std::size_t> &consumed_indices
187203
) {
188204
switch (auto const &arg = std::get<I>(this->cmd_ref.get().args); arg.type) {
189205
case ArgType::FLG: {
190206
std::size_t flg_count = 0;
191207
if (auto const it = indices.opts_n_flgs.find(arg.name); it != indices.opts_n_flgs.end()) {
192208
for (auto const idx : it->second) {
193-
if (idx > recursion_start_idx && idx <= recursion_end_idx) flg_count += 1;
209+
if (idx > recursion_start_idx && idx <= recursion_end_idx) {
210+
flg_count += 1;
211+
consumed_indices.insert(idx);
212+
}
194213
}
195214
}
196215
if (auto const it = indices.opts_n_flgs.find(arg.abbrev); it != indices.opts_n_flgs.end()) {
197216
for (auto const idx : it->second) {
198-
if (idx > recursion_start_idx && idx <= recursion_end_idx) flg_count += 1;
217+
if (idx > recursion_start_idx && idx <= recursion_end_idx) {
218+
flg_count += 1;
219+
consumed_indices.insert(idx);
220+
}
199221
}
200222
}
201223

@@ -217,15 +239,19 @@ class CmdParser {
217239
all_idxs.reserve(reserve_size);
218240
}
219241
if (has_name) {
220-
// all_idxs.append_range(it_name->second); // TODO(?): use `| filter`
221242
for (auto const idx : it_name->second) {
222-
if (idx > recursion_start_idx && idx <= recursion_end_idx) all_idxs.push_back(idx);
243+
if (idx > recursion_start_idx && idx <= recursion_end_idx) {
244+
all_idxs.push_back(idx);
245+
consumed_indices.insert(idx);
246+
}
223247
}
224248
}
225249
if (has_abbrev) {
226-
// all_idxs.append_range(it_abbrev->second); // TODO(?): use `| filter`
227250
for (auto const idx : it_abbrev->second) {
228-
if (idx > recursion_start_idx && idx <= recursion_end_idx) all_idxs.push_back(idx);
251+
if (idx > recursion_start_idx && idx <= recursion_end_idx) {
252+
all_idxs.push_back(idx);
253+
consumed_indices.insert(idx);
254+
}
229255
}
230256
}
231257
std::ranges::sort(all_idxs);
@@ -237,6 +263,7 @@ class CmdParser {
237263
else if (idx + 1 < tokens.size() && tokens[idx + 1].type == TokenType::IDENTIFIER) {
238264
opt_values.push_back(*tokens[idx + 1].value);
239265
this->indices_used_as_opt_values.insert(idx + 1);
266+
consumed_indices.insert(idx + 1);
240267
}
241268
}
242269

@@ -258,6 +285,7 @@ class CmdParser {
258285
TokenIndices const &indices,
259286
std::size_t const recursion_start_idx,
260287
std::size_t const recursion_end_idx,
288+
std::set<std::size_t> &consumed_indices,
261289
std::size_t &cur_pos_idx
262290
) {
263291
auto const &arg = std::get<I>(this->cmd_ref.get().args);
@@ -276,6 +304,7 @@ class CmdParser {
276304
if (tok_idx >= tokens.size()) return;
277305

278306
cur_pos_idx += 1;
307+
consumed_indices.insert(tok_idx);
279308
if (auto const &tok = tokens[tok_idx]; tok.value)
280309
consume_arg<I>(args_map, arg, *tok.value, this->cmd_ref.get(), this->extra_info);
281310
}
@@ -288,6 +317,23 @@ class CmdParser {
288317
if (arg.has_default()) std::get<I>(args_map.args) = *arg.default_value;
289318
}
290319
}
320+
321+
void check_unknown_args(
322+
std::span<char const *> const args,
323+
std::span<Token const> const tokens,
324+
std::size_t const recursion_start_idx,
325+
std::size_t const recursion_end_idx,
326+
std::set<std::size_t> const &consumed_indices
327+
) const {
328+
if (std::size_t const recursion_amount_indices = recursion_end_idx - recursion_start_idx + 1;
329+
consumed_indices.size() < recursion_amount_indices) {
330+
std::vector<std::string_view> unknown_args;
331+
for (std::size_t idx = recursion_start_idx; idx <= recursion_end_idx; ++idx) {
332+
if (!consumed_indices.contains(idx)) unknown_args.emplace_back(args[tokens[idx].args_idx]);
333+
}
334+
throw UnknownArguments(this->cmd_ref.get().name, unknown_args, this->get_cmd_fmt());
335+
}
336+
}
291337
};
292338

293339
} // namespace opz

0 commit comments

Comments
 (0)