CPP-AP 2.2.6
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-2025 Jakub MusiaƂ
2// This file is part of the CPP-AP project (https://github.com/SpectraL519/cpp-ap).
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 "argument/default.hpp"
13#include "argument/optional.hpp"
16#include "detail/concepts.hpp"
17
18#include <algorithm>
19#include <format>
20#include <ranges>
21#include <span>
22
23#ifdef AP_TESTING
24
25namespace ap_testing {
26struct argument_parser_test_fixture;
27} // namespace ap_testing
28
29#endif
30
31namespace ap {
32
33// TODO: argument namespace alias
34
35class argument_parser;
36
37namespace detail {
38
39void add_default_argument(const argument::default_positional, argument_parser&) noexcept;
40void add_default_argument(const argument::default_optional, argument_parser&) noexcept;
41
42} // namespace detail
43
46public:
47 argument_parser(const argument_parser&) = delete;
48 argument_parser& operator=(const argument_parser&) = delete;
49
50 argument_parser() = default;
51
53 argument_parser& operator=(argument_parser&&) = default;
54
55 ~argument_parser() = default;
56
62 argument_parser& program_name(std::string_view name) noexcept {
63 this->_program_name = name;
64 return *this;
65 }
66
72 argument_parser& program_description(std::string_view description) noexcept {
73 this->_program_description = description;
74 return *this;
75 }
76
83 argument_parser& verbose(const bool v = true) noexcept {
84 this->_verbose = v;
85 return *this;
86 }
87
94 template <detail::c_range_of<argument::default_positional> AR>
95 argument_parser& default_positional_arguments(const AR& arg_discriminator_range) noexcept {
96 for (const auto arg_discriminator : arg_discriminator_range)
97 detail::add_default_argument(arg_discriminator, *this);
98 return *this;
99 }
100
107 const std::initializer_list<argument::default_positional> arg_discriminator_list
108 ) noexcept {
109 return this->default_positional_arguments<>(arg_discriminator_list);
110 }
111
118 template <detail::c_range_of<argument::default_optional> AR>
119 argument_parser& default_optional_arguments(const AR& arg_discriminator_range) noexcept {
120 for (const auto arg_discriminator : arg_discriminator_range)
121 detail::add_default_argument(arg_discriminator, *this);
122 return *this;
123 }
124
131 const std::initializer_list<argument::default_optional> arg_discriminator_list
132 ) noexcept {
133 return this->default_optional_arguments<>(arg_discriminator_list);
134 }
135
145 template <detail::c_argument_value_type T = std::string>
146 argument::positional<T>& add_positional_argument(std::string_view primary_name) {
147 this->_verify_arg_name_pattern(primary_name);
148
149 const detail::argument_name arg_name = {primary_name};
150 if (this->_is_arg_name_used(arg_name))
151 throw invalid_configuration::argument_name_used(arg_name);
152
153 this->_positional_args.emplace_back(std::make_unique<argument::positional<T>>(arg_name));
154 return static_cast<argument::positional<T>&>(*this->_positional_args.back());
155 }
156
167 template <detail::c_argument_value_type T = std::string>
169 std::string_view primary_name, std::string_view secondary_name
170 ) {
171 this->_verify_arg_name_pattern(primary_name);
172 this->_verify_arg_name_pattern(secondary_name);
173
174 const detail::argument_name arg_name = {primary_name, secondary_name};
175 if (this->_is_arg_name_used(arg_name))
176 throw invalid_configuration::argument_name_used(arg_name);
177
178 this->_positional_args.emplace_back(std::make_unique<argument::positional<T>>(arg_name));
179 return static_cast<argument::positional<T>&>(*this->_positional_args.back());
180 }
181
191 template <detail::c_argument_value_type T = std::string>
192 argument::optional<T>& add_optional_argument(std::string_view primary_name) {
193 this->_verify_arg_name_pattern(primary_name);
194
195 const detail::argument_name arg_name = {primary_name};
196 if (this->_is_arg_name_used(arg_name))
197 throw invalid_configuration::argument_name_used(arg_name);
198
199 this->_optional_args.push_back(std::make_unique<argument::optional<T>>(arg_name));
200 return static_cast<argument::optional<T>&>(*this->_optional_args.back());
201 }
202
213 template <detail::c_argument_value_type T = std::string>
215 std::string_view primary_name, std::string_view secondary_name
216 ) {
217 this->_verify_arg_name_pattern(primary_name);
218 this->_verify_arg_name_pattern(secondary_name);
219
220 const detail::argument_name arg_name = {primary_name, secondary_name};
221 if (this->_is_arg_name_used(arg_name))
222 throw invalid_configuration::argument_name_used(arg_name);
223
224 this->_optional_args.emplace_back(std::make_unique<argument::optional<T>>(arg_name));
225 return static_cast<argument::optional<T>&>(*this->_optional_args.back());
226 }
227
234 template <bool StoreImplicitly = true>
235 argument::optional<bool>& add_flag(std::string_view primary_name) {
236 return this->add_optional_argument<bool>(primary_name)
237 .default_value(not StoreImplicitly)
238 .implicit_value(StoreImplicitly)
239 .nargs(0ull);
240 }
241
249 template <bool StoreImplicitly = true>
251 std::string_view primary_name, std::string_view secondary_name
252 ) {
253 return this->add_optional_argument<bool>(primary_name, secondary_name)
254 .default_value(not StoreImplicitly)
255 .implicit_value(StoreImplicitly)
256 .nargs(0ull);
257 }
258
265 void parse_args(int argc, char* argv[]) {
266 this->parse_args(std::span(argv + 1, static_cast<std::size_t>(argc - 1)));
267 }
268
274 template <detail::c_range_of<std::string, detail::type_validator::convertible> AR>
275 void parse_args(const AR& argv) {
276 this->_parse_args_impl(this->_tokenize(argv));
277
278 if (this->_are_required_args_bypassed())
279 return;
280
281 this->_verify_required_args();
282 this->_verify_nvalues();
283 }
284
291 void try_parse_args(int argc, char* argv[]) {
292 this->try_parse_args(std::span(argv + 1, static_cast<std::size_t>(argc - 1)));
293 }
294
305 template <detail::c_range_of<std::string, detail::type_validator::convertible> AR>
306 void try_parse_args(const AR& argv) {
307 try {
308 this->parse_args(argv);
309 }
310 catch (const ap::argument_parser_exception& err) {
311 std::cerr << "[ERROR] : " << err.what() << std::endl << *this << std::endl;
312 std::exit(EXIT_FAILURE);
313 }
314 }
315
322 // clang-format off
323 [[deprecated("The default help argument now uses the `print_config` on-flag action")]]
324 void handle_help_action() const noexcept {
325 if (this->value<bool>("help")) {
326 std::cout << *this << std::endl;
327 std::exit(EXIT_SUCCESS);
328 }
329 }
330
331 // clang-format on
332
337 [[nodiscard]] bool has_value(std::string_view arg_name) const noexcept {
338 const auto arg_opt = this->_get_argument(arg_name);
339 return arg_opt ? arg_opt->get().has_value() : false;
340 }
341
346 [[nodiscard]] std::size_t count(std::string_view arg_name) const noexcept {
347 const auto arg_opt = this->_get_argument(arg_name);
348 return arg_opt ? arg_opt->get().count() : 0ull;
349 }
350
357 template <detail::c_argument_value_type T = std::string>
358 [[nodiscard]] T value(std::string_view arg_name) const {
359 const auto arg_opt = this->_get_argument(arg_name);
360 if (not arg_opt)
361 throw lookup_failure::argument_not_found(arg_name);
362
363 const auto& arg_value = arg_opt->get().value();
364 try {
365 return std::any_cast<T>(arg_value);
366 }
367 catch (const std::bad_any_cast& err) {
368 throw type_error::invalid_value_type(arg_opt->get().name(), typeid(T));
369 }
370 }
371
380 template <detail::c_argument_value_type T = std::string, std::convertible_to<T> U>
381 [[nodiscard]] T value_or(std::string_view arg_name, U&& default_value) const {
382 const auto arg_opt = this->_get_argument(arg_name);
383 if (not arg_opt)
384 throw lookup_failure::argument_not_found(arg_name);
385
386 try {
387 const auto& arg_value = arg_opt->get().value();
388 return std::any_cast<T>(arg_value);
389 }
390 catch (const std::logic_error&) {
391 // positional: no value parsed
392 // optional: no value parsed + no predefined value
393 return T{std::forward<U>(default_value)};
394 }
395 catch (const std::bad_any_cast& err) {
396 throw type_error::invalid_value_type(arg_opt->get().name(), typeid(T));
397 }
398 }
399
406 template <detail::c_argument_value_type T = std::string>
407 [[nodiscard]] std::vector<T> values(std::string_view arg_name) const {
408 const auto arg_opt = this->_get_argument(arg_name);
409 if (not arg_opt)
410 throw lookup_failure::argument_not_found(arg_name);
411
412 const auto& arg = arg_opt->get();
413
414 try {
415 if (not arg.has_parsed_values() and arg.has_value())
416 return std::vector<T>{std::any_cast<T>(arg.value())};
417
418 std::vector<T> values;
419 // TODO: use std::ranges::to after transition to C++23
420 std::ranges::copy(
421 std::views::transform(
422 arg.values(), [](const std::any& value) { return std::any_cast<T>(value); }
423 ),
424 std::back_inserter(values)
425 );
426 return values;
427 }
428 catch (const std::bad_any_cast& err) {
429 throw type_error::invalid_value_type(arg.name(), typeid(T));
430 }
431 }
432
438 void print_config(const bool verbose, std::ostream& os = std::cout) const noexcept {
439 if (this->_program_name)
440 os << "Program: " << this->_program_name.value() << std::endl;
441
442 if (this->_program_description)
443 os << "\n"
444 << std::string(this->_indent_width, ' ') << this->_program_description.value()
445 << std::endl;
446
447 if (not this->_positional_args.empty()) {
448 os << "\nPositional arguments:\n";
449 this->_print(os, this->_positional_args, verbose);
450 }
451
452 if (not this->_optional_args.empty()) {
453 os << "\nOptional arguments:\n";
454 this->_print(os, this->_optional_args, verbose);
455 }
456 }
457
468 friend std::ostream& operator<<(std::ostream& os, const argument_parser& parser) noexcept {
469 parser.print_config(parser._verbose, os);
470 return os;
471 }
472
473#ifdef AP_TESTING
475 friend struct ::ap_testing::argument_parser_test_fixture;
476#endif
477
478private:
479 using arg_ptr_t = std::unique_ptr<detail::argument_base>;
480 using arg_ptr_list_t = std::vector<arg_ptr_t>;
481 using arg_opt_t = std::optional<std::reference_wrapper<detail::argument_base>>;
482
483 using arg_token_list_t = std::vector<detail::argument_token>;
484 using arg_token_list_iterator_t = typename arg_token_list_t::const_iterator;
485
490 void _verify_arg_name_pattern(const std::string_view arg_name) const {
491 if (arg_name.empty())
492 throw invalid_configuration::invalid_argument_name(
493 arg_name, "An argument name cannot be empty."
494 );
495
496 if (arg_name.front() == this->_flag_prefix_char)
497 throw invalid_configuration::invalid_argument_name(
498 arg_name,
499 std::format(
500 "An argument name cannot begin with a flag prefix character ({}).",
501 this->_flag_prefix_char
502 )
503 );
504
505 if (std::isdigit(arg_name.front()))
506 throw invalid_configuration::invalid_argument_name(
507 arg_name, "An argument name cannot begin with a digit."
508 );
509 }
510
516 [[nodiscard]] auto _name_match_predicate(const std::string_view arg_name) const noexcept {
517 return [arg_name](const arg_ptr_t& arg) { return arg->name().match(arg_name); };
518 }
519
525 [[nodiscard]] auto _name_match_predicate(const detail::argument_name& arg_name) const noexcept {
526 return [&arg_name](const arg_ptr_t& arg) { return arg->name().match(arg_name); };
527 }
528
534 [[nodiscard]] bool _is_arg_name_used(const detail::argument_name& arg_name) const noexcept {
535 const auto predicate = this->_name_match_predicate(arg_name);
536
537 if (std::ranges::find_if(this->_positional_args, predicate) != this->_positional_args.end())
538 return true;
539
540 if (std::ranges::find_if(this->_optional_args, predicate) != this->_optional_args.end())
541 return true;
542
543 return false;
544 }
545
552 template <detail::c_sized_range_of<std::string, detail::type_validator::convertible> AR>
553 [[nodiscard]] arg_token_list_t _tokenize(const AR& arg_range) const noexcept {
554 const auto n_args = std::ranges::size(arg_range);
555 if (n_args == 0ull)
556 return arg_token_list_t{};
557
558 arg_token_list_t toks;
559 toks.reserve(n_args);
560
561 for (const auto& arg : arg_range) {
562 std::string value = static_cast<std::string>(arg);
563 if (this->_is_flag(value)) {
564 this->_strip_flag_prefix(value);
565 toks.emplace_back(detail::argument_token::t_flag, std::move(value));
566 }
567 else {
568 toks.emplace_back(detail::argument_token::t_value, std::move(value));
569 }
570 }
571
572 return toks;
573 }
574
580 [[nodiscard]] bool _is_flag(const std::string& arg) const noexcept {
581 if (arg.starts_with(this->_flag_prefix))
582 return this->_is_arg_name_used({arg.substr(this->_primary_flag_prefix_length)});
583
584 if (arg.starts_with(this->_flag_prefix_char))
585 return this->_is_arg_name_used({arg.substr(this->_secondary_flag_prefix_length)});
586
587 return false;
588 }
589
594 void _strip_flag_prefix(std::string& arg_flag) const noexcept {
595 if (arg_flag.starts_with(this->_flag_prefix))
596 arg_flag.erase(0, this->_primary_flag_prefix_length);
597 else
598 arg_flag.erase(0, this->_secondary_flag_prefix_length);
599 }
600
606 void _parse_args_impl(const arg_token_list_t& arg_tokens) {
607 arg_token_list_iterator_t token_it = arg_tokens.begin();
608
609 this->_parse_positional_args(token_it, arg_tokens.end());
610
611 std::vector<std::string_view> dangling_values;
612 this->_parse_optional_args(token_it, arg_tokens.end(), dangling_values);
613
614 if (not dangling_values.empty())
615 throw parsing_failure::argument_deduction_failure(dangling_values);
616 }
617
623 void _parse_positional_args(
624 arg_token_list_iterator_t& token_it, const arg_token_list_iterator_t& tokens_end
625 ) noexcept {
626 for (const auto& pos_arg : this->_positional_args) {
627 if (token_it == tokens_end)
628 return;
629
630 if (token_it->type == detail::argument_token::t_flag)
631 return;
632
633 pos_arg->set_value(token_it->value);
634 ++token_it;
635 }
636 }
637
646 void _parse_optional_args(
647 arg_token_list_iterator_t& token_it,
648 const arg_token_list_iterator_t& tokens_end,
649 std::vector<std::string_view>& dangling_values
650 ) {
651 std::optional<std::reference_wrapper<arg_ptr_t>> curr_opt_arg;
652
653 while (token_it != tokens_end) {
654 switch (token_it->type) {
655 case detail::argument_token::t_flag: {
656 if (not dangling_values.empty())
657 throw parsing_failure::argument_deduction_failure(dangling_values);
658
659 auto opt_arg_it = std::ranges::find_if(
660 this->_optional_args, this->_name_match_predicate(token_it->value)
661 );
662
663 if (opt_arg_it == this->_optional_args.end())
664 throw parsing_failure::unknown_argument(token_it->value);
665
666 curr_opt_arg = std::ref(*opt_arg_it);
667 if (not curr_opt_arg->get()->mark_used())
668 curr_opt_arg.reset();
669
670 break;
671 }
672 case detail::argument_token::t_value: {
673 if (not curr_opt_arg) {
674 dangling_values.emplace_back(token_it->value);
675 break;
676 }
677
678 if (not curr_opt_arg->get()->set_value(token_it->value))
679 curr_opt_arg.reset();
680
681 break;
682 }
683 }
684
685 ++token_it;
686 }
687 }
688
693 [[nodiscard]] bool _are_required_args_bypassed() const noexcept {
694 return std::ranges::any_of(this->_optional_args, [](const arg_ptr_t& arg) {
695 return arg->is_used() and arg->bypass_required_enabled();
696 });
697 }
698
703 void _verify_required_args() const {
704 for (const auto& arg : this->_positional_args)
705 if (not arg->is_used()) // ? use has_parsed_values
706 throw parsing_failure::required_argument_not_parsed(arg->name());
707
708 for (const auto& arg : this->_optional_args)
709 if (arg->is_required() and not arg->has_value())
710 throw parsing_failure::required_argument_not_parsed(arg->name());
711 }
712
717 void _verify_nvalues() const {
718 for (const auto& arg : this->_positional_args)
719 if (const auto nv_ord = arg->nvalues_ordering(); not std::is_eq(nv_ord))
720 throw parsing_failure::invalid_nvalues(arg->name(), nv_ord);
721
722 for (const auto& arg : this->_optional_args)
723 if (const auto nv_ord = arg->nvalues_ordering(); not std::is_eq(nv_ord))
724 throw parsing_failure::invalid_nvalues(arg->name(), nv_ord);
725 }
726
732 arg_opt_t _get_argument(std::string_view arg_name) const noexcept {
733 const auto predicate = this->_name_match_predicate(arg_name);
734
735 if (auto pos_arg_it = std::ranges::find_if(this->_positional_args, predicate);
736 pos_arg_it != this->_positional_args.end()) {
737 return std::ref(**pos_arg_it);
738 }
739
740 if (auto opt_arg_it = std::ranges::find_if(this->_optional_args, predicate);
741 opt_arg_it != this->_optional_args.end()) {
742 return std::ref(**opt_arg_it);
743 }
744
745 return std::nullopt;
746 }
747
753 void _print(std::ostream& os, const arg_ptr_list_t& args, const bool verbose) const noexcept {
754 if (verbose) {
755 for (const auto& arg : args)
756 os << '\n'
757 << arg->desc(verbose, this->_flag_prefix_char).get(this->_indent_width) << '\n';
758 }
759 else {
760 std::vector<detail::argument_descriptor> descriptors;
761 descriptors.reserve(args.size());
762
763 for (const auto& arg : args)
764 descriptors.emplace_back(arg->desc(verbose, this->_flag_prefix_char));
765
766 std::size_t max_arg_name_length = 0ull;
767 for (const auto& desc : descriptors)
768 max_arg_name_length = std::max(max_arg_name_length, desc.name.length());
769
770 for (const auto& desc : descriptors)
771 os << '\n' << desc.get_basic(this->_indent_width, max_arg_name_length);
772
773 os << '\n';
774 }
775 }
776
777 std::optional<std::string> _program_name;
778 std::optional<std::string> _program_description;
779 bool _verbose = false;
780
781 arg_ptr_list_t _positional_args;
782 arg_ptr_list_t _optional_args;
783
784 static constexpr uint8_t _primary_flag_prefix_length = 2u;
785 static constexpr uint8_t _secondary_flag_prefix_length = 1u;
786 static constexpr char _flag_prefix_char = '-';
787 static constexpr std::string _flag_prefix = "--";
788 static constexpr uint8_t _indent_width = 2;
789};
790
791namespace detail {
792
799 const argument::default_positional arg_discriminator, argument_parser& arg_parser
800) noexcept {
801 switch (arg_discriminator) {
802 case argument::default_positional::input:
803 arg_parser.add_positional_argument("input")
804 .action<action_type::observe>(action::check_file_exists())
805 .help("Input file path");
806 break;
807
808 case argument::default_positional::output:
809 arg_parser.add_positional_argument("output").help("Output file path");
810 break;
811 }
812}
813
820 const argument::default_optional arg_discriminator, argument_parser& arg_parser
821) noexcept {
822 switch (arg_discriminator) {
823 case argument::default_optional::help:
824 arg_parser.add_flag("help", "h")
825 .action<action_type::on_flag>(action::print_config(arg_parser, EXIT_SUCCESS))
826 .help("Display the help message");
827 break;
828
829 case argument::default_optional::input:
830 arg_parser.add_optional_argument("input", "i")
831 .required()
832 .nargs(1)
833 .action<action_type::observe>(action::check_file_exists())
834 .help("Input file path");
835 break;
836
837 case argument::default_optional::output:
838 arg_parser.add_optional_argument("output", "o").required().nargs(1).help("Output file path");
839 break;
840
841 case argument::default_optional::multi_input:
842 arg_parser.add_optional_argument("input", "i")
843 .required()
844 .nargs(ap::nargs::at_least(1))
845 .action<action_type::observe>(action::check_file_exists())
846 .help("Input files paths");
847 break;
848
849 case argument::default_optional::multi_output:
850 arg_parser.add_optional_argument("output", "o")
851 .required()
852 .nargs(ap::nargs::at_least(1))
853 .help("Output files paths");
854 break;
855 }
856}
857
858} // namespace detail
859
860} // namespace ap
void add_default_argument(const argument::default_positional, argument_parser &) noexcept
Adds a predefined/default positional argument to the parser.
The optioanl argument class.
Definition optional.hpp:31
The positional argument class.
Main argument parser class.
argument_parser & default_positional_arguments(const AR &arg_discriminator_range) noexcept
Set default positional arguments.
argument_parser & default_positional_arguments(const std::initializer_list< argument::default_positional > arg_discriminator_list) noexcept
Set default positional arguments.
argument::optional< T > & add_optional_argument(std::string_view primary_name)
Adds a positional argument to the parser's configuration.
void print_config(const bool verbose, std::ostream &os=std::cout) const noexcept
Prints the argument parser's details to an output stream.
argument_parser & program_description(std::string_view description) noexcept
Set the program description.
T value(std::string_view arg_name) const
argument_parser & program_name(std::string_view name) noexcept
Set the program name.
bool has_value(std::string_view arg_name) const noexcept
std::vector< T > values(std::string_view arg_name) const
argument::positional< T > & add_positional_argument(std::string_view primary_name, std::string_view secondary_name)
Adds a positional argument to the parser's configuration.
argument::positional< T > & add_positional_argument(std::string_view primary_name)
Adds a positional argument to the parser's configuration.
argument_parser & verbose(const bool v=true) noexcept
Set the verbosity mode.
argument_parser & default_optional_arguments(const AR &arg_discriminator_range) noexcept
Set default optional arguments.
argument::optional< bool > & add_flag(std::string_view primary_name)
Adds a boolean flag argument (an optional argument with value_type = bool) to the parser's configurat...
void try_parse_args(int argc, char *argv[])
Parses the command-line arguments and exits on error.
void handle_help_action() const noexcept
Handles the help argument logic.
void try_parse_args(const AR &argv)
Parses the command-line arguments and exits on error.
T value_or(std::string_view arg_name, U &&default_value) const
argument_parser & default_optional_arguments(const std::initializer_list< argument::default_optional > arg_discriminator_list) noexcept
Set default optional arguments.
std::size_t count(std::string_view arg_name) const noexcept
argument::optional< bool > & add_flag(std::string_view primary_name, std::string_view secondary_name)
Adds a boolean flag argument (an optional argument with value_type = bool) to the parser's configurat...
void parse_args(const AR &argv)
Parses the command-line arguments.
friend std::ostream & operator<<(std::ostream &os, const argument_parser &parser) noexcept
Prints the argument parser's details to an output stream.
void parse_args(int argc, char *argv[])
Parses the command-line arguments.
argument::optional< T > & add_optional_argument(std::string_view primary_name, std::string_view secondary_name)
Adds a positional argument to the parser's configuration.
Provides the general concept definitions.
Defines the default argument discriminator types.
default_positional
Enum class representing positional arguments.
Definition default.hpp:17
default_optional
Enum class representing optional arguments.
Definition default.hpp:20
An observing value action specifier.
An on-flag action specifier.
Base type for the argument parser functionality errors/exceptions.
Structure holding the argument's name.