53 template<concepts::Object Base>
56 constexpr static auto max_options = 100ZU;
57 constexpr static auto max_arguments = 100ZU;
58 constexpr static auto max_subcommands = 100ZU;
62 : m_app_name(app_name), m_description(description) {}
65 requires(concepts::MemberObjectPointer<
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) && {
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);
76 constexpr auto help(Optional<char> short_name = {}, Optional<TransparentStringView> long_name =
"help"_tsv,
77 StringView description =
"Print help message"_sv) {
80 "A help message requires the argument type to have a boolean help member.");
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, {}, {},
88 requires(concepts::MemberObjectPointer<
decltype(member)> &&
91 Optional<ValueType> value_type = {}) && {
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);
99 requires(concepts::MemberObjectPointer<
decltype(member)> &&
102 constexpr auto subcommands() && {
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> }));
109 m_subcommand_is_required =
true;
113 auto do_subcommands = [&]<
typename... Types>(InPlaceType<Variant<Types...>>) {
118 return di::move(*
this);
121 template<Impl<io::Writer> Writer>
123 constexpr auto parse(Span<TransparentStringView> args,
Writer& writer,
124 Span<TransparentStringView> base_commands = {}) ->
Result<Base> {
125 using namespace di::string_literals;
130 args = *args.subspan(1);
133 auto seen_options = Array<bool, max_options> {};
134 seen_options.fill(
false);
136 auto count_option_processed =
usize { 0 };
137 auto subcommand_seen =
false;
139 auto send_arg_to_back = [&](
usize i) {
141 count_option_processed++;
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];
150 if (!arg.starts_with(
'-')) {
151 if (!has_subcommands()) {
156 auto subcommand_index = lookup_subcommand(arg);
157 if (!subcommand_index) {
158 return Unexpected(
Error(UnknownSubcommand(arg, closest_subcommand_match(arg)), use_colors));
162 subcommand_index.value(), &result,
163 args.subspan(arg_index, args.size() - arg_index - count_option_processed).value(), writer,
165 count_option_processed = args.size();
166 subcommand_seen =
true;
171 if (arg ==
"--"_tsv) {
172 send_arg_to_back(arg_index);
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]);
181 return Unexpected(
Error(UnknownShortOption(arg[char_index]), use_colors));
184 if (option_is_help(*index)) {
185 write_help(writer, base_commands);
190 if (option_boolean(*index)) {
191 DI_TRY(option_parse(*index, seen_options, &result, {},
false, use_colors));
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));
203 if (i + 1 >= args.size()) {
204 return Unexpected(
Error(ShortOptionMissingRequiredValue(arg[char_index]), use_colors));
208 DI_TRY(option_parse(*index, seen_options, &result, args[arg_index + 1],
false, use_colors));
209 send_arg_to_back(arg_index);
212 send_arg_to_back(arg_index);
217 auto equal = arg.find(
'=');
220 name = arg.substr(arg.begin() + 2);
222 name = arg.substr(arg.begin() + 2,
equal.begin());
225 auto index = lookup_long_name(name);
227 return Unexpected(
Error(UnknownLongOption(name, closest_option_match(name)), use_colors));
230 if (option_is_help(*index)) {
231 write_help(writer, base_commands);
235 if (option_boolean(*index)) {
236 auto value = Optional<TransparentStringView> {};
241 DI_TRY(option_parse(*index, seen_options, &result, value,
true, use_colors));
242 send_arg_to_back(arg_index);
247 if (!
equal && i + 1 >= args.size()) {
248 return Unexpected(
Error(LongOptionMissingRequiredValue(name), use_colors));
251 value = args[arg_index + 1];
252 send_arg_to_back(arg_index);
253 send_arg_to_back(arg_index);
257 send_arg_to_back(arg_index);
260 DI_TRY(option_parse(*index, seen_options, &result, value,
true, use_colors));
264 for (
usize i = 0; i < m_options.size(); i++) {
265 if (!seen_options[i] && option_required(i)) {
267 Error(MissingRequiredOption(m_options[i].short_name(), m_options[i].long_name()), use_colors));
272 if (has_subcommands() && m_subcommand_is_required && !subcommand_seen) {
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()),
286 available_arguments -= argument.required_argument_count();
290 auto argument_index =
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;
298 if (i < positional_arguments.size()) {
300 Error(ExtraArguments(positional_arguments.subspan(i).value() |
di::to<Vector>()), use_colors));
305 template<Impl<io::Writer> Writer>
307 constexpr void write_help(
Writer& writer, Span<TransparentStringView> base_commands = {})
const {
308 using Enc = container::string::Utf8Encoding;
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;
317 auto write_values = [&](Vector<Tuple<String, StringView>>
values) {
321 for (
auto const& [value, description] : values) {
323 description.empty() ?
""_sv :
": "_sv, description);
337 for (
auto const& option : m_options) {
338 if (option.required()) {
341 if (!option.boolean()) {
343 di::Styled(
format(
"<{}>"_sv, option.value_name()), option_value_effect));
347 for (
auto const& argument : m_arguments) {
350 if (has_subcommands()) {
351 auto display_name = m_subcommand_is_required ?
"COMMAND"_sv :
"[COMMAND]"_sv;
356 if (has_subcommands()) {
359 for (
auto const& subcommand : m_subcommands) {
361 subcommand.description(),
362 first && !m_subcommand_is_required ?
" (default)"_sv :
""_sv);
367 if (!m_arguments.empty()) {
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);
377 di::Styled(argument.display_name(), argument_effect),
378 argument.description(), default_value);
379 write_values(argument.values());
383 if (!m_options.empty()) {
385 for (
auto const& option : m_options) {
387 if (option.short_name()) {
389 if (!option.boolean() && !option.long_name()) {
392 di::Styled(
format(
"<{}>"_sv, option.value_name()), option_value_effect));
395 if (option.short_name() && option.long_name()) {
398 if (option.long_name()) {
400 if (!option.boolean()) {
403 di::Styled(
format(
"<{}>"_sv, option.value_name()), option_value_effect));
406 auto default_value = option.default_value();
407 if (!default_value.empty() && !option.required()) {
408 default_value =
format(
" (default: {})"_sv, default_value);
413 write_values(option.values());
418 constexpr void bash_completions_inner(TreeMap<Vector<TransparentString>,
String>& states,
419 Span<TransparentStringView> base_commands = {}) {
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()));
426 if (option.long_name()) {
427 possible_values.push_back(
to_string(option.long_display_name()));
430 for (
auto const& subcommand : m_subcommands) {
432 new_base_commands.push_back(m_app_name);
433 subcommand.inner_bash_completions(states, new_base_commands.span());
435 possible_values.push_back(
to_string(subcommand.name()));
437 for (
auto const& argument : m_arguments) {
438 for (
auto& [value, _] : argument.values()) {
439 possible_values.push_back(di::move(value));
443 content +=
format(
" opts=\"{}\"\n"_sv,
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()) {
456 if (option.short_name()) {
457 pattern +=
to_string(option.short_display_name());
459 if (option.long_name()) {
460 if (!pattern.empty()) {
461 pattern.push_back(U
'|');
463 pattern +=
to_string(option.long_display_name());
465 content +=
format(
" {})\n"_sv, pattern);
466 auto values = option.values();
468 content +=
format(
" return 0\n"_sv);
469 content +=
format(
" ;;\n"_sv);
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);
477 auto state = Vector<TransparentString> {};
478 for (
auto part : base_commands) {
479 state.push_back(part.to_owned());
481 state.push_back(m_app_name.to_owned());
482 states.try_emplace(di::move(state), content);
485 template<Impl<io::Writer> Writer>
486 constexpr void write_bash_completions(
Writer& writer) {
487 using Enc = container::string::Utf8Encoding;
496 local i cur prev opts cmd
498 if [[ "${{BASH_VERSINFO[0]}}" -ge 4 ]]; then
501 cur="${{COMP_WORDS[COMP_CWORD]}}"
509 auto states = TreeMap<Vector<TransparentString>,
String> {};
510 bash_completions_inner(states);
512 auto state_name = [&](Span<TransparentString const> state) ->
String {
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());
533 for (
auto const& [state, logic] : states) {
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 {}
545 complete -F _{} -o bashdefault -o default {}
547 m_app_name, m_app_name, m_app_name, m_app_name);
550 template<Impl<io::Writer> Writer>
551 void write_zsh_completions_inner(
Writer& writer,
u32 indentation, Span<TransparentStringView> base_commands) {
553 auto extra_indent =
" "_sv;
555 using Enc = container::string::Utf8Encoding;
557 for (
auto const& option : m_options) {
558 for (
auto const& spec : option.zsh_completion_specs()) {
562 for (
auto const& argument : m_arguments) {
563 auto spec = argument.zsh_completion_spec();
569 if (has_subcommands()) {
571 auto possibilities = Vector<Tuple<String, StringView>> {};
572 for (
auto const& subcommand : m_subcommands) {
573 possibilities.emplace_back(
format(
"{}"_sv, subcommand.name()), subcommand.description());
580 if (has_subcommands()) {
586 indent, extra_indent, state_name);
589 for (
auto const& subcommand : m_subcommands) {
593 new_base_commands.push_back(m_app_name);
594 subcommand.write_zsh_completions_inner(writer, indentation + 12, new_base_commands.span());
604 template<Impl<io::Writer> Writer>
605 void write_zsh_completions(
Writer& writer) {
606 using Enc = container::string::Utf8Encoding;
616autoload -U is-at-least
620 typeset -a _arguments_options
623 if is-at-least 5.2; then
624 _arguments_options=(-s -S -C)
626 _arguments_options=(-s -C)
629 local context curcontext="$curcontext" state line)~"_sv,
630 m_app_name, m_app_name);
632 write_zsh_completions_inner(writer, 4, {});
636if [ "$funcstack[1]" = "_{}" ]; then
641 m_app_name, m_app_name, m_app_name, m_app_name);
644 template<Impl<io::Writer> Writer>
645 void write_completions(
Writer& writer,
Shell shell) {
649 return write_bash_completions(writer);
651 return write_zsh_completions(writer);
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();
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(); }
667 constexpr static auto closest_match(
TransparentStringView name, Span<TransparentStringView> possibilities)
668 -> Optional<TransparentStringView> {
673 auto result =
""_tsv;
674 auto score = NumericLimits<i32>::max;
675 for (
auto possibility : possibilities) {
676 if (possibility.empty()) {
680 auto dp = di::Vector<di::Vector<i32>> {};
681 for (
auto _ :
range(name.size() + 1)) {
685 for (
auto x :
range(dp.size())) {
692 for (
auto i :
range(1zu, dp.size())) {
693 for (
auto j :
range(1zu, dp[i].
size())) {
697 i32(name[i] != possibility[j]) + dp[i - 1][j - 1],
702 auto new_score = dp.back().value().back().value();
703 if (new_score < score) {
704 result = possibility;
708 if (score <=
i32(name.size()) / 2 + 1) {
714 constexpr auto closest_option_match(
TransparentStringView name)
const -> Optional<TransparentStringView> {
715 auto possibilities = m_options |
transform(&Option::long_name) |
718 return closest_match(name, possibilities.span());
721 constexpr auto closest_subcommand_match(
TransparentStringView name)
const -> Optional<TransparentStringView> {
723 return closest_match(name, possibilities.span());
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(); }
730 constexpr auto option_parse(
usize index, Span<bool> seen_arguments, Base* output,
731 Optional<TransparentStringView> input,
bool is_long,
bool use_colors)
const
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),
740 .parse_error = di::move(error),
744 seen_arguments[index] =
true;
748 constexpr auto argument_variadic(
usize index)
const ->
bool {
return m_arguments[index].variadic(); }
750 constexpr auto argument_parse(
usize index, Base* output, Span<TransparentStringView> input,
752 return m_arguments[index].parse(output, input).transform_error([&](
auto error) ->
di::Error {
755 .argument_name = m_arguments[index].argument_name(),
757 .parse_error = di::move(error),
763 constexpr auto minimum_required_argument_count() const ->
usize {
764 return di::sum(m_arguments | di::transform(&Argument::required_argument_count));
767 constexpr auto argument_count() const ->
usize {
return m_arguments.size(); }
769 constexpr auto subcommand_parse(
usize index, Base* output, Span<TransparentStringView> input,
770 AnyRef<Writer> writer, Span<TransparentStringView> base_commands)
const
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()) {
781 ParseSubcommandError {
782 .subcommand_name = m_subcommands[index].name(),
783 .parse_error = di::move(error),
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());
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());
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());
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 };
819template<concepts::Object T>
821 return detail::Parser<T> { app_name, description };
#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
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
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