di 0.1.0
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/bash.h"
6#include "di/cli/error.h"
7#include "di/cli/option.h"
8#include "di/cli/subcommand.h"
9#include "di/cli/zsh.h"
22#include "di/format/style.h"
25#include "di/function/not_fn.h"
26#include "di/function/prelude.h"
28#include "di/io/string_writer.h"
29#include "di/io/writer_print.h"
32#include "di/meta/algorithm.h"
33#include "di/meta/constexpr.h"
34#include "di/meta/language.h"
35#include "di/meta/operations.h"
36#include "di/reflect/prelude.h"
37#include "di/util/construct.h"
39
40namespace di::cli {
41enum class Shell {
44};
45
46constexpr static auto tag_invoke(Tag<reflect>, InPlaceType<Shell>) {
47 using enum Shell;
48 return di::make_enumerators<"Shell">(di::enumerator<"bash", Bash, "bash shell">,
49 di::enumerator<"zsh", Zsh, "zsh shell">);
50}
51
52namespace detail {
53 template<concepts::Object Base>
54 class Parser {
55 private:
56 constexpr static auto max_options = 100ZU;
57 constexpr static auto max_arguments = 100ZU;
58 constexpr static auto max_subcommands = 100ZU;
59
60 public:
61 constexpr explicit Parser(TransparentStringView app_name, StringView description)
62 : m_app_name(app_name), m_description(description) {}
63
64 template<auto member>
65 requires(concepts::MemberObjectPointer<decltype(member)> &&
66 concepts::DerivedFrom<Base, meta::MemberPointerClass<decltype(member)>>)
67 constexpr auto option(Optional<char> short_name, Optional<TransparentStringView> long_name,
68 StringView description, bool required = false, Optional<StringView> value_name = {},
69 Optional<ValueType> value_type = {}, bool is_help = false) && {
70 auto new_option =
71 Option { c_<member>, short_name, long_name, description, required, value_name, value_type, is_help };
72 DI_ASSERT(m_options.push_back(new_option));
73 return di::move(*this);
74 }
75
76 constexpr auto help(Optional<char> short_name = {}, Optional<TransparentStringView> long_name = "help"_tsv,
77 StringView description = "Print help message"_sv) {
78 static_assert(
79 requires { c_<&Base::help>; },
80 "A help message requires the argument type to have a boolean help member.");
81 static_assert(SameAs<meta::MemberPointerValue<decltype(&Base::help)>, bool>,
82 "A help message requires the argument type to have a boolean help member.");
83 return di::move(*this).template option<&Base::help>(short_name, long_name, description, false, {}, {},
84 true);
85 }
86
87 template<auto member>
88 requires(concepts::MemberObjectPointer<decltype(member)> &&
89 concepts::DerivedFrom<Base, meta::MemberPointerClass<decltype(member)>>)
90 constexpr auto argument(StringView name, StringView description, bool required = false,
91 Optional<ValueType> value_type = {}) && {
92 DI_ASSERT(!has_subcommands());
93 auto new_argument = Argument { c_<member>, name, description, required, value_type };
94 DI_ASSERT(m_arguments.push_back(new_argument));
95 return di::move(*this);
96 }
97
98 template<auto member>
99 requires(concepts::MemberObjectPointer<decltype(member)> &&
100 concepts::DerivedFrom<Base, meta::MemberPointerClass<decltype(member)>> &&
101 concepts::InstanceOf<meta::MemberPointerValue<decltype(member)>, Variant>)
102 constexpr auto subcommands() && {
103 DI_ASSERT(!has_arguments());
104
105 auto do_subcommand = [&]<typename Type>(InPlaceType<Type>) {
106 if constexpr (!SameAs<Type, Void>) {
107 DI_ASSERT(m_subcommands.push_back(Subcommand { c_<member>, in_place_type<Type> }));
108 } else {
109 m_subcommand_is_required = true;
110 }
111 };
112
113 auto do_subcommands = [&]<typename... Types>(InPlaceType<Variant<Types...>>) {
114 (do_subcommand(in_place_type<Types>), ...);
115 };
116
117 do_subcommands(in_place_type<meta::MemberPointerValue<decltype(member)>>);
118 return di::move(*this);
119 }
120
121 template<Impl<io::Writer> Writer>
122 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
123 constexpr auto parse(Span<TransparentStringView> args, Writer& writer,
124 Span<TransparentStringView> base_commands = {}) -> Result<Base> {
125 using namespace di::string_literals;
126
127 // The first argument is the name of the command, so its ignored for parsing.
128 auto const use_colors = io::interactive_device(writer);
129 if (!args.empty()) {
130 args = *args.subspan(1);
131 }
132
133 auto seen_options = Array<bool, max_options> {};
134 seen_options.fill(false);
135
136 auto count_option_processed = usize { 0 };
137 auto subcommand_seen = false;
138
139 auto send_arg_to_back = [&](usize i) {
140 container::rotate(*args.subspan(i), args.begin() + i + 1);
141 count_option_processed++;
142 };
143
144 auto result = Base {};
145 for (usize i = 0; i < args.size(); i++) {
146 auto arg_index = i - count_option_processed;
147 auto arg = args[arg_index];
148
149 // Handle positional argument.
150 if (!arg.starts_with('-')) {
151 if (!has_subcommands()) {
152 continue;
153 }
154
155 // Now we need to match the subcommand to the argument.
156 auto subcommand_index = lookup_subcommand(arg);
157 if (!subcommand_index) {
158 return Unexpected(Error(UnknownSubcommand(arg, closest_subcommand_match(arg)), use_colors));
159 }
160
161 DI_TRY(subcommand_parse(
162 subcommand_index.value(), &result,
163 args.subspan(arg_index, args.size() - arg_index - count_option_processed).value(), writer,
164 base_commands));
165 count_option_processed = args.size();
166 subcommand_seen = true;
167 break;
168 }
169
170 // Handle exactly "--".
171 if (arg == "--"_tsv) {
172 send_arg_to_back(arg_index);
173 break;
174 }
175
176 // Handle short argument.
177 if (!arg.starts_with("--"_tsv)) {
178 for (usize char_index = 1; char_index < arg.size(); char_index++) {
179 auto index = lookup_short_name(arg[char_index]);
180 if (!index) {
181 return Unexpected(Error(UnknownShortOption(arg[char_index]), use_colors));
182 }
183
184 if (option_is_help(*index)) {
185 write_help(writer, base_commands);
186 return Unexpected(BasicError::Success);
187 }
188
189 // Parse boolean flag.
190 if (option_boolean(*index)) {
191 DI_TRY(option_parse(*index, seen_options, &result, {}, false, use_colors));
192 continue;
193 }
194
195 // Parse short option with directly specified value: '-iinput.txt'
196 auto value_view = arg.substr(arg.begin() + isize(char_index + 1));
197 if (!value_view.empty()) {
198 DI_TRY(option_parse(*index, seen_options, &result, value_view, false, use_colors));
199 break;
200 }
201
202 // Fail if the is no subsequent arguments left.
203 if (i + 1 >= args.size()) {
204 return Unexpected(Error(ShortOptionMissingRequiredValue(arg[char_index]), use_colors));
205 }
206
207 // Use the next argument as the value.
208 DI_TRY(option_parse(*index, seen_options, &result, args[arg_index + 1], false, use_colors));
209 send_arg_to_back(arg_index);
210 i++;
211 }
212 send_arg_to_back(arg_index);
213 continue;
214 }
215
216 // Handle long arguments.
217 auto equal = arg.find('=');
218 auto name = ""_tsv;
219 if (!equal) {
220 name = arg.substr(arg.begin() + 2);
221 } else {
222 name = arg.substr(arg.begin() + 2, equal.begin());
223 }
224
225 auto index = lookup_long_name(name);
226 if (!index) {
227 return Unexpected(Error(UnknownLongOption(name, closest_option_match(name)), use_colors));
228 }
229
230 if (option_is_help(*index)) {
231 write_help(writer, base_commands);
232 return Unexpected(BasicError::Success);
233 }
234
235 if (option_boolean(*index)) {
236 auto value = Optional<TransparentStringView> {};
237 if (equal) {
238 value = arg.substr(equal.end());
239 }
240
241 DI_TRY(option_parse(*index, seen_options, &result, value, true, use_colors));
242 send_arg_to_back(arg_index);
243 continue;
244 }
245
246 auto value = ""_tsv;
247 if (!equal && i + 1 >= args.size()) {
248 return Unexpected(Error(LongOptionMissingRequiredValue(name), use_colors));
249 }
250 if (!equal) {
251 value = args[arg_index + 1];
252 send_arg_to_back(arg_index);
253 send_arg_to_back(arg_index);
254 i++;
255 } else {
256 value = arg.substr(equal.end());
257 send_arg_to_back(arg_index);
258 }
259
260 DI_TRY(option_parse(*index, seen_options, &result, value, true, use_colors));
261 }
262
263 // Validate all required options were processed.
264 for (usize i = 0; i < m_options.size(); i++) {
265 if (!seen_options[i] && option_required(i)) {
266 return Unexpected(
267 Error(MissingRequiredOption(m_options[i].short_name(), m_options[i].long_name()), use_colors));
268 }
269 }
270
271 // If we have subcommands, we should've already matched them.
272 if (has_subcommands() && m_subcommand_is_required && !subcommand_seen) {
273 return Unexpected(Error(MissingSubcommand(), use_colors));
274 }
275
276 // All the positional arguments are now at the front of the array.
277 auto positional_arguments = *args.subspan(0, args.size() - count_option_processed);
278 if (positional_arguments.size() < minimum_required_argument_count()) {
279 auto available_arguments = positional_arguments.size();
280 for (auto& argument : m_arguments) {
281 if (argument.required_argument_count() > available_arguments) {
282 return Unexpected(Error(MissingRequiredArgument(argument.argument_name(), available_arguments,
283 argument.required_argument_count()),
284 use_colors));
285 }
286 available_arguments -= argument.required_argument_count();
287 }
288 }
289
290 auto argument_index = usize(0);
291 auto i = usize(0);
292 for (; i < positional_arguments.size() && i < m_arguments.size(); argument_index++) {
293 auto count_to_consume = !argument_variadic(i) ? 1 : positional_arguments.size() - argument_count() + 1;
294 auto input = *positional_arguments.subspan(i, count_to_consume);
295 DI_TRY(argument_parse(argument_index, &result, input, use_colors));
296 i += count_to_consume;
297 }
298 if (i < positional_arguments.size()) {
299 return Unexpected(
300 Error(ExtraArguments(positional_arguments.subspan(i).value() | di::to<Vector>()), use_colors));
301 }
302 return result;
303 }
304
305 template<Impl<io::Writer> Writer>
306 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
307 constexpr void write_help(Writer& writer, Span<TransparentStringView> base_commands = {}) const {
308 using Enc = container::string::Utf8Encoding;
309
310 constexpr auto header_effect = di::FormatEffect::Bold | di::FormatColor::Yellow;
311 constexpr auto program_effect = di::FormatEffect::Bold;
312 constexpr auto option_effect = di::FormatColor::Cyan;
313 constexpr auto option_value_effect = di::FormatColor::Green;
314 constexpr auto argument_effect = di::FormatColor::Green;
315 constexpr auto subcommand_effect = di::FormatEffect::Bold;
316
317 auto write_values = [&](Vector<Tuple<String, StringView>> values) {
318 if (values.empty() || values.size() > 10) {
319 return;
320 }
321 for (auto const& [value, description] : values) {
322 io::writer_println<Enc>(writer, " {}{}{}"_sv, di::Styled(value, option_value_effect),
323 description.empty() ? ""_sv : ": "_sv, description);
324 }
325 };
326
327 auto program_name =
328 concat(base_commands, single(m_app_name)) | join_with(' ') | di::to<TransparentString>();
329 io::writer_println<Enc>(writer, "{}"_sv, di::Styled("NAME:"_sv, header_effect));
330 io::writer_println<Enc>(writer, " {}: {}"_sv, di::Styled(program_name, program_effect), m_description);
331
332 io::writer_println<Enc>(writer, "\n{}"_sv, di::Styled("USAGE:"_sv, header_effect));
333 io::writer_print<Enc>(writer, " {}"_sv, di::Styled(program_name, program_effect));
334 if (di::any_of(m_options, di::not_fn(&Option::required))) {
335 io::writer_print<Enc>(writer, " [OPTIONS]"_sv);
336 }
337 for (auto const& option : m_options) {
338 if (option.required()) {
339 io::writer_print<Enc>(writer, " {}"_sv, di::Styled(option.display_name(), option_effect));
340
341 if (!option.boolean()) {
342 io::writer_print<Enc>(writer, " {}"_sv,
343 di::Styled(format("<{}>"_sv, option.value_name()), option_value_effect));
344 }
345 }
346 }
347 for (auto const& argument : m_arguments) {
348 io::writer_print<Enc>(writer, " {}"_sv, di::Styled(argument.display_name(), argument_effect));
349 }
350 if (has_subcommands()) {
351 auto display_name = m_subcommand_is_required ? "COMMAND"_sv : "[COMMAND]"_sv;
352 io::writer_print<Enc>(writer, " {}"_sv, display_name);
353 }
354 io::writer_println<Enc>(writer, ""_sv);
355
356 if (has_subcommands()) {
357 io::writer_println<Enc>(writer, "\n{}"_sv, di::Styled("COMMANDS:"_sv, header_effect));
358 auto first = true;
359 for (auto const& subcommand : m_subcommands) {
360 io::writer_println<Enc>(writer, " {}: {}{}"_sv, di::Styled(subcommand.name(), subcommand_effect),
361 subcommand.description(),
362 first && !m_subcommand_is_required ? " (default)"_sv : ""_sv);
363 first = false;
364 }
365 }
366
367 if (!m_arguments.empty()) {
368 io::writer_println<Enc>(writer, "\n{}"_sv, di::Styled("ARGUMENTS:"_sv, header_effect));
369 for (auto const& argument : m_arguments) {
370 auto default_value = argument.default_value();
371 if (!default_value.empty() && argument.required_argument_count() == 0) {
372 default_value = format(" (default: {})"_sv, default_value);
373 } else {
374 default_value = {};
375 }
376 io::writer_println<Enc>(writer, " {}: {}{}"_sv,
377 di::Styled(argument.display_name(), argument_effect),
378 argument.description(), default_value);
379 write_values(argument.values());
380 }
381 }
382
383 if (!m_options.empty()) {
384 io::writer_println<Enc>(writer, "\n{}"_sv, di::Styled("OPTIONS:"_sv, header_effect));
385 for (auto const& option : m_options) {
386 io::writer_print<Enc>(writer, " "_sv);
387 if (option.short_name()) {
388 io::writer_print<Enc>(writer, "{}"_sv, di::Styled(option.short_display_name(), option_effect));
389 if (!option.boolean() && !option.long_name()) {
391 writer, " {}"_sv,
392 di::Styled(format("<{}>"_sv, option.value_name()), option_value_effect));
393 }
394 }
395 if (option.short_name() && option.long_name()) {
396 io::writer_print<Enc>(writer, ", "_sv);
397 }
398 if (option.long_name()) {
399 io::writer_print<Enc>(writer, "{}"_sv, di::Styled(option.long_display_name(), option_effect));
400 if (!option.boolean()) {
402 writer, " {}"_sv,
403 di::Styled(format("<{}>"_sv, option.value_name()), option_value_effect));
404 }
405 }
406 auto default_value = option.default_value();
407 if (!default_value.empty() && !option.required()) {
408 default_value = format(" (default: {})"_sv, default_value);
409 } else {
410 default_value = {};
411 }
412 io::writer_println<Enc>(writer, ": {}{}"_sv, option.description(), default_value);
413 write_values(option.values());
414 }
415 }
416 }
417
418 constexpr void bash_completions_inner(TreeMap<Vector<TransparentString>, String>& states,
419 Span<TransparentStringView> base_commands = {}) {
420 auto content = ""_s;
421 auto possible_values = Vector<String> {};
422 for (auto const& option : m_options) {
423 if (option.short_name()) {
424 possible_values.push_back(to_string(option.short_display_name()));
425 }
426 if (option.long_name()) {
427 possible_values.push_back(to_string(option.long_display_name()));
428 }
429 }
430 for (auto const& subcommand : m_subcommands) {
431 auto new_base_commands = base_commands | di::to<Vector>();
432 new_base_commands.push_back(m_app_name);
433 subcommand.inner_bash_completions(states, new_base_commands.span());
434
435 possible_values.push_back(to_string(subcommand.name()));
436 }
437 for (auto const& argument : m_arguments) {
438 for (auto& [value, _] : argument.values()) {
439 possible_values.push_back(di::move(value));
440 }
441 }
442
443 content += format(" opts=\"{}\"\n"_sv,
444 possible_values | transform(bash::escape_value) | join_with(U' ') | di::to<String>());
445 content += format(" if [[ ${{cur}} == -* || ${{COMP_WORD}} -eq {} ]]; then\n"_sv,
446 base_commands.size() + 1);
447 content += format(" COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )\n"_sv);
448 content += format(" return 0\n"_sv);
449 content += format(" fi\n"_sv);
450 content += format(" case \"${{prev}}\" in\n"_sv);
451 for (auto const& option : m_options) {
452 if (option.boolean()) {
453 continue;
454 }
455 auto pattern = ""_s;
456 if (option.short_name()) {
457 pattern += to_string(option.short_display_name());
458 }
459 if (option.long_name()) {
460 if (!pattern.empty()) {
461 pattern.push_back(U'|');
462 }
463 pattern += to_string(option.long_display_name());
464 }
465 content += format(" {})\n"_sv, pattern);
466 auto values = option.values();
467 content += format("{}\n"_sv, bash::value_completions(option.value_type(), values.span()));
468 content += format(" return 0\n"_sv);
469 content += format(" ;;\n"_sv);
470 }
471 content += format(" *)\n"_sv);
472 content += format(" COMPREPLY=( $(compgen -W \"${{opts}}\" -- \"${{cur}}\") )\n"_sv);
473 content += format(" return 0\n"_sv);
474 content += format(" ;;\n"_sv);
475 content += format(" esac\n"_sv);
476
477 auto state = Vector<TransparentString> {};
478 for (auto part : base_commands) {
479 state.push_back(part.to_owned());
480 }
481 state.push_back(m_app_name.to_owned());
482 states.try_emplace(di::move(state), content);
483 }
484
485 template<Impl<io::Writer> Writer>
486 constexpr void write_bash_completions(Writer& writer) {
487 using Enc = container::string::Utf8Encoding;
488
489 // The bash completion header primarily just registers our completion function. Unlike
490 // zsh, we handle subcommands by determining which subcommand by scanning all the arguments.
491 // Then completion logic is handled by compgen.
492 //
493 // The logic for completions is based off the rust create
494 // [clap_complete](https://docs.rs/crate/clap_complete/latest/source/)
495 io::writer_println<Enc>(writer, R"~(_{}() {{
496 local i cur prev opts cmd
497 COMPREPLY=()
498 if [[ "${{BASH_VERSINFO[0]}}" -ge 4 ]]; then
499 cur="$2"
500 else
501 cur="${{COMP_WORDS[COMP_CWORD]}}"
502 fi
503 prev="$3"
504 cmd=""
505 opts=""
506)~"_sv,
507 m_app_name);
508
509 auto states = TreeMap<Vector<TransparentString>, String> {};
510 bash_completions_inner(states);
511
512 auto state_name = [&](Span<TransparentString const> state) -> String {
513 return state | join_with('_') | transform(construct<c32>) | di::to<String>();
514 };
515
516 io::writer_println<Enc>(writer, " for i in \"${{COMP_WORDS[@]:0:COMP_CWORD}}\"; do"_sv);
517 io::writer_println<Enc>(writer, " case \"${{cmd}},${{i}}\" in"_sv);
518 for (auto const& [state, _] : states) {
519 auto condition = "\",$1\""_s;
520 if (state.size() > 1) {
521 condition = format("{},{}"_sv, state_name(state.subspan(0, state.size() - 1).value()),
522 state.back().value());
523 }
524 io::writer_println<Enc>(writer, " {})"_sv, condition);
525 io::writer_println<Enc>(writer, " cmd=\"{}\""_sv, state_name(state.span()));
526 io::writer_println<Enc>(writer, " ;;"_sv);
527 }
528 io::writer_println<Enc>(writer, " *) ;;"_sv);
529 io::writer_println<Enc>(writer, " esac"_sv);
530 io::writer_println<Enc>(writer, " done\n"_sv);
531
532 io::writer_println<Enc>(writer, " case \"${{cmd}}\" in"_sv);
533 for (auto const& [state, logic] : states) {
534 io::writer_println<Enc>(writer, " {})"_sv, state_name(state.span()));
535 io::writer_print<Enc>(writer, "{}"_sv, logic);
536 io::writer_println<Enc>(writer, " ;;"_sv);
537 }
538 io::writer_println<Enc>(writer, " esac"_sv);
539
540 io::writer_println<Enc>(writer, R"~(}}
541
542if [[ "${{BASH_VERSINFO[0]}}" -eq 4 && "${{BASH_VERSINFO[1]}}" -ge 4 || "${{BASH_VERSINFO[0]}}" -gt 4 ]]; then
543 complete -F _{} -o nosort -o bashdefault -o default {}
544else
545 complete -F _{} -o bashdefault -o default {}
546fi)~"_sv,
547 m_app_name, m_app_name, m_app_name, m_app_name);
548 }
549
550 template<Impl<io::Writer> Writer>
551 void write_zsh_completions_inner(Writer& writer, u32 indentation, Span<TransparentStringView> base_commands) {
552 auto indent = repeat(U' ', indentation) | di::to<String>();
553 auto extra_indent = " "_sv;
554
555 using Enc = container::string::Utf8Encoding;
556 io::writer_println<Enc>(writer, "{}{}"_sv, indent, R"~(_arguments "${_arguments_options[@]}" : \)~"_sv);
557 for (auto const& option : m_options) {
558 for (auto const& spec : option.zsh_completion_specs()) {
559 io::writer_println<Enc>(writer, "{}{}'{}' \\"_sv, indent, extra_indent, spec);
560 }
561 }
562 for (auto const& argument : m_arguments) {
563 auto spec = argument.zsh_completion_spec();
564 io::writer_println<Enc>(writer, "{}{}'{}' \\"_sv, indent, extra_indent, spec);
565 }
566
567 auto const state_name = concat(base_commands, single(m_app_name)) | join_with('_') |
569 if (has_subcommands()) {
570 // Here we're just adding the optarg spec for the completions. Later we will add the subcommand logic.
571 auto possibilities = Vector<Tuple<String, StringView>> {};
572 for (auto const& subcommand : m_subcommands) {
573 possibilities.emplace_back(format("{}"_sv, subcommand.name()), subcommand.description());
574 }
575 io::writer_println<Enc>(writer, "{}{}':::{}' \\"_sv, indent, extra_indent,
576 zsh::completion_value_map(possibilities.span()));
577 io::writer_println<Enc>(writer, "{}{}'*::: :->{}' \\"_sv, indent, extra_indent, state_name);
578 }
579 io::writer_println<Enc>(writer, "{}{}&& ret=0"_sv, indent, extra_indent);
580 if (has_subcommands()) {
581 io::writer_println<Enc>(writer, "{}case $state in"_sv, indent);
582 io::writer_println<Enc>(writer, "{}({})"_sv, indent, state_name);
583 io::writer_println<Enc>(writer, "{}{}words=($line[1] \"${{words[@]}}\")"_sv, indent, extra_indent);
584 io::writer_println<Enc>(writer, "{}{}(( CURRENT += 1 ))"_sv, indent, extra_indent);
585 io::writer_println<Enc>(writer, "{}{}curcontext=\"${{curcontext%:*:*}}:{}-command-$line[1]:\""_sv,
586 indent, extra_indent, state_name);
587 io::writer_println<Enc>(writer, "{}{}case $line[1] in"_sv, indent, extra_indent);
588
589 for (auto const& subcommand : m_subcommands) {
590 io::writer_println<Enc>(writer, "{}{}{}({})"_sv, indent, extra_indent, extra_indent,
591 subcommand.name());
592 auto new_base_commands = base_commands | di::to<Vector>();
593 new_base_commands.push_back(m_app_name);
594 subcommand.write_zsh_completions_inner(writer, indentation + 12, new_base_commands.span());
595 io::writer_println<Enc>(writer, "{}{}{};;"_sv, indent, extra_indent, extra_indent);
596 }
597
598 io::writer_println<Enc>(writer, "{}{}esac"_sv, indent, extra_indent);
599 io::writer_println<Enc>(writer, "{}{};;"_sv, indent, extra_indent);
600 io::writer_println<Enc>(writer, "{}esac"_sv, indent);
601 }
602 }
603
604 template<Impl<io::Writer> Writer>
605 void write_zsh_completions(Writer& writer) {
606 using Enc = container::string::Utf8Encoding;
607
608 // The zsh completion header most declares the completion function and sets up the arguments.
609 // We want -s as we supported stacke short options, and -S since we support `--` as a delimiter.
610 // -C is needed for subcommand handling.
611 //
612 // The logic for completions is based off the rust create
613 // [clap_complete](https://docs.rs/crate/clap_complete/latest/source/)
614 io::writer_println<Enc>(writer, R"~(#compdef {}
615
616autoload -U is-at-least
617
618_{}() {{
619 typeset -A opt_args
620 typeset -a _arguments_options
621 local ret=1
622
623 if is-at-least 5.2; then
624 _arguments_options=(-s -S -C)
625 else
626 _arguments_options=(-s -C)
627 fi
628
629 local context curcontext="$curcontext" state line)~"_sv,
630 m_app_name, m_app_name);
631
632 write_zsh_completions_inner(writer, 4, {});
633
634 io::writer_println<Enc>(writer, R"~(}}
635
636if [ "$funcstack[1]" = "_{}" ]; then
637 _{} "$@"
638else
639 compdef _{} {}
640fi)~"_sv,
641 m_app_name, m_app_name, m_app_name, m_app_name);
642 }
643
644 template<Impl<io::Writer> Writer>
645 void write_completions(Writer& writer, Shell shell) {
646 using enum Shell;
647 switch (shell) {
648 case Bash:
649 return write_bash_completions(writer);
650 case Zsh:
651 return write_zsh_completions(writer);
652 }
653 };
654
655 constexpr auto help_string(Span<TransparentStringView> base_commands = {}) const {
656 auto writer = di::StringWriter {};
657 write_help(writer, base_commands);
658 return di::move(writer).output();
659 }
660
661 constexpr auto app_name() const -> TransparentStringView { return m_app_name; }
662 constexpr auto app_description() const -> StringView { return m_description; }
663 constexpr auto has_subcommands() const -> bool { return !m_subcommands.empty(); }
664 constexpr auto has_arguments() const -> bool { return !m_arguments.empty(); }
665
666 private:
667 constexpr static auto closest_match(TransparentStringView name, Span<TransparentStringView> possibilities)
668 -> Optional<TransparentStringView> {
669 if (name.empty()) {
670 return {};
671 }
672
673 auto result = ""_tsv;
674 auto score = NumericLimits<i32>::max;
675 for (auto possibility : possibilities) {
676 if (possibility.empty()) {
677 continue;
678 }
679
680 auto dp = di::Vector<di::Vector<i32>> {};
681 for (auto _ : range(name.size() + 1)) {
682 dp.push_back(repeat(NumericLimits<i32>::max, possibility.size() + 1) | di::to<Vector>());
683 }
684
685 for (auto x : range(dp.size())) {
686 dp[x][0] = i32(x);
687 }
688 for (auto x : range(dp[0].size())) {
689 dp[0][x] = i32(x);
690 }
691
692 for (auto i : range(1zu, dp.size())) {
693 for (auto j : range(1zu, dp[i].size())) {
694 dp[i][j] = di::min({
695 1 + dp[i - 1][j],
696 1 + dp[i][j - 1],
697 i32(name[i] != possibility[j]) + dp[i - 1][j - 1],
698 });
699 }
700 }
701
702 auto new_score = dp.back().value().back().value();
703 if (new_score < score) {
704 result = possibility;
705 score = new_score;
706 }
707 }
708 if (score <= i32(name.size()) / 2 + 1) {
709 return result;
710 }
711 return {};
712 }
713
714 constexpr auto closest_option_match(TransparentStringView name) const -> Optional<TransparentStringView> {
715 auto possibilities = m_options | transform(&Option::long_name) |
716 filter(&Optional<TransparentStringView>::has_value) | transform(dereference) |
718 return closest_match(name, possibilities.span());
719 }
720
721 constexpr auto closest_subcommand_match(TransparentStringView name) const -> Optional<TransparentStringView> {
722 auto possibilities = m_subcommands | transform(&Subcommand::name) | di::to<Vector>();
723 return closest_match(name, possibilities.span());
724 }
725
726 constexpr auto option_required(usize index) const -> bool { return m_options[index].required(); }
727 constexpr auto option_boolean(usize index) const -> bool { return m_options[index].boolean(); }
728 constexpr auto option_is_help(usize index) const -> bool { return m_options[index].is_help(); }
729
730 constexpr auto option_parse(usize index, Span<bool> seen_arguments, Base* output,
731 Optional<TransparentStringView> input, bool is_long, bool use_colors) const
732 -> Result<void> {
733 DI_TRY(m_options[index].parse(output, input).transform_error([&](auto error) -> di::Error {
734 return Error(
735 ParseOptionError {
736 .short_name = !is_long ? m_options[index].short_name().value_or('\0') : '\0',
737 .long_name = is_long ? m_options[index].long_name().value_or(""_tsv) : ""_tsv,
738 .bad_value = input.value_or(""_tsv),
739 .is_long = is_long,
740 .parse_error = di::move(error),
741 },
742 use_colors);
743 }));
744 seen_arguments[index] = true;
745 return {};
746 }
747
748 constexpr auto argument_variadic(usize index) const -> bool { return m_arguments[index].variadic(); }
749
750 constexpr auto argument_parse(usize index, Base* output, Span<TransparentStringView> input,
751 bool use_colors) const -> Result<void> {
752 return m_arguments[index].parse(output, input).transform_error([&](auto error) -> di::Error {
753 return Error(
754 ParseArgumentError {
755 .argument_name = m_arguments[index].argument_name(),
756 .bad_values = input | di::to<Vector>(),
757 .parse_error = di::move(error),
758 },
759 use_colors);
760 });
761 }
762
763 constexpr auto minimum_required_argument_count() const -> usize {
764 return di::sum(m_arguments | di::transform(&Argument::required_argument_count));
765 }
766
767 constexpr auto argument_count() const -> usize { return m_arguments.size(); }
768
769 constexpr auto subcommand_parse(usize index, Base* output, Span<TransparentStringView> input,
770 AnyRef<Writer> writer, Span<TransparentStringView> base_commands) const
771 -> Result<> {
772 auto new_base_commands = base_commands | di::to<Vector>();
773 new_base_commands.push_back(m_app_name);
774 return m_subcommands[index]
775 .parse(output, input, writer, new_base_commands.span())
776 .transform_error([&](auto error) -> di::Error {
777 if (error.success()) {
778 return error;
779 }
780 return Error(
781 ParseSubcommandError {
782 .subcommand_name = m_subcommands[index].name(),
783 .parse_error = di::move(error),
784 },
785 io::interactive_device(writer));
786 });
787 }
788
789 constexpr auto lookup_short_name(char short_name) const -> Optional<usize> {
790 auto const* it = di::find(m_options, short_name, &Option::short_name);
791 return lift_bool(it != m_options.end()) % [&] {
792 return usize(it - m_options.begin());
793 };
794 }
795
796 constexpr auto lookup_long_name(TransparentStringView long_name) const -> Optional<usize> {
797 auto const* it = di::find(m_options, long_name, &Option::long_name);
798 return lift_bool(it != m_options.end()) % [&] {
799 return usize(it - m_options.begin());
800 };
801 }
802
803 constexpr auto lookup_subcommand(TransparentStringView subcommand_name) const -> Optional<usize> {
804 auto const* it = di::find(m_subcommands, subcommand_name, &Subcommand::name);
805 return lift_bool(it != m_subcommands.end()) % [&] {
806 return usize(it - m_subcommands.begin());
807 };
808 }
809
810 TransparentStringView m_app_name;
811 StringView m_description;
812 StaticVector<Option, Constexpr<max_options>> m_options;
813 StaticVector<Argument, Constexpr<max_arguments>> m_arguments;
814 StaticVector<Subcommand, Constexpr<max_subcommands>> m_subcommands;
815 bool m_subcommand_is_required { false };
816 };
817}
818
819template<concepts::Object T>
820constexpr auto cli_parser(TransparentStringView app_name, StringView description) {
821 return detail::Parser<T> { app_name, description };
822}
823}
824
825namespace di {
826using cli::cli_parser;
827}
#define DI_ASSERT(...)
Definition assert_bool.h:7
constexpr auto empty() const -> bool
Definition constant_string_interface.h:65
#define DI_TRY(...)
Definition monad_try.h:13
Any< Interface, RefStorage > AnyRef
Definition any_ref.h:7
constexpr auto value_completions(ValueType value_type, Span< Tuple< String, StringView > > values) -> String
Definition bash.h:39
constexpr auto escape_value(StringView input) -> String
Definition bash.h:12
constexpr auto completion_value_map(Span< Tuple< String, StringView > > input) -> String
Definition zsh.h:125
Definition argument.h:13
Shell
Definition parser.h:41
@ Zsh
Definition parser.h:43
@ Bash
Definition parser.h:42
constexpr auto cli_parser(TransparentStringView app_name, StringView description)
Definition parser.h:820
constexpr auto first(concepts::detail::ConstantVector auto &vector, size_t count)
Definition vector_first.h:13
constexpr auto join_with
Definition join_with.h:35
constexpr auto filter
Definition filter.h:33
constexpr auto repeat
Definition repeat.h:35
constexpr auto single
Definition single.h:23
constexpr auto concat
Definition concat.h:35
constexpr auto values
Definition values.h:6
constexpr auto range
Definition range.h:22
string::StringViewImpl< string::Utf8Encoding > StringView
Definition string_view.h:12
string::StringImpl< string::Utf8Encoding > String
Definition string.h:11
constexpr auto transform
Definition transform.h:59
constexpr auto rotate
Definition rotate.h:94
string::StringViewImpl< string::TransparentEncoding > TransparentStringView
Definition string_view.h:13
constexpr auto value
Definition value.h:34
constexpr auto interactive_device
Definition writer.h:83
constexpr auto writer_print
Definition writer_print.h:21
meta::List< WriteSome, Flush, InteractiveDevice > Writer
Definition writer.h:85
constexpr auto writer_println
Definition writer_println.h:22
Type< detail::MemberPointerValueHelper< RemoveCV< T > > > MemberPointerValue
Definition language.h:184
T::Type Type
Definition core.h:26
Type< detail::MemberPointerClassHelper< RemoveCV< T > > > MemberPointerClass
Definition language.h:195
ssize_t isize
Definition integers.h:34
size_t usize
Definition integers.h:33
__INT32_TYPE__ i32
Definition integers.h:16
__UINT32_TYPE__ u32
Definition integers.h:11
di::meta::Decay< decltype(T)> Tag
Definition tag_invoke.h:28
Expected< T, Error > Result
Definition result.h:8
StatusCode< Erased< long > > Error
Definition error.h:8
Unexpected(E &&) -> Unexpected< meta::UnwrapRefDecay< E > >
Definition any_storable.h:9
constexpr auto min
Definition min.h:49
constexpr tag_invoke_detail::TagInvokeFn tag_invoke
Definition tag_invoke.h:22
constexpr auto find
Definition find.h:35
constexpr auto enumerator
Definition enumerator.h:41
constexpr auto format
Definition format.h:7
constexpr auto to(Con &&container, Args &&... args)
Definition to.h:25
constexpr auto to_string
Definition to_string.h:14
constexpr auto c_
A value of type Constexpr<val>.
Definition constexpr.h:252
constexpr auto in_place_type
Definition in_place_type.h:12
constexpr auto not_fn(F &&function)
Definition not_fn.h:55
constexpr auto size
Definition size.h:62
constexpr auto equal
Definition equal.h:23
constexpr auto construct
Definition construct.h:18
constexpr auto make_enumerators
Definition enumerator.h:84
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 dereference
Definition dereference.h:16
constexpr auto parse
Definition parse.h:23
Definition in_place_type.h:5