CPP-ARGON 4.0.1
Command-Line Argument Parser for C++20
Loading...
Searching...
No Matches
argument_parser.hpp
Go to the documentation of this file.
1// Copyright (c) 2023-2026 Jakub Musiał
2// This file is part of the CPP-ARGON project (https://github.com/SpectraL519/cpp-argon).
3// Licensed under the MIT License. See the LICENSE file in the project root for full license information.
4
10#pragma once
11
12#include "argon/argument.hpp"
15#include "argon/types.hpp"
16#include "argon/util/ranges.hpp"
17
18#include <algorithm>
19#include <format>
20#include <iostream>
21#include <ranges>
22#include <span>
23
24#ifdef AP_TESTING
25
26namespace argon_testing {
27struct argument_parser_test_fixture;
28} // namespace argon_testing
29
30#endif
31
32namespace argon {
33
34class argument_parser;
35
37enum class default_argument : std::uint8_t {
47 p_input,
48
58
68 o_help,
69
83
94 o_input,
95
105 o_output,
106
118
129};
130
132enum class unknown_policy : std::uint8_t {
133 fail,
134 warn,
135 ignore,
136 as_values
137};
138
139namespace detail {
140
141void add_default_argument(const default_argument, argument_parser&) noexcept;
142
143} // namespace detail
144
179class argument_parser {
180public:
181 argument_parser(const argument_parser&) = delete;
182 argument_parser& operator=(const argument_parser&) = delete;
183
184 argument_parser(argument_parser&&) = delete;
185 argument_parser& operator=(argument_parser&&) = delete;
186
187 argument_parser(const std::string_view name) : argument_parser(name, "") {}
188
189 ~argument_parser() = default;
190
196 argument_parser& program_version(const version& version) noexcept {
197 this->_program_version.emplace(version.str());
198 return *this;
199 }
200
206 argument_parser& program_version(std::string_view version) {
207 if (util::contains_whitespaces(version))
208 throw invalid_configuration("The program version cannot contain whitespace characters!"
209 );
210
211 this->_program_version.emplace(version);
212 return *this;
213 }
214
220 argument_parser& program_description(std::string_view description) noexcept {
221 this->_program_description.emplace(description);
222 return *this;
223 }
224
231 argument_parser& verbose(const bool v = true) noexcept {
232 this->_verbose = v;
233 return *this;
234 }
235
242 argument_parser& unknown_arguments_policy(const unknown_policy policy) noexcept {
243 this->_unknown_policy = policy;
244 return *this;
245 }
246
254 template <util::c_range_of<default_argument> AR>
255 argument_parser& default_arguments(const AR& arg_discriminators) noexcept {
256 for (const auto arg_discriminator : arg_discriminators)
257 detail::add_default_argument(arg_discriminator, *this);
258 return *this;
259 }
260
266 argument_parser& default_arguments(
267 const std::initializer_list<default_argument>& arg_discriminators
268 ) noexcept {
269 return this->default_arguments<>(arg_discriminators);
270 }
271
277 argument_parser& default_arguments(
278 const std::same_as<default_argument> auto... arg_discriminators
279 ) noexcept {
280 (detail::add_default_argument(arg_discriminators, *this), ...);
281 return *this;
282 }
283
291 template <util::c_argument_value_type T = std::string>
292 positional_argument<T>& add_positional_argument(const std::string_view name) {
293 return this->add_positional_argument<T>(this->_gr_positional_args, name);
294 }
295
303 template <util::c_argument_value_type T = std::string>
304 positional_argument<T>& add_positional_argument(
305 argument_group& group, const std::string_view name
306 ) {
307 this->_validate_group(group);
308 this->_verify_arg_name_pattern(name);
309
310 const detail::argument_name arg_name(std::make_optional<std::string>(name));
311 if (this->_is_arg_name_used(arg_name))
312 throw invalid_configuration::argument_name_used(arg_name);
313
314 auto& new_arg_ptr =
315 this->_positional_args.emplace_back(std::make_shared<positional_argument<T>>(arg_name));
316 group._add_argument(new_arg_ptr);
317 return static_cast<positional_argument<T>&>(*new_arg_ptr);
318 }
319
328 template <util::c_argument_value_type T = std::string>
329 optional_argument<T>& add_optional_argument(
330 const std::string_view name,
331 const detail::argument_name_discriminator name_discr = n_primary
332 ) {
333 return this->add_optional_argument<T>(this->_gr_optional_args, name, name_discr);
334 }
335
344 template <util::c_argument_value_type T = std::string>
345 optional_argument<T>& add_optional_argument(
346 const std::string_view primary_name, const std::string_view secondary_name
347 ) {
348 return this->add_optional_argument<T>(
349 this->_gr_optional_args, primary_name, secondary_name
350 );
351 }
352
362 template <util::c_argument_value_type T = std::string>
363 optional_argument<T>& add_optional_argument(
364 argument_group& group,
365 const std::string_view name,
366 const detail::argument_name_discriminator name_discr = n_primary
367 ) {
368 this->_validate_group(group);
369 this->_verify_arg_name_pattern(name);
370
371 const auto arg_name =
372 name_discr == n_primary
373 ? detail::
374 argument_name{std::make_optional<std::string>(name), std::nullopt, this->_flag_prefix_char}
375 : detail::argument_name{
376 std::nullopt, std::make_optional<std::string>(name), this->_flag_prefix_char
377 };
378
379 if (this->_is_arg_name_used(arg_name))
380 throw invalid_configuration::argument_name_used(arg_name);
381
382 auto& new_arg_ptr =
383 this->_optional_args.emplace_back(std::make_shared<optional_argument<T>>(arg_name));
384 group._add_argument(new_arg_ptr);
385 return static_cast<optional_argument<T>&>(*new_arg_ptr);
386 }
387
397 template <util::c_argument_value_type T = std::string>
398 optional_argument<T>& add_optional_argument(
399 argument_group& group,
400 const std::string_view primary_name,
401 const std::string_view secondary_name
402 ) {
403 this->_validate_group(group);
404 this->_verify_arg_name_pattern(primary_name);
405 this->_verify_arg_name_pattern(secondary_name);
406
407 const detail::argument_name arg_name(
408 std::make_optional<std::string>(primary_name),
409 std::make_optional<std::string>(secondary_name),
410 this->_flag_prefix_char
411 );
412 if (this->_is_arg_name_used(arg_name))
413 throw invalid_configuration::argument_name_used(arg_name);
414
415 auto& new_arg_ptr =
416 this->_optional_args.emplace_back(std::make_shared<optional_argument<T>>(arg_name));
417 group._add_argument(new_arg_ptr);
418 return static_cast<optional_argument<T>&>(*new_arg_ptr);
419 }
420
429 template <bool StoreImplicitly = true>
430 optional_argument<bool>& add_flag(
431 const std::string_view name,
432 const detail::argument_name_discriminator name_discr = n_primary
433 ) {
434 return this->add_optional_argument<bool>(name, name_discr)
435 .default_values(not StoreImplicitly)
436 .implicit_values(StoreImplicitly)
437 .nargs(0ull);
438 }
439
448 template <bool StoreImplicitly = true>
449 optional_argument<bool>& add_flag(
450 const std::string_view primary_name, const std::string_view secondary_name
451 ) {
452 return this->add_optional_argument<bool>(primary_name, secondary_name)
453 .default_values(not StoreImplicitly)
454 .implicit_values(StoreImplicitly)
455 .nargs(0ull);
456 }
457
467 template <bool StoreImplicitly = true>
468 optional_argument<bool>& add_flag(
469 argument_group& group,
470 const std::string_view name,
471 const detail::argument_name_discriminator name_discr = n_primary
472 ) {
473 return this->add_optional_argument<bool>(group, name, name_discr)
474 .default_values(not StoreImplicitly)
475 .implicit_values(StoreImplicitly)
476 .nargs(0ull);
477 }
478
488 template <bool StoreImplicitly = true>
489 optional_argument<bool>& add_flag(
490 argument_group& group,
491 const std::string_view primary_name,
492 const std::string_view secondary_name
493 ) {
494 return this->add_optional_argument<bool>(group, primary_name, secondary_name)
495 .default_values(not StoreImplicitly)
496 .implicit_values(StoreImplicitly)
497 .nargs(0ull);
498 }
499
505 argument_group& add_group(const std::string_view name) noexcept {
506 return *this->_argument_groups.emplace_back(argument_group::create(*this, name));
507 }
508
514 argument_parser& add_subparser(const std::string_view name) {
515 const auto subparser_it = std::ranges::find(
516 this->_subparsers, name, [](const auto& subparser) { return subparser->_name; }
517 );
518 if (subparser_it != this->_subparsers.end())
519 throw std::logic_error(std::format(
520 "A subparser with the given name () already exists in parser '{}'",
521 (*subparser_it)->_name,
522 this->_program_name
523 ));
524
525 return *this->_subparsers.emplace_back(
526 std::unique_ptr<argument_parser>(new argument_parser(name, this->_program_name))
527 );
528 }
529
543 void parse_args(int argc, char* argv[]) {
544 this->parse_args(std::span(argv + 1, static_cast<std::size_t>(argc - 1)));
545 }
546
555 template <util::c_forward_range_of<std::string, util::type_validator::convertible> AR>
556 void parse_args(const AR& argv_rng) {
557 parsing_state state(*this);
558 this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state);
559
560 if (not state.unknown_args.empty())
561 throw parsing_failure(std::format(
562 "Failed to deduce the argument for values [{}]", util::join(state.unknown_args)
563 ));
564 }
565
578 void try_parse_args(int argc, char* argv[]) {
579 this->try_parse_args(std::span(argv + 1, static_cast<std::size_t>(argc - 1)));
580 }
581
594 template <util::c_forward_range_of<std::string, util::type_validator::convertible> AR>
595 void try_parse_args(const AR& argv_rng) {
596 try {
597 this->parse_args(argv_rng);
598 }
599 catch (const argon::argument_parser_exception& err) {
600 std::cerr << "[argon::error] " << err.what() << std::endl
601 << this->resolved_parser() << std::endl;
602 std::exit(EXIT_FAILURE);
603 }
604 }
605
624 std::vector<std::string> parse_known_args(int argc, char* argv[]) {
625 return this->parse_known_args(std::span(argv + 1, static_cast<std::size_t>(argc - 1)));
626 }
627
642 template <util::c_forward_range_of<std::string, util::type_validator::convertible> AR>
643 std::vector<std::string> parse_known_args(const AR& argv_rng) {
644 parsing_state state(*this, true);
645 this->_parse_args_impl(std::ranges::begin(argv_rng), std::ranges::end(argv_rng), state);
646 return std::move(state.unknown_args);
647 }
648
662 std::vector<std::string> try_parse_known_args(int argc, char* argv[]) {
663 return this->try_parse_known_args(std::span(argv + 1, static_cast<std::size_t>(argc - 1)));
664 }
665
679 template <util::c_forward_range_of<std::string, util::type_validator::convertible> AR>
680 std::vector<std::string> try_parse_known_args(const AR& argv_rng) {
681 try {
682 return this->parse_known_args(argv_rng);
683 }
684 catch (const argon::argument_parser_exception& err) {
685 std::cerr << "[argon::error] " << err.what() << std::endl
686 << this->resolved_parser() << std::endl;
687 std::exit(EXIT_FAILURE);
688 }
689 }
690
692 [[nodiscard]] std::string_view name() const noexcept {
693 return this->_name;
694 }
695
706 [[nodiscard]] std::string_view program_name() const noexcept {
707 return this->_program_name;
708 }
709
715 [[nodiscard]] bool invoked() const noexcept {
716 return this->_invoked;
717 }
718
723 [[nodiscard]] bool finalized() const noexcept {
724 return this->_finalized;
725 }
726
731 [[nodiscard]] argument_parser& resolved_parser() noexcept {
732 const auto used_subparser_it = std::ranges::find_if(
733 this->_subparsers, [](const auto& subparser) { return subparser->_invoked; }
734 );
735 if (used_subparser_it == this->_subparsers.end())
736 return *this;
737 return (*used_subparser_it)->resolved_parser();
738 }
739
745 [[nodiscard]] bool is_used(std::string_view arg_name) const noexcept {
746 const auto arg = this->_get_argument(arg_name);
747 return arg ? arg->is_used() : false;
748 }
749
755 [[nodiscard]] bool has_value(std::string_view arg_name) const noexcept {
756 const auto arg = this->_get_argument(arg_name);
757 return arg ? arg->has_value() : false;
758 }
759
765 [[nodiscard]] std::size_t count(std::string_view arg_name) const noexcept {
766 const auto arg = this->_get_argument(arg_name);
767 return arg ? arg->count() : 0ull;
768 }
769
777 template <util::c_argument_value_type T = std::string>
778 [[nodiscard]] T value(std::string_view arg_name) const {
779 const auto arg = this->_get_argument(arg_name);
780 if (not arg)
781 throw lookup_failure::argument_not_found(arg_name);
782
783 const auto& arg_value = arg->value();
784 try {
785 return std::any_cast<T>(arg_value);
786 }
787 catch (const std::bad_any_cast&) {
788 throw type_error::invalid_value_type<T>(arg->name());
789 }
790 }
791
801 template <util::c_argument_value_type T = std::string, std::convertible_to<T> U>
802 [[nodiscard]] T value_or(std::string_view arg_name, U&& fallback_value) const {
803 const auto arg = this->_get_argument(arg_name);
804 if (not arg)
805 throw lookup_failure::argument_not_found(arg_name);
806
807 try {
808 const auto& arg_value = arg->value();
809 return std::any_cast<T>(arg_value);
810 }
811 catch (const std::logic_error&) {
812 // positional: no value parsed
813 // optional: no value parsed + no predefined value
814 return T{std::forward<U>(fallback_value)};
815 }
816 catch (const std::bad_any_cast&) {
817 throw type_error::invalid_value_type<T>(arg->name());
818 }
819 }
820
829 template <util::c_argument_value_type T = std::string>
830 [[nodiscard]] std::vector<T> values(std::string_view arg_name) const {
831 const auto arg = this->_get_argument(arg_name);
832 if (not arg)
833 throw lookup_failure::argument_not_found(arg_name);
834
835 try {
836 std::vector<T> values;
837 std::ranges::copy(
838 util::any_range_cast_view<T>(arg->values()), std::back_inserter(values)
839 );
840 return values;
841 }
842 catch (const std::bad_any_cast&) {
843 throw type_error::invalid_value_type<T>(arg->name());
844 }
845 }
846
852 void print_help(const bool verbose, std::ostream& os = std::cout) const noexcept {
853 os << "Program: " << this->_program_name;
854 if (this->_program_version)
855 os << " (" << this->_program_version.value() << ')';
856 os << '\n';
857
858 if (this->_program_description)
859 os << '\n'
860 << std::string(this->_indent_width, ' ') << this->_program_description.value()
861 << '\n';
862
863 this->_print_subparsers(os);
864 for (const auto& group : this->_argument_groups)
865 this->_print_group(os, *group, verbose);
866 }
867
875 void print_version(std::ostream& os = std::cout) const noexcept {
876 os << this->_program_name << " : version " << this->_program_version.value_or("unspecified")
877 << std::endl;
878 }
879
890 friend std::ostream& operator<<(std::ostream& os, const argument_parser& parser) noexcept {
891 parser.print_help(parser._verbose, os);
892 return os;
893 }
894
895#ifdef AP_TESTING
897 friend struct ::argon_testing::argument_parser_test_fixture;
898#endif
899
900private:
901 using arg_ptr_t = std::shared_ptr<detail::argument_base>;
902 using arg_ptr_vec_t = std::vector<arg_ptr_t>;
903 using arg_ptr_vec_iter_t = typename arg_ptr_vec_t::iterator;
904
905 using arg_group_ptr_t = std::unique_ptr<argument_group>;
906 using arg_group_ptr_vec_t = std::vector<arg_group_ptr_t>;
907
908 using arg_parser_ptr_t = std::unique_ptr<argument_parser>;
909 using arg_parser_ptr_vec_t = std::vector<arg_parser_ptr_t>;
910
911 using arg_token_vec_t = std::vector<detail::argument_token>;
912 using arg_token_vec_iter_t = typename arg_token_vec_t::const_iterator;
913
915 struct parsing_state {
916 parsing_state(argument_parser& parser, const bool parse_known_only = false)
917 : curr_arg(nullptr),
918 curr_pos_arg_it(parser._positional_args.begin()),
919 parse_known_only(parse_known_only) {}
920
923 void set_parser(argument_parser& parser) {
924 this->curr_arg = nullptr;
925 this->curr_pos_arg_it = parser._positional_args.begin();
926 }
927
928 arg_ptr_t curr_arg;
929 arg_ptr_vec_iter_t
930 curr_pos_arg_it;
931 const bool
932 parse_known_only;
933 std::vector<std::string> unknown_args = {};
934 };
935
936 argument_parser(const std::string_view name, const std::string_view parent_name)
937 : _name(name),
938 _program_name(
939 std::format("{}{}{}", parent_name, std::string(not parent_name.empty(), ' '), name)
940 ),
941 _gr_positional_args(add_group("Positional Arguments")),
942 _gr_optional_args(add_group("Optional Arguments")) {
943 if (name.empty())
944 throw invalid_configuration("The program name cannot be empty!");
945
946 if (util::contains_whitespaces(name))
947 throw invalid_configuration("The program name cannot contain whitespace characters!");
948 }
949
954 void _verify_arg_name_pattern(const std::string_view arg_name) const {
955 if (arg_name.empty())
956 throw invalid_configuration::invalid_argument_name(
957 arg_name, "An argument name cannot be empty."
958 );
959
960 if (util::contains_whitespaces(arg_name))
961 throw invalid_configuration::invalid_argument_name(
962 arg_name, "An argument name cannot contain whitespaces."
963 );
964
965 if (arg_name.front() == this->_flag_prefix_char)
966 throw invalid_configuration::invalid_argument_name(
967 arg_name,
968 std::format(
969 "An argument name cannot begin with a flag prefix character ({}).",
970 this->_flag_prefix_char
971 )
972 );
973
974 if (std::isdigit(arg_name.front()))
975 throw invalid_configuration::invalid_argument_name(
976 arg_name, "An argument name cannot begin with a digit."
977 );
978 }
979
986 [[nodiscard]] auto _name_match_predicate(
987 const std::string_view arg_name,
988 const detail::argument_name::match_type m_type = detail::argument_name::m_any
989 ) const noexcept {
990 return [=](const arg_ptr_t& arg) { return arg->name().match(arg_name, m_type); };
991 }
992
999 [[nodiscard]] auto _name_match_predicate(
1000 const detail::argument_name& arg_name,
1001 const detail::argument_name::match_type m_type = detail::argument_name::m_any
1002 ) const noexcept {
1003 return [&arg_name, m_type](const arg_ptr_t& arg) {
1004 return arg->name().match(arg_name, m_type);
1005 };
1006 }
1007
1014 [[nodiscard]] bool _is_arg_name_used(
1015 const detail::argument_name& arg_name,
1016 const detail::argument_name::match_type m_type = detail::argument_name::m_any
1017 ) const noexcept {
1018 const auto predicate = this->_name_match_predicate(arg_name, m_type);
1019
1020 if (std::ranges::find_if(this->_positional_args, predicate) != this->_positional_args.end())
1021 return true;
1022
1023 if (std::ranges::find_if(this->_optional_args, predicate) != this->_optional_args.end())
1024 return true;
1025
1026 return false;
1027 }
1028
1034 void _validate_group(const argument_group& group) {
1035 if (group._parser != this)
1036 throw std::logic_error(std::format(
1037 "An argument group '{}' does not belong to the given parser.", group._name
1038 ));
1039 }
1040
1050 template <util::c_forward_iterator_of<std::string, util::type_validator::convertible> AIt>
1051 void _parse_args_impl(AIt args_begin, const AIt args_end, parsing_state& state) {
1052 this->_invoked = true;
1053
1054 if (args_begin != args_end) {
1055 // try to match a subparser
1056 const auto subparser_it =
1057 std::ranges::find(this->_subparsers, *args_begin, [](const auto& subparser) {
1058 return subparser->_name;
1059 });
1060 if (subparser_it != this->_subparsers.end()) {
1061 auto& subparser = **subparser_it;
1062 state.set_parser(subparser);
1063 subparser._parse_args_impl(++args_begin, args_end, state);
1064 return;
1065 }
1066 }
1067
1068 // process command-line arguments within the current parser
1069 this->_validate_argument_configuration();
1070 for (const auto& tok : this->_tokenize(args_begin, args_end, state))
1071 this->_parse_token(tok, state);
1072 this->_verify_final_state();
1073 this->_finalized = true;
1074 }
1075
1082 void _validate_argument_configuration() const {
1083 // step 1
1084 arg_ptr_t non_required_arg = nullptr;
1085 for (const auto& arg : this->_positional_args) {
1086 if (not arg->is_required()) {
1087 non_required_arg = arg;
1088 continue;
1089 }
1090
1091 if (non_required_arg and arg->is_required())
1092 throw invalid_configuration(std::format(
1093 "Required positional argument [{}] cannot be defined after a non-required "
1094 "positional argument [{}].",
1095 arg->name().str(),
1096 non_required_arg->name().str()
1097 ));
1098 }
1099 }
1100
1110 template <util::c_forward_iterator_of<std::string, util::type_validator::convertible> AIt>
1111 [[nodiscard]] arg_token_vec_t _tokenize(
1112 AIt args_begin, const AIt args_end, const parsing_state& state
1113 ) {
1114 arg_token_vec_t toks;
1115 toks.reserve(static_cast<std::size_t>(std::ranges::distance(args_begin, args_end)));
1116 std::ranges::for_each(args_begin, args_end, [&](const auto& arg_value) {
1117 this->_tokenize_arg(arg_value, toks, state);
1118 });
1119 return toks;
1120 }
1121
1128 void _tokenize_arg(
1129 const std::string_view arg_value, arg_token_vec_t& toks, const parsing_state& state
1130 ) {
1131 detail::argument_token tok{
1132 .type = this->_deduce_token_type(arg_value), .value = std::string(arg_value)
1133 };
1134
1135 if (not tok.is_flag_token() or this->_validate_flag_token(tok)) {
1136 toks.emplace_back(std::move(tok));
1137 return;
1138 }
1139
1140 // not a value token -> flag token
1141 // flag token could not be validated -> unknown flag
1142 if (state.parse_known_only) { // do nothing (will be handled during parsing)
1143 toks.emplace_back(std::move(tok));
1144 return;
1145 }
1146
1147 switch (this->_unknown_policy) {
1148 case unknown_policy::fail:
1149 throw parsing_failure::unknown_argument(tok.value);
1150 case unknown_policy::warn:
1151 std::cerr << "[argon::warning] Unknown argument '" << tok.value << "' will be ignored."
1152 << std::endl;
1153 [[fallthrough]];
1154 case unknown_policy::ignore:
1155 return;
1156 case unknown_policy::as_values:
1157 tok.type = detail::argument_token::t_value;
1158 toks.emplace_back(std::move(tok));
1159 break;
1160 }
1161 }
1162
1172 [[nodiscard]] detail::argument_token::token_type _deduce_token_type(
1173 const std::string_view arg_value
1174 ) const noexcept {
1175 if (util::contains_whitespaces(arg_value))
1176 return detail::argument_token::t_value;
1177
1178 if (arg_value.starts_with(this->_flag_prefix))
1179 return detail::argument_token::t_flag_primary;
1180
1181 if (arg_value.starts_with(this->_flag_prefix_char))
1182 return detail::argument_token::t_flag_secondary;
1183
1184 return detail::argument_token::t_value;
1185 }
1186
1193 [[nodiscard]] bool _validate_flag_token(detail::argument_token& tok) noexcept {
1194 const auto opt_arg_it = this->_find_opt_arg(tok);
1195 if (opt_arg_it == this->_optional_args.end())
1196 return this->_validate_compound_flag_token(tok);
1197
1198 tok.args.emplace_back(*opt_arg_it);
1199 return true;
1200 }
1201
1209 bool _validate_compound_flag_token(detail::argument_token& tok) noexcept {
1210 if (tok.type != detail::argument_token::t_flag_secondary)
1211 return false;
1212
1213 const auto actual_tok_value = this->_strip_flag_prefix(tok);
1214 tok.args.reserve(actual_tok_value.size());
1215
1216 for (const char c : actual_tok_value) {
1217 const auto opt_arg_it = std::ranges::find_if(
1218 this->_optional_args,
1219 this->_name_match_predicate(
1220 std::string_view(&c, 1ull), detail::argument_name::m_secondary
1221 )
1222 );
1223
1224 if (opt_arg_it == this->_optional_args.end()) {
1225 tok.args.clear();
1226 return false;
1227 }
1228
1229 tok.args.emplace_back(*opt_arg_it);
1230 }
1231
1232 tok.type = detail::argument_token::t_flag_compound;
1233 return true;
1234 }
1235
1242 [[nodiscard]] arg_ptr_vec_iter_t _find_opt_arg(const detail::argument_token& flag_tok
1243 ) noexcept {
1244 if (not flag_tok.is_flag_token())
1245 return this->_optional_args.end();
1246
1247 const auto actual_tok_value = this->_strip_flag_prefix(flag_tok);
1248 const auto match_type =
1249 flag_tok.type == detail::argument_token::t_flag_primary
1250 ? detail::argument_name::m_primary
1251 : detail::argument_name::m_secondary;
1252
1253 return std::ranges::find_if(
1254 this->_optional_args, this->_name_match_predicate(actual_tok_value, match_type)
1255 );
1256 }
1257
1263 [[nodiscard]] std::string_view _strip_flag_prefix(const detail::argument_token& tok
1264 ) const noexcept {
1265 switch (tok.type) {
1266 case detail::argument_token::t_flag_primary:
1267 return std::string_view(tok.value).substr(this->_primary_flag_prefix_length);
1268 case detail::argument_token::t_flag_secondary:
1269 return std::string_view(tok.value).substr(this->_secondary_flag_prefix_length);
1270 default:
1271 return tok.value;
1272 }
1273 }
1274
1281 void _parse_token(const detail::argument_token& tok, parsing_state& state) {
1282 if (state.curr_arg and state.curr_arg->is_greedy()) {
1283 this->_set_argument_value(tok.value, state);
1284 return;
1285 }
1286
1287 if (tok.is_flag_token())
1288 this->_parse_flag_token(tok, state);
1289 else
1290 this->_parse_value_token(tok, state);
1291 }
1292
1299 void _parse_flag_token(const detail::argument_token& tok, parsing_state& state) {
1300 if (not tok.is_valid_flag_token()) {
1301 if (state.parse_known_only) {
1302 state.curr_arg.reset();
1303 state.unknown_args.emplace_back(tok.value);
1304 return;
1305 }
1306 else {
1307 // should never happen as unknown flags are filtered out during tokenization
1308 throw parsing_failure::unknown_argument(tok.value);
1309 }
1310 }
1311
1312 for (const auto& arg : tok.args) {
1313 if (arg->mark_used())
1314 state.curr_arg = arg;
1315 else
1316 state.curr_arg.reset();
1317 }
1318 }
1319
1326 void _parse_value_token(const detail::argument_token& tok, parsing_state& state) {
1327 if (not state.curr_arg) {
1328 if (state.curr_pos_arg_it == this->_positional_args.end()) {
1329 state.unknown_args.emplace_back(tok.value);
1330 return;
1331 }
1332
1333 state.curr_arg = *state.curr_pos_arg_it;
1334 }
1335
1336 this->_set_argument_value(tok.value, state);
1337 }
1338
1345 void _set_argument_value(const std::string_view value, parsing_state& state) noexcept {
1346 if (state.curr_arg->set_value(std::string(value)))
1347 return; // argument still accepts values
1348
1349 // advance to the next positional argument if possible
1350 if (state.curr_arg->is_positional()
1351 and state.curr_pos_arg_it != this->_positional_args.end())
1352 ++state.curr_pos_arg_it;
1353
1354 state.curr_arg.reset();
1355 }
1356
1361 void _verify_final_state() const {
1362 const auto [supress_group_checks, suppress_arg_checks] = this->_are_checks_suppressed();
1363 for (const auto& group : this->_argument_groups)
1364 this->_verify_group_requirements(*group, supress_group_checks, suppress_arg_checks);
1365 }
1366
1373 [[nodiscard]] std::pair<bool, bool> _are_checks_suppressed() const noexcept {
1374 bool suppress_group_checks = false;
1375 bool suppress_arg_checks = false;
1376
1377 auto check_arg = [&](const arg_ptr_t& arg) {
1378 if (arg->is_used()) {
1379 if (arg->suppresses_group_checks())
1380 suppress_group_checks = true;
1381 if (arg->suppresses_arg_checks())
1382 suppress_arg_checks = true;
1383 }
1384 };
1385
1386 // TODO: use std::views::concat after the transition to C++26
1387 std::ranges::for_each(this->_positional_args, check_arg);
1388 std::ranges::for_each(this->_optional_args, check_arg);
1389 return {suppress_group_checks, suppress_arg_checks};
1390 }
1391
1398 void _verify_group_requirements(
1399 const argument_group& group,
1400 const bool suppress_group_checks,
1401 const bool suppress_arg_checks
1402 ) const {
1403 if (group._arguments.empty())
1404 return;
1405
1406 if (not suppress_group_checks) {
1407 const auto n_used_args = static_cast<std::size_t>(std::ranges::count_if(
1408 group._arguments, [](const auto& arg) { return arg->is_used(); }
1409 ));
1410
1411 if (group._mutually_exclusive) {
1412 if (n_used_args > 1ull)
1413 throw parsing_failure(std::format(
1414 "At most one argument from the mutually exclusive group '{}' can be used",
1415 group._name
1416 ));
1417
1418 const auto used_arg_it = std::ranges::find_if(
1419 group._arguments, [](const auto& arg) { return arg->is_used(); }
1420 );
1421
1422 if (used_arg_it != group._arguments.end()) {
1423 // only the one used argument has to be validated
1424 this->_verify_argument_requirements(*used_arg_it, suppress_arg_checks);
1425 return;
1426 }
1427 }
1428
1429 if (group._required and n_used_args == 0ull)
1430 throw parsing_failure(std::format(
1431 "At least one argument from the required group '{}' must be used", group._name
1432 ));
1433 }
1434
1435 // all arguments in the group have to be validated
1436 for (const auto& arg : group._arguments)
1437 this->_verify_argument_requirements(arg, suppress_arg_checks);
1438 }
1439
1446 void _verify_argument_requirements(const arg_ptr_t& arg, const bool suppress_arg_checks) const {
1447 if (suppress_arg_checks)
1448 return;
1449
1450 if (arg->is_required() and not arg->has_value())
1451 throw parsing_failure(
1452 std::format("No values parsed for a required argument [{}]", arg->name().str())
1453 );
1454 if (const auto nv_ord = arg->nvalues_ordering(); not std::is_eq(nv_ord))
1455 throw parsing_failure::invalid_nvalues(arg->name(), nv_ord);
1456 }
1457
1463 arg_ptr_t _get_argument(std::string_view arg_name) const noexcept {
1464 const auto predicate = this->_name_match_predicate(arg_name);
1465
1466 if (auto pos_arg_it = std::ranges::find_if(this->_positional_args, predicate);
1467 pos_arg_it != this->_positional_args.end()) {
1468 return *pos_arg_it;
1469 }
1470
1471 if (auto opt_arg_it = std::ranges::find_if(this->_optional_args, predicate);
1472 opt_arg_it != this->_optional_args.end()) {
1473 return *opt_arg_it;
1474 }
1475
1476 return nullptr;
1477 }
1478
1479 void _print_subparsers(std::ostream& os) const noexcept {
1480 if (this->_subparsers.empty())
1481 return;
1482
1483 os << "\nCommands:\n";
1484
1485 std::vector<detail::help_builder> builders;
1486 builders.reserve(this->_subparsers.size());
1487
1488 for (const auto& subparser : this->_subparsers)
1489 builders.emplace_back(subparser->_name, subparser->_program_description);
1490
1491 std::size_t max_subparser_name_length = 0ull;
1492 for (const auto& bld : builders)
1493 max_subparser_name_length = std::max(max_subparser_name_length, bld.name.length());
1494
1495 for (const auto& bld : builders)
1496 os << '\n' << bld.get_basic(this->_indent_width, max_subparser_name_length);
1497
1498 os << '\n';
1499 }
1500
1508 void _print_group(std::ostream& os, const argument_group& group, const bool verbose)
1509 const noexcept {
1510 if (group._hidden)
1511 return;
1512
1513 auto visible_args = std::views::filter(group._arguments, [](const auto& arg) {
1514 return not arg->is_hidden();
1515 });
1516
1517 if (std::ranges::empty(visible_args))
1518 return;
1519
1520 os << '\n' << group._name << ':';
1521
1522 std::vector<std::string_view> group_attrs;
1523 if (group._required)
1524 group_attrs.emplace_back("required");
1525 if (group._mutually_exclusive)
1526 group_attrs.emplace_back("mutually exclusive");
1527 if (not group_attrs.empty())
1528 os << " (" << util::join(group_attrs) << ')';
1529 os << '\n';
1530
1531 if (verbose) {
1532 for (const auto& arg : visible_args)
1533 os << '\n' << arg->help_builder(verbose).get(this->_indent_width) << '\n';
1534 }
1535 else {
1536 std::vector<detail::help_builder> builders;
1537 builders.reserve(group._arguments.size());
1538
1539 for (const auto& arg : visible_args)
1540 builders.emplace_back(arg->help_builder(verbose));
1541
1542 std::size_t max_arg_name_length = 0ull;
1543 for (const auto& bld : builders)
1544 max_arg_name_length = std::max(max_arg_name_length, bld.name.length());
1545
1546 for (const auto& bld : builders)
1547 os << '\n' << bld.get_basic(this->_indent_width, max_arg_name_length);
1548
1549 os << '\n';
1550 }
1551 }
1552
1553 std::string _name;
1554 std::string
1555 _program_name;
1556 std::optional<std::string> _program_version;
1557 std::optional<std::string> _program_description;
1558 bool _verbose = false;
1559 unknown_policy _unknown_policy = unknown_policy::fail;
1560
1561 arg_ptr_vec_t _positional_args;
1562 arg_ptr_vec_t _optional_args;
1563 arg_group_ptr_vec_t _argument_groups;
1564 argument_group& _gr_positional_args;
1565 argument_group& _gr_optional_args;
1566 arg_parser_ptr_vec_t _subparsers;
1567
1568 bool _invoked =
1569 false;
1570 bool _finalized = false;
1571
1572 static constexpr std::uint8_t _primary_flag_prefix_length = 2u;
1573 static constexpr std::uint8_t _secondary_flag_prefix_length = 1u;
1574 static constexpr char _flag_prefix_char = '-';
1575 static constexpr std::string_view _flag_prefix = "--";
1576 static constexpr std::uint8_t _indent_width = 2;
1577};
1578
1579namespace detail {
1580
1586inline void add_default_argument(
1587 const default_argument arg_discriminator, argument_parser& arg_parser
1588) noexcept {
1589 switch (arg_discriminator) {
1591 arg_parser.add_positional_argument("input")
1592 .action<action_type::observe>(action::check_file_exists())
1593 .help("Input file path");
1594 break;
1595
1597 arg_parser.add_positional_argument("output").help("Output file path");
1598 break;
1599
1601 arg_parser.add_optional_argument<none_type>("help", "h")
1603 .help("Display the help message");
1604 break;
1605
1607 arg_parser.add_optional_argument<none_type>("version", "v")
1609 arg_parser.print_version();
1610 std::exit(EXIT_SUCCESS);
1611 })
1612 .help("Dsiplay program version info");
1613 break;
1614
1616 arg_parser.add_optional_argument("input", "i")
1617 .nargs(1ull)
1618 .action<action_type::observe>(action::check_file_exists())
1619 .help("Input file path");
1620 break;
1621
1623 arg_parser.add_optional_argument("output", "o").nargs(1ull).help("Output file path");
1624 break;
1625
1627 arg_parser.add_optional_argument("input", "i")
1628 .nargs(argon::nargs::at_least(1ull))
1629 .action<action_type::observe>(action::check_file_exists())
1630 .help("Input files paths");
1631 break;
1632
1634 arg_parser.add_optional_argument("output", "o")
1635 .nargs(argon::nargs::at_least(1ull))
1636 .help("Output files paths");
1637 break;
1638 }
1639}
1640
1641} // namespace detail
1642
1643} // namespace argon
argon::action_type::on_flag::type print_help(const argument_parser &parser, const std::optional< int > exit_code=std::nullopt, std::ostream &os=std::cout) noexcept
Returns an on-flag action which prints the argument parser's help message.
util::callable_type< argon::action_type::observe, std::string > check_file_exists() noexcept
Returns an observe action which checks whether lower_bound file with the given name exists.
void add_default_argument(const default_argument, argument_parser &) noexcept
@ n_primary
Represents the primary name (used with a long flag prefix –).
constexpr range at_least(const count_type n) noexcept
range class builder function. Creates a range [n, inf).
Definition range.hpp:128
std::ostream & operator<<(std::ostream &os, const argument_parser &) noexcept
default_argument
The enumeration of default arguments provided by the library.
@ p_output
A positional argument representing a single output file path. Equivalent to:
@ o_help
An optional argument representing the program's help flag. Equivalent to:
@ p_input
A positional argument representing a single input file path. Equivalent to:
@ o_input
A positional argument representing multiple input file paths. Equivalent to:
@ o_multi_input
A positional argument representing multiple input file paths. Equivalent to:
@ o_version
An optional argument representing the program's version flag. Equivalent to:
@ o_output
A positional argument representing multiple output file paths. Equivalent to:
@ o_multi_output
A positional argument representing multiple output file paths. Equivalent to:
unknown_policy
The enumeration of policies for handling unknown arguments.
@ warn
Issue a warning when an unknown argument is encountered.
@ as_values
Treat unknown arguments as positional values.
@ ignore
Ignore unknown arguments.
@ fail
Throw an exception when an unknown argument is encountered.
Provides common ranges utility functions.
Base type for the argument parser functionality errors/exceptions.