cli20 0.1.2
A C++20 typed-schema command-line parser
Loading...
Searching...
No Matches
parser.hh
Go to the documentation of this file.
1#pragma once
2
3// #include <expected>
4#include <array>
5#include <cstdint>
6#include <cstdio>
7#include <cstdlib>
8#include <optional>
9#include <span>
10#include <string>
11#include <string_view>
12#include <tuple>
13#include <type_traits>
14#include <utility>
15#include <vector>
16
17#ifdef _WIN32
18#define NOMINMAX
19#define WIN32_LEAN_AND_MEAN
20#include <windows.h>
21#endif
22
23#include "cli/argument.hh"
24#include "cli/constraint.hh"
25#include "cli/error.hh"
26#include "cli/help.hh"
27
28#ifndef CLI20_TOKEN_STACK_CAPACITY
29#define CLI20_TOKEN_STACK_CAPACITY 32
30#endif
31
32static_assert(CLI20_TOKEN_STACK_CAPACITY > 0,
33 "CLI20_TOKEN_STACK_CAPACITY must be greater than zero");
34
35namespace cli {
36
45inline constexpr std::size_t token_stack_capacity = CLI20_TOKEN_STACK_CAPACITY;
46
47namespace detail {
48
49inline auto write_message(FILE* file, std::string_view message) -> void {
50 if (file == nullptr || message.empty()) return;
51 (void)std::fwrite(message.data(), 1, message.size(), file);
52 (void)std::fwrite("\n", 1, 1, file);
53}
54
55inline auto value_count_message(int required_count, int provided)
56 -> std::string {
57 return "option requires at least " + std::to_string(required_count) +
58 " value(s), but " + std::to_string(provided) + " provided";
59}
60
61inline auto inline_value_count_message(int required_count) -> std::string {
62 return "option requires at least " + std::to_string(required_count) +
63 " value(s), but inline separator provides only 1";
64}
65
66} // namespace detail
67
68#ifdef _WIN32
69namespace detail {
79inline auto wide_to_utf8(const wchar_t* wide) -> std::string {
80 if (!wide) return {};
81 constexpr DWORD flags = WC_ERR_INVALID_CHARS;
82 int len = WideCharToMultiByte(CP_UTF8, flags, wide, -1, nullptr, 0, nullptr,
83 nullptr);
84 if (len <= 1) return {};
85 std::string result(static_cast<std::size_t>(len) - 1, '\0');
86 const int written = WideCharToMultiByte(CP_UTF8, flags, wide, -1,
87 result.data(), len, nullptr, nullptr);
88 if (written != len) return {};
89 return result;
90}
91} // namespace detail
92
99struct Utf8Argv {
100 std::vector<std::string> storage;
101 std::vector<char*> pointers;
102
103 [[nodiscard]] auto size() const -> int {
104 return static_cast<int>(pointers.size());
105 }
106
107 [[nodiscard]] auto data() -> char** { return pointers.data(); }
108};
109
121inline auto utf16_to_utf8(int argc, wchar_t* argv[]) -> Utf8Argv {
122 Utf8Argv result;
123 result.storage.reserve(static_cast<std::size_t>(argc));
124 result.pointers.reserve(static_cast<std::size_t>(argc));
125 for (int i = 0; i < argc; ++i) {
126 result.storage.emplace_back(detail::wide_to_utf8(argv[i]));
127 }
128 for (std::string& arg : result.storage) {
129 result.pointers.push_back(arg.data());
130 }
131 return result;
132}
133#endif
134
144enum class TokenType : uint8_t {
149};
150
151static constexpr std::uint8_t kNoPrefix = 0xff;
152static constexpr std::uint8_t kShortPrefixBit = 0x80;
153static constexpr std::uint8_t kPrefixIndexMask = 0x7f;
154
155static constexpr auto no_prefix() -> std::uint8_t { return kNoPrefix; }
156
157static constexpr auto short_prefix() -> std::uint8_t { return kShortPrefixBit; }
158
159static constexpr auto long_prefix(std::uint8_t i) -> std::uint8_t {
160 return i & kPrefixIndexMask;
161}
162
163static constexpr auto has_prefix(std::uint8_t p) -> bool {
164 return p != kNoPrefix;
165}
166
167static constexpr auto is_short_prefix(std::uint8_t p) -> bool {
168 return p != kNoPrefix && (p & kShortPrefixBit);
169}
170
174struct Token {
175 std::string_view text;
179 std::size_t position;
180 std::uint8_t prefix; // 0xff = none, high bit = short prefix, low 7bit = long
181 // prefix index
183};
184
193 template <class T, std::size_t N>
194
195 struct SmallVector {
196 std::array<T, N> stack{};
197 std::vector<T> heap;
198 std::size_t size_value{};
199
200 auto push_back(T value) -> void {
201 if (size_value < N && heap.empty()) {
202 stack[size_value++] = std::move(value);
203 return;
204 }
205
206 if (heap.empty()) {
207 heap.reserve(N * 2);
208 for (std::size_t i = 0; i < size_value; ++i) {
209 heap.push_back(std::move(stack[i]));
210 }
211 }
212
213 heap.push_back(std::move(value));
214 ++size_value;
215 }
216
217 [[nodiscard]] auto size() const -> std::size_t { return size_value; }
218 [[nodiscard]] auto empty() const -> bool { return size_value == 0; }
219
220 auto operator[](std::size_t i) -> T& {
221 return heap.empty() ? stack[i] : heap[i];
222 }
223
224 auto operator[](std::size_t i) const -> const T& {
225 return heap.empty() ? stack[i] : heap[i];
226 }
227
228 [[nodiscard]] auto begin() const {
229 return heap.empty() ? stack.data() : heap.data();
230 }
231
232 [[nodiscard]] auto end() const { return begin() + size_value; }
233
234 [[nodiscard]] auto data() const {
235 return heap.empty() ? stack.data() : heap.data();
236 }
237 };
238
241
243 [[nodiscard]]
244 constexpr explicit operator bool() const noexcept {
245 return !error.has_error();
246 }
247
249 [[nodiscard]]
250 constexpr auto has_value() const noexcept -> bool {
251 return !error.has_error();
252 }
253
255 [[nodiscard]]
256 constexpr auto has_error() const noexcept -> bool {
257 return error.has_error();
258 }
259
272 auto fail(ErrorCode code, std::size_t pos, std::string subject,
273 std::string detail = {}) -> TokenizeResult& {
275 .code = code,
276 .kind = ErrorKind::parse,
277 .position = static_cast<int>(pos),
278 .subject = std::move(subject),
279 .detail = std::move(detail),
280 };
281 return *this;
282 }
283};
284
292 static constexpr std::size_t max_option_prefixes = 4;
293
294 private:
295 std::array<std::string_view, max_option_prefixes> option_prefixes_{"--"};
296 std::uint8_t option_prefix_count_{1};
297
298 std::string_view short_option_prefix_{"-"};
299 std::string_view end_of_options_separator_{"--"};
300 char inline_value_separator_{'='};
301
302 public:
303 constexpr TokenizerConfig() = default;
304
305 [[nodiscard]] constexpr auto option_prefixes() const
306 -> std::span<const std::string_view> {
307 return {option_prefixes_.data(), option_prefix_count_};
308 }
309
310 [[nodiscard]] constexpr auto short_option_prefix() const -> std::string_view {
311 return short_option_prefix_;
312 }
313
314 [[nodiscard]] constexpr auto end_of_options_separator() const
315 -> std::string_view {
316 return end_of_options_separator_;
317 }
318
319 [[nodiscard]] constexpr auto inline_value_separator() const -> char {
320 return inline_value_separator_;
321 }
322
323 template <StringLiteral... Prefixes>
324 constexpr auto set_option_prefixes() -> void {
325 static_assert(sizeof...(Prefixes) > 0,
326 "TokenizerConfig needs at least one option prefix");
327 static_assert(sizeof...(Prefixes) <= max_option_prefixes,
328 "Too many option prefixes for TokenizerConfig");
329 option_prefix_count_ = 0;
330 ((option_prefixes_[option_prefix_count_++] = Prefixes.view()), ...);
331 }
332
333 template <StringLiteral... Prefixes>
334 [[nodiscard]] constexpr auto with_option_prefixes() const -> TokenizerConfig {
335 auto cfg = *this;
336 cfg.template set_option_prefixes<Prefixes...>();
337 return cfg;
338 }
339
340 template <StringLiteral Prefix>
341 [[nodiscard]] constexpr auto with_short_option_prefix() const
342 -> TokenizerConfig {
343 auto cfg = *this;
344 cfg.short_option_prefix_ = Prefix.view();
345 return cfg;
346 }
347
348 template <StringLiteral Separator>
349 [[nodiscard]] constexpr auto with_end_of_options_separator() const
350 -> TokenizerConfig {
351 auto cfg = *this;
352 cfg.end_of_options_separator_ = Separator.view();
353 return cfg;
354 }
355
356 [[nodiscard]] constexpr auto with_inline_value_separator(char separator) const
357 -> TokenizerConfig {
358 auto cfg = *this;
359 cfg.inline_value_separator_ = separator;
360 return cfg;
361 }
362};
363
376inline auto token_prefix_view(const Token& tok, const TokenizerConfig& cfg)
377 -> std::string_view {
378 if (tok.prefix == kNoPrefix) return {};
379 if (tok.prefix & kShortPrefixBit) return cfg.short_option_prefix();
380 return cfg.option_prefixes()[tok.prefix & kPrefixIndexMask];
381}
382
412template <class OptionLookup, class CommandLookup>
413inline auto tokenize(std::span<const std::string_view> args,
414 OptionLookup option_nargs, CommandLookup is_command,
415 const TokenizerConfig& cfg = {}) -> TokenizeResult {
416 TokenizeResult result;
417
418 // Returns an encoded prefix tag that can be decoded with token_prefix_view().
419 const auto find_prefix = [&](std::string_view tok) -> std::uint8_t {
420 for (std::uint8_t pi = 0;
421 pi < static_cast<uint8_t>(cfg.option_prefixes().size()); ++pi) {
422 const auto& p = cfg.option_prefixes()[pi];
423 if (tok.size() > p.size() && tok.starts_with(p)) {
424 return long_prefix(pi);
425 }
426 }
427
428 if (tok.size() > cfg.short_option_prefix().size() &&
429 tok.starts_with(cfg.short_option_prefix())) {
430 return short_prefix();
431 }
432
433 return no_prefix();
434 };
435
436 const auto prefix_view = [&](std::uint8_t p) -> std::string_view {
437 if (p == no_prefix()) return {};
438 if (is_short_prefix(p)) return cfg.short_option_prefix();
439 return cfg.option_prefixes()[p & kPrefixIndexMask];
440 };
441
442 const auto push = [&](TokenType type, std::string_view text, std::size_t pos,
443 std::uint8_t prefix = kNoPrefix) -> void {
444 result.tokens.push_back({
445 .text = text,
446 .position = pos,
447 .prefix = prefix,
448 .type = type,
449 });
450 };
451
452 bool post_separator = false;
453 std::size_t i = 0;
454
455 while (i < args.size()) {
456 const std::string_view tok = args[i];
457
458 // ── option parsing terminator ─────────────────────────────────────────
459 if (!post_separator && tok == cfg.end_of_options_separator()) {
460 post_separator = true;
461 ++i;
462 continue;
463 }
464
465 // ── after separator: everything is positional ─────────────────────────
466 if (post_separator) {
467 push(TokenType::positional, tok, i++);
468 continue;
469 }
470
471 // ── command name ──────────────────────────────────────────────────────
472 const auto prefix = find_prefix(tok);
473 const auto prefix_sv = prefix_view(prefix);
474 if (!has_prefix(prefix) && is_command(tok)) {
475 push(TokenType::command, tok, i);
476 break; // args[i+1:] are the sub-command's responsibility
477 }
478
479 // ── option-looking token ──────────────────────────────────────────────
480 if (has_prefix(prefix)) {
481 const auto sep = cfg.inline_value_separator();
482 const auto eq_pos = tok.find(sep);
483 const bool has_inline_val = (eq_pos != std::string_view::npos);
484 const std::string_view opt_sv =
485 has_inline_val ? tok.substr(0, eq_pos) : tok;
486 const auto nargs_opt = option_nargs(opt_sv);
487 if (!nargs_opt) {
488 // Short cluster (-xvf) or attached value (-ofile): only when the token
489 // uses the short prefix and carries more than one character after it.
490 const std::string_view body = opt_sv.substr(prefix_sv.size());
491 if (is_short_prefix(prefix) && !has_inline_val && body.size() > 1) {
492 bool advanced_i = false;
493 const std::string_view char_prefix = tok.substr(0, prefix_sv.size());
494 std::string char_full{char_prefix};
495 char_full.push_back('\0');
496 for (std::size_t k = 0; k < body.size(); ++k) {
497 const std::string_view char_bare =
498 tok.substr(prefix_sv.size() + k, 1);
499 char_full.back() = body[k];
500 const auto char_nargs_opt = option_nargs(char_full);
501 if (!char_nargs_opt) {
502 return result.fail(ErrorCode::unknown_option, i, char_full);
503 }
504 const Nargs nargs = *char_nargs_opt;
505 const bool is_flag = (nargs.min == 0 && nargs.max == 0);
506 push(TokenType::option, char_bare, i, prefix);
507 if (!is_flag) {
508 if (k + 1 < body.size()) {
509 // First value is attached (rest of body).
510 push(TokenType::value, tok.substr(prefix_sv.size() + k + 1), i);
511 ++i;
512 // Consume additional tokens for nargs > 1.
513 int count = 1;
514 while (i < args.size()) {
515 const std::string_view next = args[i];
516 if (next == cfg.end_of_options_separator()) break;
517 if (is_command(next)) break;
518 if (nargs.max != -1 && count >= nargs.max) break;
519 if (has_prefix(find_prefix(next))) {
520 const auto next_sep = next.find(sep);
521 const std::string_view next_opt =
522 next_sep != std::string_view::npos
523 ? next.substr(0, next_sep)
524 : next;
525 if (option_nargs(next_opt).has_value()) break;
526 if (count >= 1 || nargs.min == 0) break;
527 }
528 push(TokenType::value, args[i], i);
529 ++i;
530 ++count;
531 }
532 if (count < nargs.min) {
533 return result.fail(
534 ErrorCode::missing_value, i, char_full,
535 detail::value_count_message(nargs.min, count));
536 }
537 } else {
538 // Consume next token(s) greedily as values
539 ++i;
540 int count = 0;
541 while (i < args.size()) {
542 const std::string_view next = args[i];
543 if (next == cfg.end_of_options_separator()) break;
544 if (is_command(next)) break;
545 if (nargs.max != -1 && count >= nargs.max) break;
546 if (has_prefix(find_prefix(next))) {
547 const auto next_sep = next.find(sep);
548 const std::string_view next_opt =
549 next_sep != std::string_view::npos
550 ? next.substr(0, next_sep)
551 : next;
552 if (option_nargs(next_opt).has_value()) break;
553 if (count >= 1 || nargs.min == 0) break;
554 }
555 push(TokenType::value, args[i], i);
556 ++i;
557 ++count;
558 }
559 if (count < nargs.min) {
560 return result.fail(
561 ErrorCode::missing_value, i, char_full,
562 detail::value_count_message(nargs.min, count));
563 }
564 }
565 advanced_i = true;
566 break;
567 }
568 }
569 if (!advanced_i) ++i;
570 continue;
571 }
572 return result.fail(ErrorCode::unknown_option, i, std::string(opt_sv));
573 }
574
575 const Nargs& nargs = *nargs_opt;
576 const bool is_flag = (nargs.min == 0 && nargs.max == 0);
577
578 // Store bare name (without prefix) in text; matched_prefix holds the
579 // prefix.
580
581 push(TokenType::option, opt_sv.substr(prefix_sv.size()), i, prefix);
582
583 if (has_inline_val) {
584 if (is_flag) {
585 return result.fail(ErrorCode::invalid_value, i, std::string(opt_sv),
586 "flag does not accept a value");
587 }
588 if (nargs.min > 1) {
589 return result.fail(ErrorCode::missing_value, i, std::string(opt_sv),
590 detail::inline_value_count_message(nargs.min));
591 }
592 push(TokenType::value, tok.substr(eq_pos + 1), i);
593 ++i;
594 continue;
595 }
596
597 // prefix-opt form: advance, then greedily consume value tokens
598 ++i;
599
600 if (is_flag) continue;
601
602 int count = 0;
603 while (i < args.size()) {
604 const std::string_view next = args[i];
605
606 if (next == cfg.end_of_options_separator()) break;
607 if (is_command(next)) break;
608 if (nargs.max != -1 && count >= nargs.max) break;
609
610 if (has_prefix(find_prefix(next))) {
611 const auto next_sep = next.find(sep);
612 const std::string_view next_opt = next_sep != std::string_view::npos
613 ? next.substr(0, next_sep)
614 : next;
615 if (option_nargs(next_opt).has_value()) break;
616
617 // Unknown option-looking: consume only as first value when min > 0
618 if (count >= 1 || nargs.min == 0) break;
619 }
620
621 push(TokenType::value, args[i], i);
622 ++i;
623 ++count;
624 }
625
626 if (count < nargs.min) {
627 return result.fail(ErrorCode::missing_value, i, std::string(opt_sv),
628 detail::value_count_message(nargs.min, count));
629 }
630 continue;
631 }
632
633 // ── bare token → positional ───────────────────────────────────────────
634 push(TokenType::positional, tok, i++);
635 }
636
637 return result;
638}
639
660template <class T>
662 T value{};
666
668 [[nodiscard]]
669 constexpr explicit operator bool() const noexcept {
670 return !error.has_error();
671 }
672
674 [[nodiscard]]
675 constexpr auto has_value() const noexcept -> bool {
676 return !error.has_error();
677 }
678
680 [[nodiscard]]
681 constexpr auto has_error() const noexcept -> bool {
682 return error.has_error();
683 }
684
692 template <class U>
693 [[nodiscard]]
694 static constexpr auto ok(U&& value) -> ParseResult<T> {
695 return {
696 .value = std::forward<U>(value),
697 .error = {},
698 };
699 }
700
707 [[nodiscard]]
708 static constexpr auto fail(ParseError error) -> ParseResult<T> {
709 return {
710 .value = {},
711 .error = std::move(error),
712 };
713 }
714};
715
735template <ArgumentSpec T>
736struct Parser {
737 template <ArgumentSpec>
738 friend struct Parser;
739
748 // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays)
749 auto parse(int argc, char* argv[]) -> ParseResult<T> {
750 std::vector<std::string_view> args;
751 args.reserve(static_cast<std::size_t>(argc));
752 for (int i = 0; i < argc; ++i) {
753 args.emplace_back(argv[i]);
754 }
755 if (argc > 0) {
756 set_program_name(argv[0]);
757 }
758 return this->parse(std::span<const std::string_view>(args), 1);
759 }
760
772 // NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays)
773 auto parse(T initial, int argc, char* argv[]) -> ParseResult<T> {
774 std::vector<std::string_view> args;
775 args.reserve(static_cast<std::size_t>(argc));
776 for (int i = 0; i < argc; ++i) {
777 args.emplace_back(argv[i]);
778 }
779 if (argc > 0) {
780 set_program_name(argv[0]);
781 }
782 return this->parse(std::move(initial),
783 std::span<const std::string_view>(args), 1);
784 }
785
794 auto parse(std::span<const std::string_view> args, std::size_t first_index = 0)
795 -> ParseResult<T> {
796 ParseResult<T> result;
797 parse_body(result, args, first_index);
798 return result;
799 }
800
809 auto parse(std::span<const std::string_view> args,
810 std::string_view program_name, std::size_t first_index = 0)
811 -> ParseResult<T> {
812 set_program_name(std::string(program_name));
813 ParseResult<T> result;
814 parse_body(result, args, first_index);
815 return result;
816 }
817
829 auto parse(T initial, std::span<const std::string_view> args,
830 std::size_t first_index = 0) -> ParseResult<T> {
831 ParseResult<T> result;
832 result.value = std::move(initial);
833 parse_body(result, args, first_index);
834 return result;
835 }
836
847 auto parse(T initial, std::span<const std::string_view> args,
848 std::string_view program_name, std::size_t first_index = 0)
849 -> ParseResult<T> {
851 ParseResult<T> result;
852 result.value = std::move(initial);
853 parse_body(result, args, first_index);
854 return result;
855 }
856
861 [[nodiscard]] auto program_name() const -> const std::string& {
862 return program_name_;
863 }
864
869 auto set_program_name(std::string name) -> void {
870 program_name_ = std::move(name);
871 }
872
873 private:
874 TokenizerConfig cfg_;
875 std::string program_name_ = "program";
876
877 static constexpr std::size_t field_count = std::tuple_size_v<
878 std::remove_reference_t<decltype(as_tuple(std::declval<T&>()))>>;
879
880 auto parse_body(ParseResult<T>& result, std::span<const std::string_view> args,
881 std::size_t first_index) -> void {
882 auto lookup = get_option_spec_map(result.value);
883
884 auto tokenized = tokenize(
885 args.subspan(std::min(first_index, args.size())),
886 [&](std::string_view opt) -> auto { return lookup.option_nargs(opt); },
887 [&](std::string_view name) -> auto { return lookup.is_command(name); },
888 cfg_);
889 if (tokenized.has_error()) {
890 if (tokenized.error.has_position()) {
891 tokenized.error.position += static_cast<int>(first_index);
892 }
893 result.error = std::move(tokenized.error);
894 return;
895 }
896
897 const auto& tokens = tokenized.tokens;
898 std::size_t pos_idx = 0, pos_cnt = 0;
899
900 for (std::size_t i = 0; i < tokens.size();) {
901 const Token& tok = tokens[i];
902
903 if (tok.type == TokenType::option) {
904 std::size_t j = i + 1;
905 while (j < tokens.size() && tokens[j].type == TokenType::value) ++j;
906 auto err =
907 dispatch_option(result.value, token_prefix_view(tok, cfg_), tok.text,
908 std::span(tokens).subspan(i + 1, j - i - 1),
909 first_index, tok.position);
910 if (err.has_error()) {
911 result.error = finalize_special_error(std::move(err.error));
912 return;
913 }
914 i = j;
915
916 } else if (tok.type == TokenType::positional) {
917 auto err = dispatch_positional(result.value, pos_idx, pos_cnt, tok.text,
918 tok.position + first_index);
919 if (err.has_error()) {
920 result.error = finalize_special_error(std::move(err.error));
921 return;
922 }
923 ++i;
924
925 } else if (tok.type == TokenType::command) {
926 auto err = dispatch_command(result.value, tok.text, args,
927 tok.position + first_index + 1);
928 if (err.has_error()) {
929 result.error = finalize_special_error(std::move(err.error));
930 return;
931 }
932 break;
933
934 } else {
935 ++i;
936 }
937 }
938
939 // Fire on_parse callbacks for positionals once, after all tokens are
940 // consumed.
941 for_each_field(result.value, [](auto& f) -> auto {
942 using F = std::remove_cvref_t<decltype(f)>;
943 if constexpr (std::derived_from<F, PositionalTag>) {
944 if (f.provided()) f.on_parse();
945 }
946 });
947
948 if (auto err = apply_env_fallback(result.value); err.has_error()) {
949 result.error = std::move(err.error);
950 return;
951 }
952
953 if (auto err = validate_required(result.value); err.has_error()) {
954 result.error = std::move(err.error);
955 return;
956 }
957
958 if (auto err = validate_relations(result.value); err.has_error()) {
959 result.error = std::move(err.error);
960 return;
961 }
962
963 if constexpr (requires(T& t) {
964 { t.constraints() } -> std::same_as<ConstraintResult>;
965 }) {
966 if (auto cr = result.value.constraints(); !cr.ok()) {
967 result.error = std::move(*cr.error);
968 }
969 }
970 }
971
972 public:
980 HelpPalette palette = default_help_palette) -> std::string {
981 return cli::format_help<T>(scratch_, program_name_, color_mode, false,
982 palette);
983 }
984
990 auto format_help(RecurseHelpTag) -> std::string {
992 }
993
994 auto format_help(HelpPalette palette) -> std::string {
995 return format_help(ColorMode::detect, palette);
996 }
997
1005 auto format_help(ColorMode color_mode, RecurseHelpTag) -> std::string {
1006 return cli::format_help<T>(scratch_, program_name_, color_mode, true);
1007 }
1008
1010 -> std::string {
1011 return cli::format_help<T>(scratch_, program_name_, color_mode, true,
1012 palette);
1013 }
1014
1015 private:
1016 T scratch_{};
1017
1018 // Iterate every field of val, calling fn(field) for each.
1019 template <class Fn>
1020 static constexpr void for_each_field(T& val, Fn fn) {
1021 auto&& tup = as_tuple(val);
1022 [&]<std::size_t... I>(std::index_sequence<I...>)
1023 -> auto {
1024 (fn(std::get<I>(tup)), ...);
1025 }(std::make_index_sequence<
1026 std::tuple_size_v<std::remove_reference_t<decltype(tup)>>>{});
1027 }
1028
1029 template <class Fn>
1030 static constexpr void for_each_field(const T& val, Fn fn) {
1031 auto&& tup = as_tuple(val);
1032 [&]<std::size_t... I>(std::index_sequence<I...>)
1033 -> auto {
1034 (fn(std::get<I>(tup)), ...);
1035 }(std::make_index_sequence<
1036 std::tuple_size_v<std::remove_reference_t<decltype(tup)>>>{});
1037 }
1038
1039 // Build a flat lookup table once per parse so tokenization does not reflect
1040 // over every field for every argv token.
1041 struct Lookup {
1043 std::string_view prefix;
1044 std::string_view name;
1047 bool is_short{};
1048 };
1049
1051 std::string_view name;
1052 };
1053
1054 std::array<OptionEntry,
1055 field_count*(TokenizerConfig::max_option_prefixes + 1)>
1056 options{};
1057 std::array<CommandEntry, field_count> commands{};
1058 std::size_t option_count{};
1059 std::size_t command_count{};
1060
1061 auto add_long_option(std::string_view prefix, std::string_view name,
1062 Nargs nargs) -> void {
1063 options[option_count++] = OptionEntry{
1064 .prefix = prefix,
1065 .name = name,
1066 .short_name = '\0',
1067 .nargs = nargs,
1068 .is_short = false,
1069 };
1070 }
1071
1072 auto add_short_option(std::string_view prefix, char short_name, Nargs nargs)
1073 -> void {
1074 options[option_count++] = OptionEntry{
1075 .prefix = prefix,
1076 .name = {},
1077 .short_name = short_name,
1078 .nargs = nargs,
1079 .is_short = true,
1080 };
1081 }
1082
1083 auto add_command(std::string_view name) -> void {
1084 commands[command_count++] = CommandEntry{.name = name};
1085 }
1086
1087 [[nodiscard]] auto option_nargs(std::string_view opt) const
1088 -> std::optional<Nargs> {
1089 for (std::size_t i = 0; i < option_count; ++i) {
1090 const auto& entry = options[i];
1091 if (!opt.starts_with(entry.prefix)) {
1092 continue;
1093 }
1094 const auto rest = opt.substr(entry.prefix.size());
1095 if (entry.is_short) {
1096 if (rest.size() == 1 && rest.front() == entry.short_name) {
1097 return entry.nargs;
1098 }
1099 } else if (rest == entry.name) {
1100 return entry.nargs;
1101 }
1102 }
1103 return std::nullopt;
1104 }
1105
1106 [[nodiscard]] auto is_command(std::string_view name) const -> bool {
1107 for (std::size_t i = 0; i < command_count; ++i) {
1108 if (commands[i].name == name) return true;
1109 }
1110 return false;
1111 }
1112 };
1113
1114 auto get_option_spec_map(const T& val) const -> Lookup {
1115 Lookup lookup{};
1116 for_each_field(val, [&](const auto& f) -> auto {
1117 using F = std::remove_cvref_t<decltype(f)>;
1118 if constexpr (std::derived_from<F, OptionTag>) {
1119 for (const auto& p : cfg_.option_prefixes()) {
1120 lookup.add_long_option(p, F::name.view(), F::nargs);
1121 }
1122 if constexpr (F::short_name != '\0') {
1123 lookup.add_short_option(cfg_.short_option_prefix(), F::short_name,
1124 F::nargs);
1125 }
1126 } else if constexpr (std::derived_from<F, CommandTag>) {
1127 lookup.add_command(F::command_name());
1128 }
1129 });
1130 return lookup;
1131 }
1132
1133 // Find the option field matching (prefix, bare), notify it, and invoke its
1134 // action on each value token. notify_option_seen() is called once;
1135 // invoke_action() is called once for flags / missing optional entries, or
1136 // once per value token otherwise.
1137 auto dispatch_option(T& val, std::string_view prefix, std::string_view bare,
1138 std::span<const Token> vals, std::size_t first_index,
1139 std::size_t option_position) -> ActionResult<void> {
1141 bool found = false;
1142 for_each_field(val, [&](auto& f) -> auto {
1143 using F = std::remove_cvref_t<decltype(f)>;
1144 if constexpr (std::derived_from<F, OptionTag>) {
1145 if (found) {
1146 return;
1147 }
1148 bool match = false;
1149 for (const auto& p : cfg_.option_prefixes()) {
1150 if (p == prefix && F::name.view() == bare) {
1151 match = true;
1152 break;
1153 }
1154 }
1155 if (!match && F::short_name != '\0' &&
1156 cfg_.short_option_prefix() == prefix && bare.size() == 1 &&
1157 bare[0] == F::short_name) {
1158 match = true;
1159 }
1160 if (!match) {
1161 return;
1162 }
1163 found = true;
1164 f.notify_option_seen();
1165 if constexpr (F::entry_is_optional) {
1166 res = f.invoke_action(std::nullopt, option_position + first_index,
1167 vals.size(), 0);
1168 }
1169 const std::size_t nargs = vals.size();
1170 for (std::size_t vi = 0; vi < vals.size(); ++vi) {
1171 if (res.has_error()) {
1172 break;
1173 }
1174 res = f.invoke_action(vals[vi].text, vals[vi].position + first_index,
1175 nargs, vi);
1176 }
1177 if (!res.has_error()) f.on_parse();
1178 }
1179 });
1180 return res;
1181 }
1182
1183 // Fill the pos_idx-th positional field with text. Advances pos_idx/pos_cnt
1184 // when the current field reaches its nargs.max.
1185 static auto dispatch_positional(T& val, std::size_t& pos_idx,
1186 std::size_t& pos_cnt, std::string_view text,
1187 std::size_t position) -> ActionResult<void> {
1189 bool invoked = false;
1190 std::size_t cur = 0;
1191 for_each_field(val, [&](auto& f) -> auto {
1192 using F = std::remove_cvref_t<decltype(f)>;
1193 if constexpr (std::derived_from<F, PositionalTag>) {
1194 if (cur++ != pos_idx || invoked) return;
1195 invoked = true;
1196 res = f.invoke_action(text, position);
1197 if (res.has_value()) {
1198 ++pos_cnt;
1199 if (F::nargs.max != -1 &&
1200 pos_cnt >= static_cast<std::size_t>(F::nargs.max)) {
1201 ++pos_idx;
1202 pos_cnt = 0;
1203 }
1204 }
1205 }
1206 });
1207 if (!invoked)
1210 .kind = ErrorKind::parse,
1211 .position = static_cast<int>(position),
1212 .subject = std::string(text)});
1213 return res;
1214 }
1215
1216 auto dispatch_command(T& val, std::string_view name,
1217 std::span<const std::string_view> args,
1218 std::size_t first_index) -> ActionResult<void> {
1220 bool found = false;
1221 for_each_field(val, [&](auto& f) -> auto {
1222 using F = std::remove_cvref_t<decltype(f)>;
1223 if constexpr (std::derived_from<F, CommandTag>) {
1224 if (found || F::command_name() != name) {
1225 return;
1226 }
1227 found = true;
1228 f.mark_provided();
1229 auto sub_parser = Parser<typename F::argument_type>{};
1230 sub_parser.set_program_name(program_name_ + " " +
1231 std::string(F::command_name()));
1232 auto sub = sub_parser.parse(args, first_index);
1233 if (sub.has_error()) {
1234 res = ActionResult<void>::fail(std::move(sub.error));
1235 } else {
1236 static_cast<typename F::argument_type&>(f) = std::move(sub.value);
1237 f.mark_provided();
1238 }
1239 }
1240 });
1241 if (!found) {
1244 .kind = ErrorKind::parse,
1245 .position = static_cast<int>(first_index - 1),
1246 .subject = std::string(name),
1247 });
1248 }
1249 return res;
1250 }
1251
1252 static auto validate_required(T& val) -> ActionResult<void> {
1254 for_each_field(val, [&](auto& f) -> auto {
1255 using F = std::remove_cvref_t<decltype(f)>;
1256 if constexpr (std::derived_from<F, OptionTag> ||
1257 std::derived_from<F, PositionalTag>) {
1258 if (res.has_error()) {
1259 return;
1260 }
1261 if (f.get_param().presence == Presence::required &&
1262 !static_cast<bool>(f)) {
1263 std::string subject;
1264 if constexpr (std::derived_from<F, OptionTag>) {
1265 subject = "--" + std::string(F::name.view());
1266 } else {
1267 subject = "<positional>";
1268 }
1271 .kind = ErrorKind::parse,
1272 .subject = std::move(subject),
1273 });
1274 }
1275 }
1276 });
1277 return res;
1278 }
1279
1280 static auto relation_name_list(NameList<> names) -> std::string {
1281 std::string out;
1282 for (std::size_t i = 0; i < names.size; ++i) {
1283 if (i != 0) out += ", ";
1284 out += "--";
1285 out += names.values[i];
1286 }
1287 return out;
1288 }
1289
1290 static auto relation_group_exists(std::string_view name) -> bool {
1291 if constexpr (!detail::HasRelations<T>) {
1292 return false;
1293 } else {
1294 bool found = false;
1295 std::apply(
1296 [&](auto... rels) -> auto {
1297 (
1298 [&]() -> auto {
1299 using R = std::remove_cvref_t<decltype(rels)>;
1300 if constexpr (std::same_as<R, GroupRelation>) {
1301 if (rels.name == name) found = true;
1302 }
1303 }(),
1304 ...);
1305 },
1306 T::relations.items);
1307 return found;
1308 }
1309 }
1310
1311 static auto relation_operand_label(std::string_view operand) -> std::string {
1312 if (relation_group_exists(operand)) {
1313 return std::string(operand);
1314 }
1315 return "--" + std::string(operand);
1316 }
1317
1318 static auto relation_operand_present(T& val, std::string_view name) -> bool {
1319 bool found_field = false;
1320 bool present = false;
1321 for_each_field(val, [&](auto& f) -> auto {
1322 using F = std::remove_cvref_t<decltype(f)>;
1323 if constexpr (std::derived_from<F, OptionTag> ||
1324 std::derived_from<F, CommandTag>) {
1325 if (!found_field && F::name.view() == name) {
1326 found_field = true;
1327 present = f.provided();
1328 }
1329 }
1330 });
1331 if (found_field) return present;
1332
1333 if constexpr (detail::HasRelations<T>) {
1334 bool found_group = false;
1335 bool group_present = false;
1336 std::apply(
1337 [&](auto... rels) -> auto {
1338 (
1339 [&]() -> auto {
1340 using R = std::remove_cvref_t<decltype(rels)>;
1341 if constexpr (std::same_as<R, GroupRelation>) {
1342 if (!found_group && rels.name == name) {
1343 found_group = true;
1344 group_present = true;
1345 for (auto member : rels.members) {
1346 group_present = group_present &&
1347 relation_operand_present(val, member);
1348 }
1349 }
1350 }
1351 }(),
1352 ...);
1353 },
1354 T::relations.items);
1355 return group_present;
1356 }
1357 return false;
1358 }
1359
1360 static auto validate_relations(T& val) -> ActionResult<void> {
1361 if constexpr (!detail::HasRelations<T>) {
1362 return ActionResult<void>::ok();
1363 } else {
1365 std::apply(
1366 [&](auto... rels) -> auto {
1367 (
1368 [&]() -> auto {
1369 if (res.has_error()) return;
1370 using R = std::remove_cvref_t<decltype(rels)>;
1371 if constexpr (std::same_as<R, GroupRelation>) {
1372 std::size_t count = 0;
1373 for (auto member : rels.members) {
1374 if (relation_operand_present(val, member)) ++count;
1375 }
1376 if (count != 0 && count != rels.members.size) {
1379 .kind = ErrorKind::validation,
1380 .subject = std::string(rels.name),
1381 .detail = "arguments must be provided together: " +
1382 relation_name_list(rels.members),
1383 });
1384 }
1385 } else if constexpr (std::same_as<R, ConflictsRelation>) {
1386 if (relation_operand_present(val, rels.left) &&
1387 relation_operand_present(val, rels.right)) {
1390 .kind = ErrorKind::validation,
1391 .subject = relation_operand_label(rels.left) + ", " +
1392 relation_operand_label(rels.right),
1393 .detail =
1394 "these arguments cannot be provided together",
1395 });
1396 }
1397 } else if constexpr (std::same_as<R, DependsOnRelation>) {
1398 if (relation_operand_present(val, rels.source) &&
1399 !relation_operand_present(val, rels.target)) {
1402 .kind = ErrorKind::validation,
1403 .subject = relation_operand_label(rels.source),
1404 .detail =
1405 "requires " + relation_operand_label(rels.target),
1406 });
1407 }
1408 }
1409 }(),
1410 ...);
1411 },
1412 T::relations.items);
1413 return res;
1414 }
1415 }
1416
1417 auto apply_env_fallback(T& val) -> ActionResult<void> {
1419 for_each_field(val, [&](auto& f) -> auto {
1420 using F = std::remove_cvref_t<decltype(f)>;
1421 if constexpr (std::derived_from<F, OptionTag>) {
1422 if (res.has_error() || f.provided() || f.get_param().env.empty()) return;
1423#ifdef _WIN32
1424 char* raw_buf = nullptr;
1425 std::size_t raw_len = 0;
1426 _dupenv_s(&raw_buf, &raw_len, std::string(f.get_param().env).c_str());
1427 std::unique_ptr<char, decltype(&free)> raw_guard(raw_buf, &free);
1428 const char* raw = raw_buf;
1429#else
1430 const char* raw = std::getenv(std::string(f.get_param().env).c_str());
1431#endif
1432 if (!raw) return;
1433 if constexpr (F::nargs.min == 0 && F::nargs.max == 0) {
1434 res = f.invoke_action(std::nullopt, 0, 0, 0, false);
1435 } else {
1436 res = f.invoke_action(std::string_view(raw), 0, 1, 0, false);
1437 }
1438 if (!res.has_error()) f.on_parse();
1439 }
1440 });
1441 return res;
1442 }
1443
1444 auto finalize_special_error(ParseError error) -> ParseError {
1445 if (error.code == ErrorCode::help_requested && error.detail.empty()) {
1447 }
1448 return error;
1449 }
1450};
1451
1462template <ArgumentSpec T>
1463// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays)
1464auto parse(int argc, char* argv[]) -> ParseResult<T> {
1465 return Parser<T>{}.parse(argc, argv);
1466}
1467
1486template <ArgumentSpec T>
1487// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays, modernize-avoid-c-arrays)
1488auto parse_or_exit(int argc, char* argv[]) -> T {
1489 auto result = parse<T>(argc, argv);
1490 if (!result) {
1491 if (const auto message = result.error.message(); !message.empty()) {
1492 detail::write_message(result.error.use_stdout() ? stdout : stderr,
1493 message);
1494 }
1495 std::exit(result.error.exit_code());
1496 }
1497 return std::move(result.value);
1498}
1499
1500} // namespace cli
constexpr auto push
Definition action.hh:1880
Definition action.hh:29
constexpr HelpPalette default_help_palette
Definition color.hh:39
auto parse_or_exit(int argc, char *argv[]) -> T
Parses argc/argv and exits the process on failure.
Definition parser.hh:1488
auto parse(int argc, char *argv[]) -> ParseResult< T >
Convenience free function that parses argc/argv into type T.
Definition parser.hh:1464
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
auto token_prefix_view(const Token &tok, const TokenizerConfig &cfg) -> std::string_view
Retrieves the matched prefix string for an option token.
Definition parser.hh:376
static constexpr std::uint8_t kPrefixIndexMask
Definition parser.hh:153
static constexpr auto long_prefix(std::uint8_t i) -> std::uint8_t
Definition parser.hh:159
TokenType
Classifies a single token produced by tokenize().
Definition parser.hh:144
constexpr RecurseHelpTag recurse_help
Convenience instance of RecurseHelpTag.
Definition color.hh:54
auto tokenize(std::span< const std::string_view > args, OptionLookup option_nargs, CommandLookup is_command, const TokenizerConfig &cfg={}) -> TokenizeResult
Converts a raw argument span into an ordered sequence of typed tokens.
Definition parser.hh:413
static constexpr std::uint8_t kShortPrefixBit
Definition parser.hh:152
static constexpr auto has_prefix(std::uint8_t p) -> bool
Definition parser.hh:163
static constexpr std::uint8_t kNoPrefix
Definition parser.hh:151
static constexpr auto is_short_prefix(std::uint8_t p) -> bool
Definition parser.hh:167
ErrorCode
Identifies the specific failure that occurred during parsing or validation.
Definition error.hh:20
@ invalid_value
A value could not be interpreted as the required type.
Definition error.hh:26
@ unknown_option
An unrecognised option name was encountered.
Definition error.hh:21
@ unknown_command
An unrecognised subcommand name was encountered.
Definition error.hh:22
@ missing_value
An option that requires a value was given none.
Definition error.hh:25
@ help_requested
The --help flag (or equivalent) was seen; print help and exit 0.
Definition error.hh:40
@ dependency_missing
An option that depends on another option was given without it.
Definition error.hh:35
@ mutually_exclusive
Two mutually-exclusive options were both supplied.
Definition error.hh:34
@ missing_required
A required option or positional was not provided.
Definition error.hh:33
@ unexpected_argument
A positional argument was supplied when none was expected.
Definition error.hh:23
static constexpr auto no_prefix() -> std::uint8_t
Definition parser.hh:155
static constexpr auto short_prefix() -> std::uint8_t
Definition parser.hh:157
constexpr auto as_tuple(T &t)
Converts an aggregate value to a std::tuple of references to its members.
Definition meta.hh:125
constexpr std::size_t token_stack_capacity
Number of tokenizer tokens kept in inline stack storage before falling back to heap allocation.
Definition parser.hh:45
ColorMode
Controls whether ANSI escape codes are emitted by format_help().
Definition color.hh:20
#define CLI20_TOKEN_STACK_CAPACITY
Definition parser.hh:29
The result type produced and consumed at each step of an action pipeline.
Definition action.hh:48
static constexpr auto fail(ParseError e) -> ActionResult
Creates a failure result carrying error.
Definition action.hh:100
ANSI palette used by generated help output.
Definition color.hh:28
Specifies the minimum and maximum number of values an option or positional accepts.
Carries all context about a single parse/validation failure.
Definition error.hh:123
ErrorCode code
The specific error code; unknown_error means no error.
Definition error.hh:125
std::string detail
Additional human-readable context about the failure (may be empty).
Definition error.hh:139
The result returned by Parser::parse() and the free cli::parse() function.
Definition parser.hh:661
ParseError error
The error details (valid only when has_error() is true).
Definition parser.hh:665
T value
The parsed argument struct (valid only when has_value() is true).
Definition parser.hh:662
constexpr auto has_error() const noexcept -> bool
Returns true when parsing failed.
Definition parser.hh:681
constexpr auto has_value() const noexcept -> bool
Returns true when parsing succeeded.
Definition parser.hh:675
static constexpr auto fail(ParseError error) -> ParseResult< T >
Creates a failure result carrying error.
Definition parser.hh:708
static constexpr auto ok(U &&value) -> ParseResult< T >
Creates a success result holding value.
Definition parser.hh:694
The main parsing engine for a typed argument specification.
Definition parser.hh:736
auto program_name() const -> const std::string &
Get the program name used in help output.
Definition parser.hh:861
auto parse(T initial, std::span< const std::string_view > args, std::size_t first_index=0) -> ParseResult< T >
Parses a span starting from a pre-initialised argument struct.
Definition parser.hh:829
auto format_help(ColorMode color_mode, HelpPalette palette, RecurseHelpTag) -> std::string
Definition parser.hh:1009
auto format_help(ColorMode color_mode=ColorMode::detect, HelpPalette palette=default_help_palette) -> std::string
Generates a help string for the argument specification.
Definition parser.hh:979
auto parse(T initial, int argc, char *argv[]) -> ParseResult< T >
Parses argc/argv starting from a pre-initialised argument struct.
Definition parser.hh:773
auto parse(std::span< const std::string_view > args, std::size_t first_index=0) -> ParseResult< T >
Parses a span of string views, skipping first_index elements.
Definition parser.hh:794
friend struct Parser
Definition parser.hh:738
auto set_program_name(std::string name) -> void
Set the program name used in help output.
Definition parser.hh:869
auto parse(std::span< const std::string_view > args, std::string_view program_name, std::size_t first_index=0) -> ParseResult< T >
Parses a span with an explicit program name for help output.
Definition parser.hh:809
auto format_help(HelpPalette palette) -> std::string
Definition parser.hh:994
auto format_help(RecurseHelpTag) -> std::string
Generates a help string including recursive subcommand help.
Definition parser.hh:990
auto parse(T initial, std::span< const std::string_view > args, std::string_view program_name, std::size_t first_index=0) -> ParseResult< T >
Parses a span with a pre-initialised struct and explicit program name.
Definition parser.hh:847
auto format_help(ColorMode color_mode, RecurseHelpTag) -> std::string
Generates a help string with explicit color mode and recursive subcommand help.
Definition parser.hh:1005
auto parse(int argc, char *argv[]) -> ParseResult< T >
Parses standard C-style argc/argv, using argv[0] as the program name.
Definition parser.hh:749
Tag type passed to format_help() to request recursive sub-command output.
Definition color.hh:51
Compile-time string type usable as a non-type template parameter.
A single token produced by tokenize().
Definition parser.hh:174
std::string_view text
For option tokens: the bare name without prefix (e.g.
Definition parser.hh:175
std::uint8_t prefix
Definition parser.hh:180
TokenType type
Classification of this token.
Definition parser.hh:182
std::size_t position
Zero-based index into the original args span.
Definition parser.hh:179
auto empty() const -> bool
Definition parser.hh:218
auto size() const -> std::size_t
Definition parser.hh:217
auto push_back(T value) -> void
Definition parser.hh:200
auto operator[](std::size_t i) -> T &
Definition parser.hh:220
std::array< T, N > stack
Definition parser.hh:196
auto operator[](std::size_t i) const -> const T &
Definition parser.hh:224
The result returned by tokenize().
Definition parser.hh:192
SmallVector< Token, token_stack_capacity > tokens
Definition parser.hh:239
auto fail(ErrorCode code, std::size_t pos, std::string subject, std::string detail={}) -> TokenizeResult &
Sets the error and returns *this for method chaining.
Definition parser.hh:272
constexpr auto has_error() const noexcept -> bool
Returns true when tokenization failed.
Definition parser.hh:256
constexpr auto has_value() const noexcept -> bool
Returns true when tokenization succeeded.
Definition parser.hh:250
ParseError error
Definition parser.hh:240
Controls how tokenize() recognises option and separator syntax.
Definition parser.hh:291
constexpr auto option_prefixes() const -> std::span< const std::string_view >
Definition parser.hh:305
constexpr auto with_inline_value_separator(char separator) const -> TokenizerConfig
Definition parser.hh:356
constexpr auto short_option_prefix() const -> std::string_view
Definition parser.hh:310
constexpr auto with_short_option_prefix() const -> TokenizerConfig
Definition parser.hh:341
constexpr auto with_end_of_options_separator() const -> TokenizerConfig
Definition parser.hh:349
constexpr auto inline_value_separator() const -> char
Definition parser.hh:319
constexpr auto with_option_prefixes() const -> TokenizerConfig
Definition parser.hh:334
constexpr TokenizerConfig()=default
constexpr auto set_option_prefixes() -> void
Definition parser.hh:324
constexpr auto end_of_options_separator() const -> std::string_view
Definition parser.hh:314
static constexpr std::size_t max_option_prefixes
Definition parser.hh:292