cli20 0.1.2
A C++20 typed-schema command-line parser
Loading...
Searching...
No Matches
help.hh
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <iterator>
5#include <optional>
6#include <sstream>
7#include <string>
8#include <string_view>
9#include <tuple>
10#include <type_traits>
11#include <utility>
12#include <vector>
13
14#include "cli/argument.hh"
15#include "cli/color.hh"
16#include "cli/meta.hh"
17
18namespace cli {
19
20namespace detail {
21
30template <class T>
31struct MetavarName {
32 static constexpr std::string_view value = "value";
33};
34
36template <>
37struct MetavarName<std::string> {
38 static constexpr std::string_view value = "string";
39};
40
42template <>
43struct MetavarName<bool> {
44 static constexpr std::string_view value = "bool";
45};
46
48template <>
49struct MetavarName<float> {
50 static constexpr std::string_view value = "float";
51};
52
54template <>
55struct MetavarName<double> {
56 static constexpr std::string_view value = "double";
57};
58
60template <>
61struct MetavarName<std::filesystem::path> {
62 static constexpr std::string_view value = "path";
63};
64
66template <std::signed_integral T>
67struct MetavarName<T> {
68 static constexpr std::string_view value = "int";
69};
70
72template <std::unsigned_integral T>
73struct MetavarName<T> {
74 static constexpr std::string_view value = "uint";
75};
76
87template <class T>
88struct UnwrapStorage {
89 using type = std::remove_cvref_t<T>;
90 static constexpr bool variadic = false;
91};
92
94template <class T>
95struct UnwrapStorage<std::optional<T>> {
96 using type = std::remove_cvref_t<T>;
97 static constexpr bool variadic = false;
98};
99
101template <class T>
102struct UnwrapStorage<std::vector<T>> {
103 using type = std::remove_cvref_t<T>;
104 static constexpr bool variadic = true;
105};
106
108template <class T>
109using unwrap_storage_t = typename UnwrapStorage<std::remove_cvref_t<T>>::type;
110
121template <class T>
122[[nodiscard]]
123auto metavar_for() -> std::string {
124 using Storage = std::remove_cvref_t<T>;
125 using Base = unwrap_storage_t<Storage>;
126 std::string out = "<";
127 out += MetavarName<Base>::value;
128 out += ">";
129 if constexpr (UnwrapStorage<Storage>::variadic) {
130 out.insert(out.size() - 1, "...");
131 }
132 return out;
133}
134
145template <class T>
146[[nodiscard]]
147auto command_usage_suffix(T& value) -> std::string {
148 std::string out;
149 bool has_options = false;
150 bool has_positionals = false;
151 bool has_commands = false;
152
153 std::apply(
154 [&](auto&... fields) -> auto {
155 (
156 [&]() -> auto {
157 using F = std::remove_cvref_t<decltype(fields)>;
158 if constexpr (std::derived_from<F, OptionTag>) {
159 if (!fields.hidden()) has_options = true;
160 } else if constexpr (std::derived_from<F, PositionalTag>) {
161 if (!fields.hidden()) has_positionals = true;
162 } else if constexpr (std::derived_from<F, CommandTag>) {
163 if (!fields.hidden()) has_commands = true;
164 }
165 }(),
166 ...);
167 },
168 as_tuple(value));
169
170 if (has_options) {
171 out += " [options]";
172 }
173 if (has_positionals) {
174 out += " [args]";
175 }
176 if (has_commands) {
177 out += " [command]";
178 }
179 return out;
180}
181
194template <class T>
195[[nodiscard]]
196auto field_usage_label() -> std::string {
197 using F = std::remove_cvref_t<T>;
198 if constexpr (std::derived_from<F, OptionTag>) {
199 std::string out;
200 if constexpr (F::short_name != '\0') {
201 out += "-";
202 out += F::short_name;
203 out += ", ";
204 }
205 out += "--";
206 out += F::name.view();
207 if constexpr (!(F::nargs.min == 0 && F::nargs.max == 0)) {
208 out += " ";
209 out += metavar_for<typename F::value_type>();
210 }
211 return out;
212 } else if constexpr (std::derived_from<F, PositionalTag>) {
213 return metavar_for<typename F::value_type>();
214 } else if constexpr (std::derived_from<F, CommandTag>) {
215 return std::string(F::command_name());
216 } else {
217 return std::string{};
218 }
219}
220
221template <class T>
222[[nodiscard]]
223auto styled_field_usage_label(const AnsiStyle& style) -> std::string {
224 using F = std::remove_cvref_t<T>;
225 const auto reset = style.reset();
226 if constexpr (std::derived_from<F, OptionTag>) {
227 std::string out;
228 if constexpr (F::short_name != '\0') {
229 out += style.option();
230 out += "-";
231 out += F::short_name;
232 out += reset;
233 out += ", ";
234 }
235 out += style.option();
236 out += "--";
237 out += F::name.view();
238 out += reset;
239 if constexpr (!(F::nargs.min == 0 && F::nargs.max == 0)) {
240 out += " ";
241 out += style.metavar();
242 out += metavar_for<typename F::value_type>();
243 out += reset;
244 }
245 return out;
246 } else if constexpr (std::derived_from<F, PositionalTag>) {
247 std::string out = std::string(style.metavar());
248 out += metavar_for<typename F::value_type>();
249 out += reset;
250 return out;
251 } else if constexpr (std::derived_from<F, CommandTag>) {
252 std::string out = std::string(style.command());
253 out += F::command_name();
254 out += reset;
255 return out;
256 } else {
257 return std::string{};
258 }
259}
260
270inline auto split_lines(std::string_view text) -> std::vector<std::string> {
271 std::vector<std::string> lines;
272 std::size_t start = 0;
273 while (start <= text.size()) {
274 const auto end = text.find('\n', start);
275 if (end == std::string_view::npos) {
276 lines.emplace_back(text.substr(start));
277 break;
278 }
279 lines.emplace_back(text.substr(start, end - start));
280 start = end + 1;
281 }
282 if (lines.empty()) {
283 lines.emplace_back();
284 }
285 return lines;
286}
287
299inline auto append_wrapped_description(std::string& out,
300 std::string_view description,
301 std::size_t indent) -> void {
302 if (description.empty()) {
303 out += '\n';
304 return;
305 }
306
307 auto lines = split_lines(description);
308 out += lines.front();
309 out += '\n';
310 for (std::size_t i = 1; i < lines.size(); ++i) {
311 out.append(indent, ' ');
312 out += lines[i];
313 out += '\n';
314 }
315}
316
336template <class T, class Pred, class Render>
337auto append_section(std::string& out, std::string_view title,
338 std::string_view heading_color, std::string_view reset_color,
339 T& value, Pred pred, Render render) -> void {
340 std::vector<std::string> rows;
341 std::size_t width = 0;
342
343 std::apply(
344 [&](auto&... fields) -> auto {
345 (
346 [&]() -> auto {
347 using F = std::remove_cvref_t<decltype(fields)>;
348 if constexpr (pred.template operator()<F>()) {
349 const auto label = field_usage_label<F>();
350 width = std::max(width, label.size());
351 rows.push_back(render(fields, label));
352 }
353 }(),
354 ...);
355 },
356 as_tuple(value));
357
358 if (rows.empty()) {
359 return;
360 }
361
362 out += '\n';
363 out += heading_color;
364 out += title;
365 out += reset_color;
366 out += '\n';
367 for (auto& row : rows) {
368 out += row;
369 }
370}
371
388template <class Field>
389consteval auto relation_field_name() -> std::string_view {
390 using F = std::remove_cvref_t<Field>;
391 if constexpr (std::derived_from<F, OptionTag> ||
392 std::derived_from<F, CommandTag>) {
393 return F::name.view();
394 } else {
395 return {};
396 }
397}
398
399inline auto relation_name_list(NameList<> names) -> std::string {
400 std::string out;
401 for (std::size_t i = 0; i < names.size; ++i) {
402 if (i != 0) out += ", ";
403 out += "--";
404 out += names.values[i];
405 }
406 return out;
407}
408
409template <class T>
410constexpr auto relation_group(std::string_view name) -> NameList<> {
411 NameList<> result;
412 if constexpr (detail::HasRelations<T>) {
413 std::apply(
414 [&](auto... rels) -> auto {
415 (
416 [&]() -> auto {
417 using R = std::remove_cvref_t<decltype(rels)>;
418 if constexpr (std::same_as<R, GroupRelation>) {
419 if (rels.name == name) result = rels.members;
420 }
421 }(),
422 ...);
423 },
424 T::relations.items);
425 }
426 return result;
427}
428
429template <class T>
430constexpr auto relation_operand_contains(std::string_view operand,
431 std::string_view field_name) -> bool {
432 if (operand == field_name) return true;
433 const auto group = relation_group<T>(operand);
434 for (auto member : group) {
435 if (member == field_name) return true;
436 }
437 return false;
438}
439
440template <class T>
441auto relation_operand_label(std::string_view operand) -> std::string {
442 if constexpr (detail::HasRelations<T>) {
443 if (!relation_group<T>(operand).empty()) {
444 return std::string(operand);
445 }
446 }
447 return "--" + std::string(operand);
448}
449
450template <class T, class Field>
451auto relation_help_metadata() -> std::vector<std::string> {
452 std::vector<std::string> metadata;
453 constexpr auto field_name = relation_field_name<Field>();
454 if constexpr (field_name.empty()) {
455 return metadata;
456 } else if constexpr (detail::HasRelations<T>) {
457 std::apply(
458 [&](auto... rels) -> auto {
459 (
460 [&]() -> auto {
461 using R = std::remove_cvref_t<decltype(rels)>;
462 if constexpr (std::same_as<R, GroupRelation>) {
463 (void)field_name;
464 } else if constexpr (std::same_as<R, ConflictsRelation>) {
465 if (relation_operand_contains<T>(rels.left, field_name)) {
466 metadata.emplace_back("conflicts: " +
467 relation_operand_label<T>(rels.right));
468 }
469 if (relation_operand_contains<T>(rels.right, field_name)) {
470 metadata.emplace_back("conflicts: " +
471 relation_operand_label<T>(rels.left));
472 }
473 } else if constexpr (std::same_as<R, DependsOnRelation>) {
474 if (relation_operand_contains<T>(rels.source, field_name)) {
475 metadata.emplace_back(
476 "requires: " + relation_operand_label<T>(rels.target));
477 }
478 if (relation_operand_contains<T>(rels.target, field_name)) {
479 metadata.emplace_back(
480 "required by: " +
481 relation_operand_label<T>(rels.source));
482 }
483 }
484 }(),
485 ...);
486 },
487 T::relations.items);
488 return metadata;
489 } else {
490 return metadata;
491 }
492}
493
494template <class T, class Field>
495auto help_metadata(Field& field) -> std::vector<std::string> {
496 std::vector<std::string> metadata;
497 if constexpr (std::derived_from<std::remove_cvref_t<Field>, OptionTag> ||
498 std::derived_from<std::remove_cvref_t<Field>, PositionalTag>) {
499 if (field.presence() == Presence::required) {
500 metadata.emplace_back("required");
501 }
502 if (!field.env().empty()) {
503 metadata.emplace_back("env: " + std::string(field.env()));
504 }
505 if (const auto& default_value = field.default_value()) {
506 using Value = std::remove_cvref_t<decltype(*default_value)>;
507 if constexpr (requires(std::ostream& os, const Value& value) {
508 os << value;
509 }) {
510 std::ostringstream out;
511 out << *default_value;
512 metadata.emplace_back("default: " + out.str());
513 }
514 }
515 }
516 if (!field.deprecated().empty()) {
517 metadata.emplace_back("deprecated: " + std::string(field.deprecated()));
518 }
519 auto relation_metadata = relation_help_metadata<T, Field>();
520 metadata.insert(metadata.end(),
521 std::make_move_iterator(relation_metadata.begin()),
522 std::make_move_iterator(relation_metadata.end()));
523 return metadata;
524}
525
526inline auto append_help_metadata(std::string& out,
527 const std::vector<std::string>& metadata,
528 const AnsiStyle& style) -> void {
529 if (metadata.empty()) {
530 return;
531 }
532 out += style.metadata();
533 out += " [";
534 for (std::size_t i = 0; i < metadata.size(); ++i) {
535 if (i != 0) {
536 out += "] [";
537 }
538 out += metadata[i];
539 }
540 out += "]";
541 out += style.reset();
542}
543
544inline auto append_row_description(std::string& out,
545 std::string_view description,
546 const std::vector<std::string>& metadata,
547 const AnsiStyle& style, std::size_t indent)
548 -> void {
549 auto lines = split_lines(description);
550 out += lines.front();
551 append_help_metadata(out, metadata, style);
552 out += '\n';
553 for (std::size_t i = 1; i < lines.size(); ++i) {
554 out.append(indent, ' ');
555 out += lines[i];
556 out += '\n';
557 }
558}
559
560template <class T>
561auto append_group_requirements(std::string& row, std::string_view group_name)
562 -> void {
563 if constexpr (detail::HasRelations<T>) {
564 std::apply(
565 [&](auto... rels) -> auto {
566 (
567 [&]() -> auto {
568 using R = std::remove_cvref_t<decltype(rels)>;
569 if constexpr (std::same_as<R, DependsOnRelation>) {
570 if (rels.source == group_name) {
571 row += " requires: ";
572 row += relation_operand_label<T>(rels.target);
573 row += "\n";
574 }
575 }
576 }(),
577 ...);
578 },
579 T::relations.items);
580 }
581}
582
583template <class T>
584auto relation_option_group_row(const GroupRelation& group,
585 const AnsiStyle& style) -> std::string {
586 std::string row = " ";
587 row += style.group();
588 row += group.name;
589 row += style.reset();
590 row += ":\n ";
591 row += relation_name_list(group.members);
592 row += " must be used together\n";
593 append_group_requirements<T>(row, group.name);
594 return row;
595}
596
597template <class T>
598auto relation_option_group_rows(const AnsiStyle& style)
599 -> std::vector<std::string> {
600 std::vector<std::string> rows;
601 if constexpr (detail::HasRelations<T>) {
602 std::apply(
603 [&](auto... rels) -> auto {
604 (
605 [&]() -> auto {
606 using R = std::remove_cvref_t<decltype(rels)>;
607 if constexpr (std::same_as<R, GroupRelation>) {
608 rows.push_back(relation_option_group_row<T>(rels, style));
609 }
610 }(),
611 ...);
612 },
613 T::relations.items);
614 }
615 return rows;
616}
617
618template <class T, class Field>
619auto render_help_row(Field& field, std::string_view label,
620 std::string_view styled_label, std::size_t width,
621 const AnsiStyle& style) -> std::string {
622 std::string out = " ";
623 out += styled_label;
624 const auto metadata = help_metadata<T>(field);
625 if (!field.help_text().empty() || !metadata.empty()) {
626 out.append(width - label.size() + 2, ' ');
627 append_row_description(out, field.help_text(), metadata, style, width + 4);
628 } else {
629 out += '\n';
630 }
631 return out;
632}
633
645template <class T>
646auto find_description(T& value) -> std::string_view {
647 std::string_view description;
648 std::apply(
649 [&](auto&... fields) -> auto {
650 (
651 [&]() -> auto {
652 using F = std::remove_cvref_t<decltype(fields)>;
653 if constexpr (std::derived_from<F, DescriptionTag>) {
654 if (description.empty()) {
655 description = fields;
656 }
657 }
658 }(),
659 ...);
660 },
661 as_tuple(value));
662 return description;
663}
664
687template <class T>
688auto format_help_impl(T& value, std::string_view program_name,
689 ColorMode color_mode, bool recurse,
690 std::string_view command_path,
691 HelpPalette palette = default_help_palette)
692 -> std::string {
693 const auto style = resolve_color(color_mode, palette);
694 const std::string heading = std::string(style.heading());
695 const std::string reset = std::string(style.reset());
696
697 std::string full_program = std::string(program_name);
698 if (!command_path.empty()) {
699 full_program += " ";
700 full_program += command_path;
701 }
702
703 std::string out = std::string(style.usage());
704 out += "Usage:";
705 out += reset;
706 out += " ";
707 out += full_program;
708 out += command_usage_suffix(value);
709 out += "\n";
710
711 if (const auto description = find_description(value); !description.empty()) {
712 out += '\n';
713 out += description;
714 out += '\n';
715 }
716
717 std::vector<std::pair<std::string, std::string>> option_rows;
718 std::vector<std::pair<std::string, std::string>> positional_rows;
719 std::vector<std::pair<std::string, std::string>> command_rows;
720 std::size_t option_width = 0;
721 std::size_t positional_width = 0;
722 std::size_t command_width = 0;
723
724 std::apply(
725 [&](auto&... fields) -> auto {
726 (
727 [&]() -> auto {
728 using F = std::remove_cvref_t<decltype(fields)>;
729 if constexpr (std::derived_from<F, OptionTag>) {
730 if (fields.hidden()) return;
731 auto label = field_usage_label<F>();
732 option_width = std::max(option_width, label.size());
733 option_rows.emplace_back(std::move(label), std::string{});
734 } else if constexpr (std::derived_from<F, PositionalTag>) {
735 if (fields.hidden()) return;
736 auto label = field_usage_label<F>();
737 positional_width = std::max(positional_width, label.size());
738 positional_rows.emplace_back(std::move(label), std::string{});
739 } else if constexpr (std::derived_from<F, CommandTag>) {
740 if (fields.hidden()) return;
741 auto label = field_usage_label<F>();
742 command_width = std::max(command_width, label.size());
743 command_rows.emplace_back(std::move(label), std::string{});
744 }
745 }(),
746 ...);
747 },
748 as_tuple(value));
749
750 auto append_rows =
751 [&](std::string_view title,
752 const std::vector<std::pair<std::string, std::string>>& rows) -> auto {
753 if (rows.empty()) {
754 return;
755 }
756 out += '\n';
757 out += heading;
758 out += title;
759 out += reset;
760 out += '\n';
761 for (const auto& [label, row] : rows) {
762 (void)label;
763 out += row;
764 }
765 };
766
767 option_rows.clear();
768 positional_rows.clear();
769 command_rows.clear();
770 std::apply(
771 [&](auto&... fields) -> auto {
772 (
773 [&]() -> auto {
774 using F = std::remove_cvref_t<decltype(fields)>;
775 if constexpr (std::derived_from<F, OptionTag>) {
776 if (fields.hidden()) return;
777 auto label = field_usage_label<F>();
778 auto styled_label = styled_field_usage_label<F>(style);
779 option_rows.emplace_back(
780 label, render_help_row<T>(fields, label, styled_label,
781 option_width, style));
782 } else if constexpr (std::derived_from<F, PositionalTag>) {
783 if (fields.hidden()) return;
784 auto label = field_usage_label<F>();
785 auto styled_label = styled_field_usage_label<F>(style);
786 positional_rows.emplace_back(
787 label, render_help_row<T>(fields, label, styled_label,
788 positional_width, style));
789 } else if constexpr (std::derived_from<F, CommandTag>) {
790 if (fields.hidden()) return;
791 auto label = field_usage_label<F>();
792 auto styled_label = styled_field_usage_label<F>(style);
793 command_rows.emplace_back(
794 label, render_help_row<T>(fields, label, styled_label,
795 command_width, style));
796 }
797 }(),
798 ...);
799 },
800 as_tuple(value));
801
802 append_rows("Options:", option_rows);
803 append_rows("Positional arguments:", positional_rows);
804 append_rows("Commands:", command_rows);
805
806 const auto option_group_rows = relation_option_group_rows<T>(style);
807 if (!option_group_rows.empty()) {
808 out += '\n';
809 out += heading;
810 out += "Option groups:";
811 out += reset;
812 out += '\n';
813 for (const auto& row : option_group_rows) {
814 out += row;
815 }
816 }
817
818 if (recurse && !command_rows.empty()) {
819 std::apply(
820 [&](auto&... fields) -> auto {
821 (
822 [&]() -> auto {
823 using F = std::remove_cvref_t<decltype(fields)>;
824 if constexpr (std::derived_from<F, CommandTag>) {
825 if (fields.hidden()) return;
826 out += '\n';
827 out += format_help_impl<typename F::argument_type>(
828 static_cast<typename F::argument_type&>(fields),
829 program_name, color_mode, true, F::command_name(),
830 palette);
831 }
832 }(),
833 ...);
834 },
835 as_tuple(value));
836 }
837
838 return out;
839}
840
841} // namespace detail
842
858template <ArgumentSpec T>
859auto format_help(T& value, std::string_view program_name = "program",
860 ColorMode color_mode = ColorMode::detect, bool recurse = false,
861 HelpPalette palette = default_help_palette) -> std::string {
862 return detail::format_help_impl<T>(value, program_name, color_mode, recurse,
863 {}, palette);
864}
865
866} // namespace cli
constexpr auto string
Definition action.hh:978
Definition action.hh:29
constexpr HelpPalette default_help_palette
Definition color.hh:39
auto format_help(T &value, std::string_view program_name="program", ColorMode color_mode=ColorMode::detect, bool recurse=false, HelpPalette palette=default_help_palette) -> std::string
Generates a complete help string for an argument specification.
Definition help.hh:859
constexpr auto optional
Convenience constant for Presence::optional.
constexpr auto group(GroupParams params) -> GroupRelation
Definition relation.hh:67
constexpr auto as_tuple(T &t)
Converts an aggregate value to a std::tuple of references to its members.
Definition meta.hh:125
ColorMode
Controls whether ANSI escape codes are emitted by format_help().
Definition color.hh:20
ANSI palette used by generated help output.
Definition color.hh:28