Iros
 
Loading...
Searching...
No Matches
parser.h
Go to the documentation of this file.
1#pragma once
2
4#include "di/cli/argument.h"
5#include "di/cli/option.h"
13#include "di/format/style.h"
15#include "di/function/not_fn.h"
16#include "di/function/prelude.h"
18#include "di/io/string_writer.h"
19#include "di/io/writer_print.h"
21#include "di/meta/constexpr.h"
22#include "di/meta/language.h"
24
25namespace di::cli {
26namespace detail {
27 template<concepts::Object Base>
28 class Parser {
29 private:
30 constexpr static auto max_options = 100ZU;
31 constexpr static auto max_arguments = 100ZU;
32
33 public:
34 constexpr explicit Parser(StringView app_name, StringView description)
35 : m_app_name(app_name), m_description(description) {}
36
37 template<auto member>
38 requires(concepts::MemberObjectPointer<decltype(member)> &&
39 concepts::SameAs<Base, meta::MemberPointerClass<decltype(member)>>)
40 constexpr auto option(Optional<char> short_name, Optional<TransparentStringView> long_name,
41 StringView description, bool required = false, bool always_succeed = false) && {
42 auto new_option = Option { c_<member>, short_name, long_name, description, required, always_succeed };
43 DI_ASSERT(m_options.push_back(new_option));
44 return di::move(*this);
45 }
46
47 constexpr auto help(Optional<char> short_name = {}, Optional<TransparentStringView> long_name = "help"_tsv,
48 StringView description = "Print help message"_sv) {
49 static_assert(
50 requires { c_<&Base::help>; },
51 "A help message requires the argument type to have a boolean help member.");
52 static_assert(SameAs<meta::MemberPointerValue<decltype(&Base::help)>, bool>,
53 "A help message requires the argument type to have a boolean help member.");
54 return di::move(*this).template option<&Base::help>(short_name, long_name, description, false, true);
55 }
56
57 template<auto member>
58 requires(concepts::MemberObjectPointer<decltype(member)> &&
59 concepts::SameAs<Base, meta::MemberPointerClass<decltype(member)>>)
60 constexpr auto argument(StringView name, StringView description, bool required = false) && {
61 auto new_argument = Argument { c_<member>, name, description, required };
62 DI_ASSERT(m_arguments.push_back(new_argument));
63 return di::move(*this);
64 }
65
66 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
68 using namespace di::string_literals;
69
70 if (args.empty()) {
71 return Unexpected(BasicError::InvalidArgument);
72 }
73 args = *args.subspan(1);
74
75 auto seen_arguments = Array<bool, max_options> {};
76 seen_arguments.fill(false);
77
78 auto count_option_processed = usize { 0 };
79
80 auto send_arg_to_back = [&](usize i) {
81 container::rotate(*args.subspan(i), args.begin() + i + 1);
82 count_option_processed++;
83 };
84
85 auto result = Base {};
86 for (usize i = 0; i < args.size(); i++) {
87 auto arg_index = i - count_option_processed;
88 auto arg = args[arg_index];
89
90 // Handle positional argument.
91 if (!arg.starts_with('-')) {
92 continue;
93 }
94
95 // Handle exactly "--".
96 if (arg == "--"_tsv) {
97 send_arg_to_back(arg_index);
98 break;
99 }
100
101 // Handle short argument.
102 if (!arg.starts_with("--"_tsv)) {
103 for (usize char_index = 1; char_index < arg.size(); char_index++) {
104 auto index = lookup_short_name(arg[char_index]);
105 if (!index) {
106 return Unexpected(BasicError::InvalidArgument);
107 }
108
109 // Parse boolean flag.
110 if (option_boolean(*index)) {
111 DI_TRY(option_parse(*index, seen_arguments, &result, {}));
112 if (option_always_succeeds(*index)) {
113 return result;
114 }
115 continue;
116 }
117
118 // Parse short option with directly specified value: '-iinput.txt'
119 auto value_view = arg.substr(arg.begin() + isize(char_index + 1));
120 if (!value_view.empty()) {
121 DI_TRY(option_parse(*index, seen_arguments, &result, value_view));
122 if (option_always_succeeds(*index)) {
123 return result;
124 }
125 break;
126 }
127
128 // Fail if the is no subsequent arguments left.
129 if (i + 1 >= args.size()) {
130 return Unexpected(BasicError::InvalidArgument);
131 }
132
133 // Use the next argument as the value.
134 DI_TRY(option_parse(*index, seen_arguments, &result, args[arg_index + 1]));
135 if (option_always_succeeds(*index)) {
136 return result;
137 }
138 send_arg_to_back(arg_index);
139 i++;
140 }
141 send_arg_to_back(arg_index);
142 continue;
143 }
144
145 // Handle long arguments.
146 auto equal = arg.find('=');
147 auto name = ""_tsv;
148 if (!equal) {
149 name = arg.substr(arg.begin() + 2);
150 } else {
151 name = arg.substr(arg.begin() + 2, equal.begin());
152 }
153
154 auto index = lookup_long_name(name);
155 if (!index) {
156 return Unexpected(BasicError::InvalidArgument);
157 }
158
159 if (option_boolean(*index)) {
160 if (equal) {
161 return Unexpected(BasicError::InvalidArgument);
162 }
163 DI_TRY(option_parse(*index, seen_arguments, &result, {}));
164 if (option_always_succeeds(*index)) {
165 return result;
166 }
167 send_arg_to_back(arg_index);
168 continue;
169 }
170
171 auto value = ""_tsv;
172 if (!equal && i + 1 >= args.size()) {
173 return Unexpected(BasicError::InvalidArgument);
174 }
175 if (!equal) {
176 value = args[arg_index + 1];
177 send_arg_to_back(arg_index);
178 send_arg_to_back(arg_index);
179 i++;
180 } else {
181 value = arg.substr(equal.end());
182 send_arg_to_back(arg_index);
183 }
184
185 DI_TRY(option_parse(*index, seen_arguments, &result, value));
186 if (option_always_succeeds(*index)) {
187 return result;
188 }
189 }
190
191 // Validate all required arguments were processed.
192 for (usize i = 0; i < m_options.size(); i++) {
193 if (!seen_arguments[i] && option_required(i)) {
194 return Unexpected(BasicError::InvalidArgument);
195 }
196 }
197
198 // All the positional arguments are now at the front of the array.
199 auto positional_arguments = *args.subspan(0, args.size() - count_option_processed);
200 if (positional_arguments.size() < minimum_required_argument_count()) {
201 return Unexpected(BasicError::InvalidArgument);
202 }
203
204 auto argument_index = usize(0);
205 for (auto i = usize(0); i < positional_arguments.size(); argument_index++) {
206 auto count_to_consume = !argument_variadic(i) ? 1 : positional_arguments.size() - argument_count() + 1;
207 auto input = *positional_arguments.subspan(i, count_to_consume);
208 DI_TRY(argument_parse(argument_index, &result, input));
209 i += count_to_consume;
210 }
211 return result;
212 }
213
214 template<Impl<io::Writer> Writer>
215 constexpr void write_help(Writer& writer) const {
217
218 constexpr auto header_effect = di::FormatEffect::Bold | di::FormatColor::Yellow;
219 constexpr auto program_effect = di::FormatEffect::Bold;
220 constexpr auto option_effect = di::FormatColor::Cyan;
221 constexpr auto option_value_effect = di::FormatColor::Green;
222 constexpr auto argument_effect = di::FormatColor::Green;
223
224 io::writer_println<Enc>(writer, "{}"_sv, di::Styled("NAME:"_sv, header_effect));
225 io::writer_println<Enc>(writer, " {}: {}"_sv, di::Styled(m_app_name, program_effect), m_description);
226
227 io::writer_println<Enc>(writer, "\n{}"_sv, di::Styled("USAGE:"_sv, header_effect));
228 io::writer_print<Enc>(writer, " {}"_sv, di::Styled(m_app_name, program_effect));
229 if (di::any_of(m_options, di::not_fn(&Option::required))) {
230 io::writer_print<Enc>(writer, " [OPTIONS]"_sv);
231 }
232 for (auto const& option : m_options) {
233 if (option.required()) {
234 io::writer_print<Enc>(writer, " {}"_sv, di::Styled(option.display_name(), option_effect));
235
236 if (!option.boolean()) {
237 io::writer_print<Enc>(writer, " {}"_sv, di::Styled("<VALUE>"_sv, option_value_effect));
238 }
239 }
240 }
241 for (auto const& argument : m_arguments) {
242 io::writer_print<Enc>(writer, " {}"_sv, di::Styled(argument.display_name(), argument_effect));
243 }
244 io::writer_println<Enc>(writer, ""_sv);
245
246 if (!m_arguments.empty()) {
247 io::writer_println<Enc>(writer, "\n{}"_sv, di::Styled("ARGUMENTS:"_sv, header_effect));
248 for (auto const& argument : m_arguments) {
249 io::writer_println<Enc>(writer, " {}: {}"_sv, di::Styled(argument.display_name(), argument_effect),
250 argument.description());
251 }
252 }
253
254 if (!m_options.empty()) {
255 io::writer_println<Enc>(writer, "\n{}"_sv, di::Styled("OPTIONS:"_sv, header_effect));
256 for (auto const& option : m_options) {
257 io::writer_print<Enc>(writer, " "_sv);
258 if (option.short_name()) {
259 io::writer_print<Enc>(writer, "{}"_sv, di::Styled(option.short_display_name(), option_effect));
260 if (!option.boolean() && !option.long_name()) {
261 io::writer_print<Enc>(writer, " {}"_sv, di::Styled("<VALUE>"_sv, option_value_effect));
262 }
263 }
264 if (option.short_name() && option.long_name()) {
265 io::writer_print<Enc>(writer, ", "_sv);
266 }
267 if (option.long_name()) {
268 io::writer_print<Enc>(writer, "{}"_sv, di::Styled(option.long_display_name(), option_effect));
269 if (!option.boolean()) {
270 io::writer_print<Enc>(writer, " {}"_sv, di::Styled("<VALUE>"_sv, option_value_effect));
271 }
272 }
273 io::writer_println<Enc>(writer, ": {}"_sv, option.description());
274 }
275 }
276 }
277
278 constexpr auto help_string() const {
279 auto writer = di::StringWriter {};
280 write_help(writer);
281 return di::move(writer).output();
282 }
283
284 private:
285 constexpr auto option_required(usize index) const -> bool { return m_options[index].required(); }
286 constexpr auto option_boolean(usize index) const -> bool { return m_options[index].boolean(); }
287 constexpr auto option_always_succeeds(usize index) const -> bool { return m_options[index].always_succeeds(); }
288
289 constexpr auto option_parse(usize index, Span<bool> seen_arguments, Base* output,
291 DI_TRY(m_options[index].parse(output, input));
292 seen_arguments[index] = true;
293 return {};
294 }
295
296 constexpr auto argument_variadic(usize index) const -> bool { return m_arguments[index].variadic(); }
297
298 constexpr auto argument_parse(usize index, Base* output, Span<TransparentStringView> input) const
299 -> Result<void> {
300 return m_arguments[index].parse(output, input);
301 }
302
303 constexpr auto minimum_required_argument_count() const -> usize {
304 return di::sum(m_arguments | di::transform(&Argument::required_argument_count));
305 }
306
307 constexpr auto argument_count() const -> usize { return m_arguments.size(); }
308
309 constexpr auto lookup_short_name(char short_name) const -> Optional<usize> {
310 auto const* it = di::find(m_options, short_name, &Option::short_name);
311 return lift_bool(it != m_options.end()) % [&] {
312 return usize(it - m_options.begin());
313 };
314 }
315
316 constexpr auto lookup_long_name(TransparentStringView long_name) const -> Optional<usize> {
317 auto const* it = di::find(m_options, long_name, &Option::long_name);
318 return lift_bool(it != m_options.end()) % [&] {
319 return usize(it - m_options.begin());
320 };
321 }
322
323 StringView m_app_name;
324 StringView m_description;
325 StaticVector<Option, Constexpr<max_options>> m_options;
326 StaticVector<Argument, Constexpr<max_arguments>> m_arguments;
327 };
328}
329
330template<concepts::Object T>
331constexpr auto cli_parser(StringView app_name, StringView description) {
332 return detail::Parser<T> { app_name, description };
333}
334}
335
336namespace di {
337using cli::cli_parser;
338}
#define DI_ASSERT(...)
Definition assert_bool.h:7
Definition argument.h:12
constexpr auto required_argument_count() const -> usize
Definition argument.h:75
Definition option.h:13
constexpr auto long_name() const
Definition option.h:58
constexpr auto short_name() const
Definition option.h:57
constexpr auto required() const
Definition option.h:60
Definition parser.h:28
constexpr auto parse(Span< TransparentStringView > args) -> Result< Base >
Definition parser.h:67
constexpr void write_help(Writer &writer) const
Definition parser.h:215
constexpr Parser(StringView app_name, StringView description)
Definition parser.h:34
constexpr auto option(Optional< char > short_name, Optional< TransparentStringView > long_name, StringView description, bool required=false, bool always_succeed=false) &&
Definition parser.h:40
constexpr auto help(Optional< char > short_name={}, Optional< TransparentStringView > long_name="help"_tsv, StringView description="Print help message"_sv)
Definition parser.h:47
constexpr auto help_string() const
Definition parser.h:278
constexpr auto argument(StringView name, StringView description, bool required=false) &&
Definition parser.h:60
constexpr auto substr(Iterator first, Optional< Iterator > last={}) const
Definition constant_string_interface.h:99
Definition utf8_encoding.h:107
Definition style.h:131
Definition string_writer.h:14
Definition optional_forward_declaration.h:5
Definition span_forward_declaration.h:10
Definition unexpected.h:14
Definition language.h:203
Definition core.h:114
#define DI_TRY(...)
Definition monad_try.h:13
Definition argument.h:11
Definition argument.h:11
constexpr auto cli_parser(StringView app_name, StringView description)
Definition parser.h:331
string::StringViewImpl< string::Utf8Encoding > StringView
Definition string_view.h:12
constexpr auto rotate
Definition rotate.h:94
string::StringViewImpl< string::TransparentEncoding > TransparentStringView
Definition string_view.h:13
constexpr auto writer_print
Definition writer_print.h:20
meta::List< WriteSome, Flush > Writer
Definition writer.h:59
constexpr auto writer_println
Definition writer_println.h:21
Definition string.h:17
Type< detail::MemberPointerValueHelper< RemoveCV< T > > > MemberPointerValue
Definition language.h:184
Type< detail::MemberPointerClassHelper< RemoveCV< T > > > MemberPointerClass
Definition language.h:195
ssize_t isize
Definition integers.h:34
size_t usize
Definition integers.h:33
Expected< T, Error > Result
Definition result.h:8
Definition zstring_parser.h:9
constexpr auto find
Definition find.h:35
constexpr auto c_
A value of type Constexpr<val>.
Definition constexpr.h:252
constexpr auto not_fn(F &&function)
Definition not_fn.h:55
constexpr auto equal
Definition equal.h:23
constexpr auto lift_bool
Definition lift_bool.h:13
constexpr auto sum
Definition sum.h:26
constexpr auto any_of
Definition any_of.h:24
constexpr auto parse
Definition parse.h:23
Definition span_fixed_size.h:37
constexpr void fill(T const &value)
Definition array.h:88
Definition getopt.h:12