diff --git a/CMakeLists.txt b/CMakeLists.txt index 618e338..ca38425 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) include(cmake/color.cmake) -set(BLT_VERSION 5.2.21) +set(BLT_VERSION 5.2.22) set(BLT_TARGET BLT) diff --git a/libraries/parallel-hashmap b/libraries/parallel-hashmap index 154c634..93201da 160000 --- a/libraries/parallel-hashmap +++ b/libraries/parallel-hashmap @@ -1 +1 @@ -Subproject commit 154c63489e84d5569d3b466342a2ae8fd99e4734 +Subproject commit 93201da2ba5a6aba0a6e57ada64973555629b3e3 diff --git a/src/blt/parse/argparse_v2.cpp b/src/blt/parse/argparse_v2.cpp index 446ac86..5694868 100644 --- a/src/blt/parse/argparse_v2.cpp +++ b/src/blt/parse/argparse_v2.cpp @@ -28,1630 +28,1564 @@ namespace blt::argparse { - constexpr static auto printer_primitive = [](const auto& v) - { - std::cout << v; - }; + constexpr static auto printer_primitive = [](const auto& v) { + std::cout << v; + }; - constexpr static auto printer_vector = [](const auto& v) - { - std::cout << "["; - for (const auto& [i, a] : enumerate(v)) - { - std::cout << a; - if (i != v.size() - 1) - std::cout << ", "; - } - std::cout << "]"; - }; + constexpr static auto printer_vector = [](const auto& v) { + std::cout << "["; + for (const auto& [i, a] : enumerate(v)) + { + std::cout << a; + if (i != v.size() - 1) + std::cout << ", "; + } + std::cout << "]"; + }; - auto print_visitor = detail::arg_meta_type_helper_t::make_visitor(printer_primitive, printer_vector); + auto print_visitor = detail::arg_meta_type_helper_t::make_visitor(printer_primitive, printer_vector); - template - size_t get_const_char_size(const T& t) - { - if constexpr (std::is_convertible_v) - { - return std::char_traits::length(t); - } - else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) - { - return 1; - } - else if constexpr (std::is_same_v || std::is_same_v) - { - return t.size(); - } - else - { - return 0; - } - } + template + size_t get_const_char_size(const T& t) + { + if constexpr (std::is_convertible_v) + { + return std::char_traits::length(t); + } else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) + { + return 1; + } else if constexpr (std::is_same_v || std::is_same_v) + { + return t.size(); + } else + { + return 0; + } + } - template - std::string to_string(const T& t) - { - if constexpr (std::is_same_v || std::is_same_v) - { - return std::string(t); - } - else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) - { - std::string str; - str += t; - return str; - } - else - { - return t; - } - } + template + std::string to_string(const T& t) + { + if constexpr (std::is_same_v || std::is_same_v) + { + return std::string(t); + } else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) + { + std::string str; + str += t; + return str; + } else + { + return t; + } + } - template - std::string make_string(Strings&&... strings) - { - std::string out; - out.reserve((get_const_char_size(strings) + ...)); - ((out += detail::ensure_is_string(std::forward(strings))), ...); - return out; - } + template + std::string make_string(Strings&&... strings) + { + std::string out; + out.reserve((get_const_char_size(strings) + ...)); + ((out += detail::ensure_is_string(std::forward(strings))), ...); + return out; + } - template - std::vector make_arguments(Strings... strings) - { - return std::vector{"./program", strings...}; - } + template + std::vector make_arguments(Strings... strings) + { + return std::vector{"./program", strings...}; + } - class aligned_internal_string_t - { - public: - explicit aligned_internal_string_t(std::string& str, const size_t max_line_length, - const size_t line_start_size): string(str), max_line_size(max_line_length), - line_start_size(line_start_size) - { - } + class aligned_internal_string_t + { + public: + explicit aligned_internal_string_t(std::string& str, const size_t max_line_length, const size_t line_start_size): string(str), + max_line_size( + max_line_length), + line_start_size( + line_start_size) + {} - void add(const std::string_view str) const - { - const auto lines = string::split(string, '\n'); - if (lines.empty()) - { - string += str; - return; - } - if (lines.back().size() + str.size() > max_line_size) - { - string += '\n'; - for (size_t i = 0; i < line_start_size; i++) - string += ' '; - bool blank = true; - // we don't want to write blank only strings - for (const char c : str) + void add(const std::string_view str) const + { + const auto lines = string::split(string, '\n'); + if (lines.empty()) + { + string += str; + return; + } + if (lines.back().size() + str.size() > max_line_size) + { + string += '\n'; + for (size_t i = 0; i < line_start_size; i++) + string += ' '; + bool blank = true; + // we don't want to write blank only strings + for (const char c : str) + { + if (!std::isblank(c)) + { + blank = false; + break; + } + } + if (blank) + return; + } + + string += str; + } + + template + aligned_internal_string_t& operator+=(T&& value) + { + const auto str = to_string(detail::ensure_is_string(std::forward(value))); + for (size_t i = 0; i < str.size(); i++) + { + size_t j = i; + for (; j < str.size() && !std::isblank(str[j]); ++j) + {} + add(std::string_view(str.data() + i, j - i)); + if (j < str.size()) + add(std::string_view(str.data() + j, 1)); + i = j; + } + return *this; + } + + [[nodiscard]] std::string& str() const + { + return string; + } + + private: + std::string& string; + size_t max_line_size; + size_t line_start_size; + }; + + class aligner_t + { + public: + aligner_t(std::vector& buffer, const size_t start_index, const size_t max_line_size): buffer(buffer), start_index(start_index), + max_line_size(max_line_size) + {} + + void align(const size_t spaces_between) const + { + const size_t take = compute_take(); + size_t aligned_size = 0; + for (const auto& v : iterate(buffer).skip(start_index).take(take)) + { + auto size = static_cast(v.size()); + for (; size > 0 && std::isblank(v[size - 1]); size--) + {} + aligned_size = std::max(aligned_size, static_cast(size)); + } + const auto offset_size = aligned_size + spaces_between; + + for (auto& v : iterate(buffer).skip(start_index).take(take)) + { + for (size_t i = v.size(); i < offset_size; i++) + v += ' '; + } + } + + [[nodiscard]] auto iter() + { + return iterate(buffer).skip(start_index).take(compute_take()).map([this](std::string& x) { + return aligned_internal_string_t{x, max_line_size, x.size()}; + }); + } + + [[nodiscard]] auto iter() const + { + return iterate(buffer).skip(start_index).take(compute_take()).map([this](std::string& x) { + return aligned_internal_string_t{x, max_line_size, x.size()}; + }); + } + + void take(const size_t amount) + { + this->amount = amount; + } + + private: + [[nodiscard]] size_t compute_take() const + { + return amount == -1ul ? (buffer.size() - start_index - 1) : amount;; + } + + std::vector& buffer; + size_t start_index; + size_t max_line_size; + size_t amount = -1; + }; + + class aligned_printer_t + { + public: + explicit aligned_printer_t(std::string line_begin = "\t", const size_t max_line_size = 120, const size_t spaces_per_tab = 4): + line_begin(std::move(line_begin)), max_line_size(max_line_size) + { + buffer.emplace_back(); + for (size_t i = 0; i < spaces_per_tab; i++) + spaces_from_tab += ' '; + } + + [[nodiscard]] std::string str() const + { + std::string combined; + for (const auto& str : buffer) + { + combined += str; + combined += '\n'; + } + return combined; + } + + auto mark() + { + return aligner_t{buffer, buffer.size() - 1, max_line_size}; + } + + template + aligned_printer_t& add(T&& value) + { + const auto str = to_string(detail::ensure_is_string(std::forward(value))); + if (buffer.back().size() + str.size() > max_line_size) + newline(); + buffer.back() += replace_tabs(str); + return *this; + } + + void newline() + { + buffer.emplace_back(replace_tabs(line_begin)); + } + + [[nodiscard]] std::string replace_tabs(std::string str) const + { + string::replaceAll(str, "\t", spaces_from_tab); + return str; + } + + template + aligned_printer_t& operator+=(T&& value) + { + return add(std::forward(value)); + } + + [[nodiscard]] auto iter() + { + return iterate(buffer); + } + + [[nodiscard]] auto iter() const + { + return iterate(buffer); + } + + private: + std::vector buffer; + std::string line_begin; + std::string spaces_from_tab; + size_t max_line_size; + }; + + argument_builder_t& argument_builder_t::set_action(const action_t action) + { + m_action = action; + switch (m_action) + { + case action_t::STORE_TRUE: + set_nargs(0); + as_type(); + set_default(false); + break; + case action_t::STORE_FALSE: + set_nargs(0); + as_type(); + set_default(true); + break; + case action_t::STORE_CONST: + case action_t::APPEND_CONST: + set_nargs(0); + break; + case action_t::COUNT: + set_nargs(0); + as_type(); + break; + case action_t::EXTEND: + set_nargs(nargs_t::ALL); + break; + case action_t::HELP: + case action_t::VERSION: + set_nargs(0); + break; + default: + break; + } + return *this; + } + + argument_subparser_t* argument_parser_t::add_subparser(const std::string_view dest) + { + m_subparsers.emplace_back(dest, argument_subparser_t{*this}); + return &m_subparsers.back().second; + } + + argument_storage_t argument_parser_t::parse(argument_consumer_t& consumer) + { + if (!m_name) + m_name = fs::base_name_sv(consumer.absolute_first().get_argument()); + argument_positional_storage_t positional_storage{m_positional_arguments}; + hashset_t found_flags; + argument_storage_t parsed_args; + // first, we consume flags which may be part of this parser + while (consumer.can_consume() && consumer.peek().is_flag()) + handle_compound_flags(found_flags, parsed_args, consumer, consumer.consume()); + + for (auto& [key, subparser] : m_subparsers) + { + auto [parsed_subparser, storage] = subparser.parse(consumer); + storage.m_data.emplace(std::string{key}, detail::arg_data_t{std::string{parsed_subparser.get_argument()}}); + parsed_args.add(storage); + } + + while (consumer.can_consume()) + { + if (consumer.peek().is_flag()) + handle_compound_flags(found_flags, parsed_args, consumer, consumer.consume()); + else + parse_positional(parsed_args, consumer, positional_storage, consumer.peek().get_argument()); + } + handle_missing_and_default_args(m_flag_arguments, found_flags, parsed_args, "flag"); + + for (auto& [name, value] : positional_storage.remaining()) + { + std::visit(lambda_visitor{ + [](const nargs_t) {}, + [](const int argc) { + if (argc == 0) + throw detail::bad_positional("Positional Argument takes no values, this is invalid!"); + } + }, value.m_nargs); + + if (value.m_required) + throw detail::missing_argument_error(make_string("Error: argument '", name, "' was not found but is required by the program")); + if (value.m_default_value && !parsed_args.contains(value.m_dest.value_or(name))) + parsed_args.m_data.emplace(value.m_dest.value_or(name), *value.m_default_value); + } + + return parsed_args; + } + + void argument_parser_t::print_help() + { + print_usage(); + aligned_printer_t help{""}; + + if (!m_subparsers.empty()) + { + help += "Subcommands:"; + help.newline(); + for (const auto& [key, value] : m_subparsers) + { + auto map = value.get_allowed_strings(); + help += '\t'; + help += key; + help += ": {"; + for (const auto& [i, parser, strings] : enumerate(map).flatten()) + { + if (strings.size() > 1) + help += '['; + for (const auto& [i, str] : enumerate(strings)) + { + help += str; + if (i != strings.size() - 1) + help += ", "; + } + if (strings.size() > 1) + help += ']'; + if (i != map.size() - 1) + help += ", "; + } + help += "}"; + help.newline(); + } + help.newline(); + } + + if (!m_positional_arguments.empty()) + { + help += "Positional Arguments:"; + help.newline(); + auto mark = help.mark(); + for (auto& [name, builder] : m_positional_arguments) + { + help += '\t'; + if (!builder.m_required) + help += '['; + help += name; + if (!builder.m_required) + help += ']'; + help.newline(); + } + mark.align(4); + for (auto zipped_positionals : mark.iter().zip(m_positional_arguments)) + { + auto& line = std::get<0>(zipped_positionals); + auto& [name, builder] = std::get<1>(zipped_positionals); + line += builder.m_help.value_or(""); + if (builder.m_default_value && !(builder.m_action == action_t::STORE_TRUE || builder.m_action == action_t::STORE_FALSE)) { - if (!std::isblank(c)) - { - blank = false; - break; - } - } - if (blank) - return; - } - - string += str; - } - - template - aligned_internal_string_t& operator+=(T&& value) - { - const auto str = to_string(detail::ensure_is_string(std::forward(value))); - for (size_t i = 0; i < str.size(); i++) - { - size_t j = i; - for (; j < str.size() && !std::isblank(str[j]); ++j) - { - } - add(std::string_view(str.data() + i, j - i)); - if (j < str.size()) - add(std::string_view(str.data() + j, 1)); - i = j; - } - return *this; - } - - [[nodiscard]] std::string& str() const - { - return string; - } - - private: - std::string& string; - size_t max_line_size; - size_t line_start_size; - }; - - class aligner_t - { - public: - aligner_t(std::vector& buffer, const size_t start_index, const size_t max_line_size): - buffer(buffer), start_index(start_index), max_line_size(max_line_size) - { - } - - void align(const size_t spaces_between) const - { - const size_t take = compute_take(); - size_t aligned_size = 0; - for (const auto& v : iterate(buffer).skip(start_index).take(take)) - { - auto size = static_cast(v.size()); - for (; size > 0 && std::isblank(v[size - 1]); size--) - { - } - aligned_size = std::max(aligned_size, static_cast(size)); - } - const auto offset_size = aligned_size + spaces_between; - - for (auto& v : iterate(buffer).skip(start_index).take(take)) - { - for (size_t i = v.size(); i < offset_size; i++) - v += ' '; - } - } - - [[nodiscard]] auto iter() - { - return iterate(buffer).skip(start_index).take(compute_take()).map([this](std::string& x) - { - return aligned_internal_string_t{x, max_line_size, x.size()}; - }); - } - - [[nodiscard]] auto iter() const - { - return iterate(buffer).skip(start_index).take(compute_take()).map([this](std::string& x) - { - return aligned_internal_string_t{x, max_line_size, x.size()}; - }); - } - - void take(const size_t amount) - { - this->amount = amount; - } - - private: - [[nodiscard]] size_t compute_take() const - { - return amount == -1ul ? (buffer.size() - start_index - 1) : amount;; - } - - std::vector& buffer; - size_t start_index; - size_t max_line_size; - size_t amount = -1; - }; - - class aligned_printer_t - { - public: - explicit aligned_printer_t(std::string line_begin = "\t", const size_t max_line_size = 120, const size_t spaces_per_tab = 4): - line_begin(std::move(line_begin)), max_line_size(max_line_size) - { - buffer.emplace_back(); - for (size_t i = 0; i < spaces_per_tab; i++) - spaces_from_tab += ' '; - } - - [[nodiscard]] std::string str() const - { - std::string combined; - for (const auto& str : buffer) - { - combined += str; - combined += '\n'; - } - return combined; - } - - auto mark() - { - return aligner_t{buffer, buffer.size() - 1, max_line_size}; - } - - template - aligned_printer_t& add(T&& value) - { - const auto str = to_string(detail::ensure_is_string(std::forward(value))); - if (buffer.back().size() + str.size() > max_line_size) - newline(); - buffer.back() += replace_tabs(str); - return *this; - } - - void newline() - { - buffer.emplace_back(replace_tabs(line_begin)); - } - - [[nodiscard]] std::string replace_tabs(std::string str) const - { - string::replaceAll(str, "\t", spaces_from_tab); - return str; - } - - template - aligned_printer_t& operator+=(T&& value) - { - return add(std::forward(value)); - } - - [[nodiscard]] auto iter() - { - return iterate(buffer); - } - - [[nodiscard]] auto iter() const - { - return iterate(buffer); - } - - private: - std::vector buffer; - std::string line_begin; - std::string spaces_from_tab; - size_t max_line_size; - }; - - argument_builder_t& argument_builder_t::set_action(const action_t action) - { - m_action = action; - switch (m_action) - { - case action_t::STORE_TRUE: - set_nargs(0); - as_type(); - set_default(false); - break; - case action_t::STORE_FALSE: - set_nargs(0); - as_type(); - set_default(true); - break; - case action_t::STORE_CONST: - case action_t::APPEND_CONST: - set_nargs(0); - break; - case action_t::COUNT: - set_nargs(0); - as_type(); - break; - case action_t::EXTEND: - set_nargs(nargs_t::ALL); - break; - case action_t::HELP: - case action_t::VERSION: - set_nargs(0); - break; - default: - break; - } - return *this; - } - - argument_subparser_t* argument_parser_t::add_subparser(const std::string_view dest) - { - m_subparsers.emplace_back(dest, argument_subparser_t{*this}); - return &m_subparsers.back().second; - } - - argument_storage_t argument_parser_t::parse(argument_consumer_t& consumer) - { - if (!m_name) - m_name = fs::base_name_sv(consumer.absolute_first().get_argument()); - argument_positional_storage_t positional_storage{m_positional_arguments}; - hashset_t found_flags; - argument_storage_t parsed_args; - // first, we consume flags which may be part of this parser - while (consumer.can_consume() && consumer.peek().is_flag()) - handle_compound_flags(found_flags, parsed_args, consumer, consumer.consume()); - - for (auto& [key, subparser] : m_subparsers) - { - auto [parsed_subparser, storage] = subparser.parse(consumer); - storage.m_data.emplace(std::string{key}, detail::arg_data_t{std::string{parsed_subparser.get_argument()}}); - parsed_args.add(storage); - } - - while (consumer.can_consume()) - { - if (consumer.peek().is_flag()) - handle_compound_flags(found_flags, parsed_args, consumer, consumer.consume()); - else - parse_positional(parsed_args, consumer, positional_storage, consumer.peek().get_argument()); - } - handle_missing_and_default_args(m_flag_arguments, found_flags, parsed_args, "flag"); - - for (auto& [name, value] : positional_storage.remaining()) - { - std::visit(lambda_visitor{ - [](const nargs_t) - { - }, - [](const int argc) - { - if (argc == 0) - throw detail::bad_positional("Positional Argument takes no values, this is invalid!"); - } - }, value.m_nargs); - - if (value.m_required) - throw detail::missing_argument_error(make_string("Error: argument '", name, "' was not found but is required by the program")); - if (value.m_default_value && !parsed_args.contains(value.m_dest.value_or(name))) - parsed_args.m_data.emplace(value.m_dest.value_or(name), *value.m_default_value); - } - - return parsed_args; - } - - void argument_parser_t::print_help() - { - print_usage(); - aligned_printer_t help{""}; - - if (!m_subparsers.empty()) - { - help += "Subcommands:"; - help.newline(); - for (const auto& [key, value] : m_subparsers) - { - auto map = value.get_allowed_strings(); - help += '\t'; - help += key; - help += ": {"; - for (const auto& [i, parser, strings] : enumerate(map).flatten()) - { - if (strings.size() > 1) - help += '['; - for (const auto& [i, str] : enumerate(strings)) - { - help += str; - if (i != strings.size() - 1) - help += ", "; - } - if (strings.size() > 1) - help += ']'; - if (i != map.size() - 1) - help += ", "; - } - help += "}"; - help.newline(); - } - help.newline(); - } - - if (!m_positional_arguments.empty()) - { - help += "Positional Arguments:"; - help.newline(); - for (auto& [name, builder] : m_positional_arguments) - { - help += '\t'; - if (!builder.m_required) - help += '['; - help += name; - if (!builder.m_required) - help += ']'; - if (builder.m_default_value && !(builder.m_action == action_t::STORE_TRUE || builder.m_action == action_t::STORE_FALSE)) - { - if (!std::isblank(help.str().back())) - help += " "; - help += "(Default: "; + if (!std::isblank(line.str().back())) + line += " "; + line += "(Default: "; std::visit(detail::arg_meta_type_helper_t::make_visitor( [&](auto& value) { - help += value; + line += value; }, [&](auto& vec) { if constexpr (!std::is_same_v>, std::vector>) { - help += '['; + line += '['; for (const auto& [i, v] : enumerate(vec)) { - help += v; + line += v; if (i != vec.size() - 1) - help += ", "; + line += ", "; } - help += ']'; + line += ']'; } }), *builder.m_default_value); - help += ")"; + line += ")"; } if (builder.m_choices) { - if (!std::isblank(help.str().back())) - help += " "; - help += "(Choices: "; + if (!std::isblank(line.str().back())) + line += " "; + line += "(Choices: "; for (const auto& [i, v] : enumerate(*builder.m_choices)) { - help += '\''; - help += v; - help += '\''; + line += '\''; + line += v; + line += '\''; if (i != builder.m_choices->size() - 1) - help += ", "; + line += ", "; } - help += ')'; + line += ')'; } - help.newline(); - help.newline(); - } - } - - if (!m_flag_arguments.empty()) - { - help += "Options:"; - help.newline(); - hashmap_t> same_flags; - for (const auto& [key, value] : m_flag_arguments) - same_flags[value].emplace_back(key); - auto mark = help.mark(); - for (const auto& [builder, flag_list] : same_flags) - { - // find max size and align? - help += '\t'; - for (const auto& [i, flag] : enumerate(flag_list)) - { - help += flag; - if (i != flag_list.size() - 1) - help += ", "; - } - const argument_string_t arg{flag_list.front(), allowed_flag_prefixes}; - auto metavar = builder->m_metavar.value_or(string::toUpperCase(arg.get_name())); - auto lambda = [&]() - { - help += ' '; - help += metavar; - }; - std::visit(lambda_visitor{ - [&](const nargs_t type) - { - lambda(); - switch (type) - { - case nargs_t::IF_POSSIBLE: - break; - case nargs_t::ALL: - case nargs_t::ALL_AT_LEAST_ONE: - help += "..."; - break; - } - }, - [&](const int argc) - { - if (argc == 0) - return; - lambda(); - if (argc > 1) - { - help += "... x"; - help += std::to_string(argc); - } - } - }, builder->m_nargs); - help.newline(); - } - mark.align(4); - for (auto zipped_flags : mark.iter().zip(same_flags)) - { - auto& str = std::get<0>(zipped_flags); - auto& [builder, flag_list] = std::get<1>(zipped_flags); - str += builder->m_help.value_or(""); - if (builder->m_default_value && !(builder->m_action == action_t::STORE_TRUE || builder->m_action == action_t::STORE_FALSE)) - { - if (!std::isblank(str.str().back())) - str += " "; - str += "(Default: '"; - std::visit(detail::arg_meta_type_helper_t::make_visitor( - [&](auto& value) - { - str += value; - }, - [&](auto& vec) - { - if constexpr (!std::is_same_v>, std::vector>) - { - str += '['; - for (const auto& [i, v] : enumerate(vec)) - { - str += v; - if (i != vec.size() - 1) - str += ", "; - } - str += ']'; - } - }), *builder->m_default_value); - str += "')"; - } - if (builder->m_choices) - { - if (!std::isblank(str.str().back())) - str += " "; - str += "(Choices: "; - for (const auto& [i, v] : enumerate(*builder->m_choices)) - { - str += '\''; - str += v; - str += '\''; - if (i != builder->m_choices->size() - 1) - str += ", "; - } - str += ')'; - } - if (builder->m_required) - { - if (!std::isblank(str.str().back())) - str += " "; - str += "(Required)"; - } - } - } - - std::cout << help.str() << std::endl; - } - - void argument_parser_t::print_usage() - { - if (!m_usage) - { - aligned_printer_t aligner; - aligner += m_name.value_or(""); - aligner += ' '; - - auto parent = m_parent; - while (parent != nullptr) - { - if (!parent->m_last_parsed_parser) - throw detail::missing_value_error( - "Error: Help called on subparser but unable to find parser chain. This condition should be impossible."); - aligner += parent->m_last_parsed_parser.value(); - aligner += ' '; - parent = parent->m_parent->m_parent; - } - - for (const auto& [key, _] : m_subparsers) - { - aligner += '{'; - aligner += key; - aligner += '}'; - aligner += ' '; - } - - hashmap_t> singleFlags; - std::vector> compoundFlags; - - for (const auto& [key, value] : m_flag_arguments) - { - const argument_string_t arg{key, allowed_flag_prefixes}; - if (arg.get_flag().size() == 1) - { - if (std::holds_alternative(value->m_nargs) && std::get(value->m_nargs) == 0) - singleFlags[arg.get_flag()].emplace_back(arg.get_name()); - else - compoundFlags.emplace_back(arg, value); - } - else - compoundFlags.emplace_back(arg, value); - } - - for (const auto& [i, kv] : enumerate(singleFlags)) - { - const auto& [key, value] = kv; - aligner += '['; - aligner += key; - for (const auto& name : value) - aligner += name; - aligner += ']'; - aligner += ' '; - } - - for (const auto& [i, kv] : enumerate(compoundFlags)) - { - const auto& name = kv.first; - const auto& builder = kv.second; - aligner += builder->m_required ? '<' : '['; - aligner += name.get_argument(); - auto lambda = [&]() - { - aligner += ' '; - aligner += builder->m_metavar.value_or(string::toUpperCase(name.get_name())); - }; - std::visit(lambda_visitor{ - [&](const nargs_t type) - { - lambda(); - switch (type) - { - case nargs_t::IF_POSSIBLE: - break; - case nargs_t::ALL: - case nargs_t::ALL_AT_LEAST_ONE: - aligner += "..."; - break; - } - }, - [&](const int argc) - { - for (int j = 0; j < argc; j++) - lambda(); - } - }, builder->m_nargs); - aligner += builder->m_required ? '>' : ']'; - aligner += ' '; - } - - for (const auto& [i, pair] : enumerate(m_positional_arguments)) - { - const auto& [name, _] = pair; - aligner += '<'; - aligner += name; - aligner += '>'; - if (i != m_positional_arguments.size() - 1) - aligner += ' '; - } - - m_usage = aligner.str(); - } - std::cout << "Usage: " << *m_usage << std::endl; - } - - void argument_parser_t::print_version() const - { - std::cout << m_name.value_or("NO NAME") << " " << m_version.value_or("NO VERSION") << std::endl; - } - - void argument_parser_t::handle_compound_flags(hashset_t& found_flags, argument_storage_t& parsed_args, - argument_consumer_t& consumer, const argument_string_t& arg) - { - // i kinda hate this, TODO? - std::vector compound_flags; - if (arg.get_flag().size() == 1) - { - for (const auto c : arg.get_name()) - compound_flags.emplace_back(std::string{arg.get_flag()} + c); - } - else - { - if (arg.get_flag().size() > 2) - throw detail::bad_flag(make_string("Error: Flag '", arg.get_argument(), "' is too long!")); - compound_flags.emplace_back(arg.get_argument()); - } - - for (const auto& flag_key : compound_flags) - { - const auto flag = m_flag_arguments.find(flag_key); - if (flag == m_flag_arguments.end()) - throw detail::bad_flag(make_string("Error: Unknown flag: ", flag_key)); - found_flags.insert(flag_key); - parse_flag(parsed_args, consumer, flag_key); - } - } - - void argument_parser_t::parse_flag(argument_storage_t& parsed_args, argument_consumer_t& consumer, const std::string_view arg) - { - auto flag = m_flag_arguments.find(arg)->second; - const auto dest = flag->m_dest.value_or(std::string{arg}); - std::visit(lambda_visitor{ - [&parsed_args, &consumer, &dest, &flag, arg](const nargs_t arg_enum) - { - switch (arg_enum) - { - case nargs_t::IF_POSSIBLE: - if (consumer.can_consume() && !consumer.peek().is_flag()) - flag->m_dest_func(dest, parsed_args, consumer.consume().get_argument()); - else - { - if (flag->m_const_value) - parsed_args.m_data.insert({dest, *flag->m_const_value}); - } - break; - case nargs_t::ALL_AT_LEAST_ONE: - if (!consumer.can_consume()) - throw detail::missing_argument_error( - make_string("Error expected at least one argument to be consumed by '", arg, '\'')); - [[fallthrough]]; - case nargs_t::ALL: - auto result = consume_until_flag_or_end(consumer, flag->m_choices ? &*flag->m_choices : nullptr); - if (!result) - throw detail::bad_choice_error(make_string('\'', consumer.peek().get_argument(), - "' is not a valid choice for argument '", arg, - "'! Expected one of ", result.error())); - flag->m_dest_vec_func(dest, parsed_args, result.value()); - break; - } - }, - [&parsed_args, &consumer, &dest, &flag, arg, this](const i32 argc) - { - const auto args = consume_argc(argc, consumer, flag->m_choices ? &*flag->m_choices : nullptr, arg); - - switch (flag->m_action) - { - case action_t::STORE: - if (argc == 0) - throw detail::missing_argument_error( - make_string("Argument '", arg, "'s action is store but takes in no arguments?")); - if (argc == 1) - flag->m_dest_func(dest, parsed_args, args.front()); - else - throw detail::unexpected_argument_error(make_string("Argument '", arg, - "'s action is store but takes in more than one argument. " - "Did you mean to use action_t::APPEND or action_t::EXTEND?")); - break; - case action_t::APPEND: - case action_t::EXTEND: - if (argc == 0) - throw detail::missing_argument_error( - make_string("Argument '", arg, "'s action is append or extend but takes in no arguments.")); - flag->m_dest_vec_func(dest, parsed_args, args); - break; - case action_t::APPEND_CONST: - if (argc != 0) - throw detail::unexpected_argument_error( - make_string("Argument '", arg, "'s action is append const but takes in arguments.")); - if (!flag->m_const_value) - { - throw detail::missing_value_error( - make_string("Append const chosen as an action but const value not provided for argument '", arg, - '\'')); - } - if (parsed_args.contains(dest)) - { - auto& data = parsed_args.m_data[dest]; - std::visit(detail::arg_meta_type_helper_t::make_visitor( - [arg](auto& primitive) - { - throw detail::type_error(make_string( - "Invalid type for argument '", arg, "' expected list type, found '", - blt::type_string(), "' with value ", primitive)); - }, - [&flag, arg](auto& vec) - { - using type = typename meta::remove_cvref_t::value_type; - if (!std::holds_alternative(*flag->m_const_value)) - { - throw detail::type_error(make_string("Constant value for argument '", arg, - "' type doesn't match values already present! Expected to be of type '", - blt::type_string(), "'!")); - } - vec.push_back(std::get(*flag->m_const_value)); - }), data); - } - else - { - std::visit(detail::arg_meta_type_helper_t::make_visitor( - [&parsed_args, &dest](auto& primitive) - { - std::vector> vec; - vec.emplace_back(primitive); - parsed_args.m_data.emplace(dest, std::move(vec)); - }, - [](auto&) - { - throw detail::type_error("Append const should not be a list type!"); - }), *flag->m_const_value); - } - break; - case action_t::STORE_CONST: - if (argc != 0) - { - print_usage(); - throw detail::unexpected_argument_error( - make_string("Argument '", arg, "' is store const but called with an argument.")); - } - if (!flag->m_const_value) - throw detail::missing_value_error( - make_string("Argument '", arg, "' is store const, but const storage has no value.")); - parsed_args.m_data.emplace(dest, *flag->m_const_value); - break; - case action_t::STORE_TRUE: - if (argc != 0) - { - print_usage(); - throw detail::unexpected_argument_error("Store true flag called with an argument."); - } - parsed_args.m_data.emplace(dest, true); - break; - case action_t::STORE_FALSE: - if (argc != 0) - { - print_usage(); - throw detail::unexpected_argument_error("Store false flag called with an argument."); - } - parsed_args.m_data.insert({dest, false}); - break; - case action_t::COUNT: - if (parsed_args.m_data.contains(dest)) - { - auto visitor = detail::arg_meta_type_helper_t::make_visitor( - [](auto& primitive) -> detail::arg_data_t - { - using type = meta::remove_cvref_t; - if constexpr (std::is_convertible_v) - { - return primitive + static_cast(1); - } - else - throw detail::type_error( - "Error: count called but stored type is " + blt::type_string()); - }, - [](auto&) -> detail::arg_data_t - { - throw detail::type_error("List present on count. This condition doesn't make any sense! " - "(How did we get here, please report this!)"); - } - ); - parsed_args.m_data[dest] = std::visit(visitor, parsed_args.m_data[dest]); - } - else // I also hate this! - flag->m_dest_func(dest, parsed_args, "1"); - break; - case action_t::HELP: - print_help(); - std::exit(0); - case action_t::VERSION: - print_version(); - std::exit(0); - } - } - }, flag->m_nargs); - } - - void argument_parser_t::parse_positional(argument_storage_t& parsed_args, argument_consumer_t& consumer, argument_positional_storage_t& storage, - const std::string_view arg) - { - if (!storage.has_positional()) - throw detail::missing_argument_error(make_string("Error: '", arg, "' positional argument does not match any defined for this parser")); - auto& positional = storage.next(); - const auto dest = positional.m_dest.value_or(std::string{arg}); - std::visit(lambda_visitor{ - [&consumer, &positional, &dest, &parsed_args, arg](const nargs_t arg_enum) - { - switch (arg_enum) - { - case nargs_t::IF_POSSIBLE: - throw detail::bad_positional( - "Positional argument asked to consume if possible. We do not consider this to be a valid ask."); - case nargs_t::ALL_AT_LEAST_ONE: - if (!consumer.can_consume()) - throw detail::missing_argument_error( - make_string("Error expected at least one argument to be consumed by '", arg, '\'')); - [[fallthrough]]; - case nargs_t::ALL: - auto result = consume_until_flag_or_end( - consumer, positional.m_choices ? &*positional.m_choices : nullptr); - if (!result) - throw detail::bad_choice_error(make_string('\'', consumer.peek().get_argument(), - "' is not a valid choice for argument '", arg, - "'! Expected one of ", result.error())); - positional.m_dest_vec_func(dest, parsed_args, result.value()); - break; - } - }, - [this, &consumer, &positional, &dest, &parsed_args, arg](const i32 argc) - { - const auto args = consume_argc(argc, consumer, positional.m_choices ? &*positional.m_choices : nullptr, arg); - - switch (positional.m_action) - { - case action_t::STORE: - if (argc == 0) - throw detail::missing_argument_error( - make_string("Argument '", arg, "'s action is store but takes in no arguments?")); - if (argc == 1) - positional.m_dest_func(dest, parsed_args, args.front()); - else - throw detail::unexpected_argument_error(make_string("Argument '", arg, - "'s action is store but takes in more than one argument. " - "Did you mean to use action_t::APPEND or action_t::EXTEND?")); - break; - case action_t::APPEND: - case action_t::EXTEND: - if (argc == 0) - throw detail::missing_argument_error( - make_string("Argument '", arg, "'s action is append or extend but takes in no arguments.")); - positional.m_dest_vec_func(dest, parsed_args, args); - break; - case action_t::APPEND_CONST: - throw detail::bad_positional("action_t::APPEND_CONST does not make sense for positional arguments"); - case action_t::STORE_CONST: - throw detail::bad_positional("action_t::STORE_CONST does not make sense for positional arguments"); - case action_t::STORE_TRUE: - throw detail::bad_positional("action_t::STORE_TRUE does not make sense for positional arguments"); - case action_t::STORE_FALSE: - throw detail::bad_positional("action_t::STORE_FALSE does not make sense for positional arguments"); - case action_t::COUNT: - throw detail::bad_positional("action_t::COUNT does not make sense for positional arguments"); - case action_t::HELP: - print_help(); - std::exit(0); - case action_t::VERSION: - print_version(); - std::exit(0); - } - } - }, positional.m_nargs); - } - - void argument_parser_t::handle_missing_and_default_args(hashmap_t& arguments, - const hashset_t& found, argument_storage_t& parsed_args, - const std::string_view type) - { - for (const auto& [key, value] : arguments) - { - if (!found.contains(key)) - { - if (value->m_required) - throw detail::missing_argument_error(make_string("Error: ", type, " argument '", key, - "' was not found but is required by the program")); - auto dest = value->m_dest.value_or(std::string{key}); - if (value->m_default_value && !parsed_args.contains(dest)) - parsed_args.m_data.emplace(dest, *value->m_default_value); - } - } - } - - expected, std::string> argument_parser_t::consume_until_flag_or_end(argument_consumer_t& consumer, - hashset_t* allowed_choices) - { - std::vector args; - while (consumer.can_consume() && !consumer.peek().is_flag()) - { - if (allowed_choices != nullptr && !allowed_choices->contains(consumer.peek().get_argument())) - { - std::string valid_choices = "{"; - for (const auto& [i, choice] : enumerate(*allowed_choices)) - { - valid_choices += choice; - if (i != allowed_choices->size() - 1) - valid_choices += ", "; - } - valid_choices += "}"; - return unexpected(valid_choices); - } - args.emplace_back(consumer.consume().get_argument()); - } - return args; - } - - std::vector argument_parser_t::consume_argc(const int argc, argument_consumer_t& consumer, - hashset_t* allowed_choices, - const std::string_view arg) - { - std::vector args; - for (i32 i = 0; i < argc; ++i) - { - if (!consumer.can_consume()) - { - throw detail::missing_argument_error( - make_string("Expected ", argc, " arguments to be consumed by '", arg, "' but found ", i)); - } - if (consumer.peek().is_flag()) - { - std::cout << "Warning: arg '" << arg << "' expects " << argc << - " arguments to be consumed but we found a flag '" << consumer.peek(). - get_argument() << - "'. We will comply as this may be desired if this argument is a file." << std::endl; - } - if (allowed_choices != nullptr && !allowed_choices->contains(consumer.peek().get_argument())) - { - std::string valid_choices = "{"; - for (const auto& [i, choice] : enumerate(*allowed_choices)) - { - valid_choices += choice; - if (i != allowed_choices->size() - 1) - valid_choices += ", "; - } - valid_choices += "}"; - throw detail::bad_choice_error(make_string('\'', consumer.peek().get_argument(), - "' is not a valid choice for argument '", arg, - "'! Expected one of ", valid_choices)); - } - args.emplace_back(consumer.consume().get_argument()); - } - if (args.size() != static_cast(argc)) - { - throw std::runtime_error( - "This error condition should not be possible. " - "Args consumed didn't equal the arguments requested and previous checks didn't fail. " - "Please report as an issue on the GitHub"); - } - return args; - } - - std::pair argument_subparser_t::parse(argument_consumer_t& consumer) - { - if (!consumer.can_consume()) - throw detail::missing_argument_error("Subparser requires an argument."); - const auto key = consumer.consume(); - if (key.is_flag()) - throw detail::subparse_error(key.get_argument(), to_vec(get_allowed_strings())); - const auto it = m_aliases.find(key.get_name()); - if (it == m_aliases.end()) - throw detail::subparse_error(key.get_argument(), to_vec(get_allowed_strings())); - it->second->m_name = m_parent->m_name; - m_last_parsed_parser = key.get_name(); - return {key, it->second->parse(consumer)}; - } - - hashmap_t> argument_subparser_t::get_allowed_strings() const - { - hashmap_t> map; - for (const auto& [key, value] : m_aliases) - map[value].emplace_back(key); - return map; - } - - std::vector> argument_subparser_t::to_vec(const hashmap_t>& map) - { - std::vector> vec; - for (const auto& [key, value] : map) - vec.push_back(value); - return vec; - } - - namespace detail - { - // Unit Tests for class argument_string_t - // Test Case 1: Ensure the constructor handles flags correctly - void test_argument_string_t_flag_basic(const hashset_t& prefixes) - { - const argument_string_t arg("-f", prefixes); - BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); - BLT_ASSERT(arg.value() == "f" && "Flag value should match the input string."); - } - - // Test Case 2: Ensure the constructor handles long flags correctly - void test_argument_string_t_long_flag(const hashset_t& prefixes) - { - const argument_string_t arg("--file", prefixes); - BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); - BLT_ASSERT(arg.value() == "file" && "Long flag value should match the input string."); - } - - // Test Case 3: Ensure positional arguments are correctly identified - void test_argument_string_t_positional_argument(const hashset_t& prefixes) - { - const argument_string_t arg("filename.txt", prefixes); - BLT_ASSERT(!arg.is_flag() && "Expected argument to be identified as positional."); - BLT_ASSERT(arg.value() == "filename.txt" && "Positional argument value should match the input string."); - } - - // Test Case 5: Handle an empty string - void test_argument_string_t_empty_input(const hashset_t& prefixes) - { - const argument_string_t arg("", prefixes); - BLT_ASSERT(!arg.is_flag() && "Expected an empty input to be treated as positional, not a flag."); - BLT_ASSERT(arg.value().empty() && "Empty input should have an empty value."); - } - - // Test Case 6: Handle edge case of a single hyphen (`-`) which might be ambiguous - void test_argument_string_t_single_hyphen(const hashset_t& prefixes) - { - const argument_string_t arg("-", prefixes); - BLT_ASSERT(arg.is_flag() && "Expected single hyphen (`-`) to be treated as a flag."); - BLT_ASSERT(arg.value().empty() && "Single hyphen flag should have empty value."); - BLT_ASSERT(arg.get_flag() == "-" && "Single hyphen flag should match the input string."); - } - - // Test Case 8: Handle arguments with prefix only (like "--") - void test_argument_string_t_double_hyphen(const hashset_t& prefixes) - { - const argument_string_t arg("--", prefixes); - BLT_ASSERT(arg.is_flag() && "Double hyphen ('--') should be treated as a flag."); - BLT_ASSERT(arg.value().empty() && "Double hyphen flag should have empty value."); - BLT_ASSERT(arg.get_flag() == "--" && "Double hyphen value should match the input string."); - } - - // Test Case 9: Validate edge case of an argument with spaces - void test_argument_string_t_with_spaces(const hashset_t& prefixes) - { - const argument_string_t arg(" ", prefixes); - BLT_ASSERT(!arg.is_flag() && "Arguments with spaces should not be treated as flags."); - BLT_ASSERT(arg.value() == " " && "Arguments with spaces should match the input string."); - } - - // Test Case 10: Validate arguments with numeric characters - void test_argument_string_t_numeric_flag(const hashset_t& prefixes) - { - const argument_string_t arg("-123", prefixes); - BLT_ASSERT(arg.is_flag() && "Numeric flags should still be treated as flags."); - BLT_ASSERT(arg.value() == "123" && "Numeric flag value should match the input string."); - } - - // Test Case 11: Ensure the constructor handles '+' flag correctly - void test_argument_string_t_plus_flag_basic(const hashset_t& prefixes) - { - const argument_string_t arg("+f", prefixes); - BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); - BLT_ASSERT(arg.value() == "f" && "Plus flag value should match the input string."); - } - - // Test Case 13: Handle edge case of a single plus (`+`) which might be ambiguous - void test_argument_string_t_single_plus(const hashset_t& prefixes) - { - const argument_string_t arg("+", prefixes); - BLT_ASSERT(arg.is_flag() && "Expected single plus (`+`) to be treated as a flag."); - BLT_ASSERT(arg.value().empty() && "Single plus flag should have empty value."); - BLT_ASSERT(arg.get_flag() == "+" && "Single plus flag should match the input string."); - } - - // Test Case 14: Handle arguments with prefix only (like '++') - void test_argument_string_t_double_plus(const hashset_t& prefixes) - { - const argument_string_t arg("++", prefixes); - BLT_ASSERT(arg.is_flag() && "Double plus ('++') should be treated as a flag."); - BLT_ASSERT(arg.value().empty() && "Double plus flag should have empty value."); - BLT_ASSERT(arg.get_flag() == "++" && "Double plus value should match the input string."); - } - - - void run_all_tests_argument_string_t() - { - const hashset_t prefixes = {'-', '+'}; - test_argument_string_t_flag_basic(prefixes); - test_argument_string_t_long_flag(prefixes); - test_argument_string_t_positional_argument(prefixes); - test_argument_string_t_empty_input(prefixes); - test_argument_string_t_single_hyphen(prefixes); - test_argument_string_t_double_hyphen(prefixes); - test_argument_string_t_with_spaces(prefixes); - test_argument_string_t_numeric_flag(prefixes); - test_argument_string_t_plus_flag_basic(prefixes); - test_argument_string_t_single_plus(prefixes); - test_argument_string_t_double_plus(prefixes); - } - - void test_argparse_empty() - { - const std::vector argv{"./program"}; - argument_parser_t parser; - const auto args = parser.parse(argv); - BLT_ASSERT(args.size() == 0 && "Empty argparse should have no args on output"); - } - - void test_single_flag_prefixes() - { - argument_parser_t parser; - parser.add_flag("-a").set_action(action_t::STORE_TRUE); - parser.add_flag("+b").set_action(action_t::STORE_FALSE); - parser.add_flag("/c").as_type().set_action(action_t::STORE); - - const std::vector args = {"./program", "-a", "+b", "/c", "42"}; - const auto parsed_args = parser.parse(args); - - BLT_ASSERT(parsed_args.get("-a") == true && "Flag '-a' should store `true`"); - BLT_ASSERT(parsed_args.get("+b") == false && "Flag '+b' should store `false`"); - BLT_ASSERT(parsed_args.get("/c") == 42 && "Flag '/c' should store the value 42"); - } - - // Test: Invalid flag prefixes - void test_invalid_flag_prefixes() - { - argument_parser_t parser; - parser.add_flag("-a"); - parser.add_flag("+b"); - parser.add_flag("/c"); - - const std::vector args = {"./program", "!d", "-a"}; - try - { - parser.parse(args); - BLT_ASSERT(false && "Parsing should fail with invalid flag prefix '!'"); - } - catch (...) - { - BLT_ASSERT(true && "Correctly threw on bad flag prefix"); - } - } - - void test_compound_flags() - { - argument_parser_t parser; - parser.add_flag("-v").as_type().set_action(action_t::COUNT); - - const std::vector args = {"./program", "-vvv"}; - const auto parsed_args = parser.parse(args); - - BLT_ASSERT(parsed_args.get("-v") == 3 && "Flag '-v' should count occurrences in compound form"); - } - - void test_combination_of_valid_and_invalid_flags() - { - using namespace argparse; - - argument_parser_t parser; - parser.add_flag("-x").as_type(); - parser.add_flag("/y").as_type(); - - const std::vector args = {"./program", "-x", "10", "!z", "/y", "value"}; - try - { - parser.parse(args); - BLT_ASSERT(false && "Parsing should fail due to invalid flag '!z'"); - } - catch (...) - { - BLT_ASSERT(true && "Correctly threw an exception for invalid flag"); - } - } - - void test_flags_with_different_actions() - { - using namespace argparse; - - argument_parser_t parser; - parser.add_flag("-k").as_type().set_action(action_t::STORE); // STORE action - parser.add_flag("-t").as_type().set_action(action_t::STORE_CONST).set_const(999); // STORE_CONST action - parser.add_flag("-f").set_action(action_t::STORE_FALSE); // STORE_FALSE action - parser.add_flag("-c").set_action(action_t::STORE_TRUE); // STORE_TRUE action - - const std::vector args = {"./program", "-k", "100", "-t", "-f", "-c"}; - const auto parsed_args = parser.parse(args); - - BLT_ASSERT(parsed_args.get("-k") == 100 && "Flag '-k' should store 100"); - BLT_ASSERT(parsed_args.get("-t") == 999 && "Flag '-t' should store a const value of 999"); - BLT_ASSERT(parsed_args.get("-f") == false && "Flag '-f' should store `false`"); - BLT_ASSERT(parsed_args.get("-c") == true && "Flag '-c' should store `true`"); - } - - // Helper function to simulate argument parsing for nargs tests - bool parse_arguments(const std::vector& args, const nargs_v expected_nargs) - { - argument_parser_t parser; - - std::vector arg_strings; - arg_strings.reserve(args.size()); - for (const auto& arg : args) - arg_strings.emplace_back(arg, parser.get_allowed_flag_prefixes()); - argument_consumer_t consumer{arg_strings}; - - parser.add_positional("positional").set_nargs(expected_nargs); - try - { - auto parsed_args = parser.parse(consumer); - return consumer.remaining() == 0; - } - catch (const std::exception&) - { - return false; - } - } - - // Test case for nargs = 0 - void test_nargs_0() - { - std::cout << "[Running Test: test_nargs_0]\n"; - - // Valid case: No arguments - const std::vector valid_args = {"./program"}; - BLT_ASSERT(!parse_arguments(valid_args, 0) && "nargs=0: Should fail"); - - // Invalid case: 1 argument - const std::vector invalid_args = {"./program", "arg1"}; - BLT_ASSERT(!parse_arguments(invalid_args, 0) && "nargs=0: Should not accept any arguments"); - - std::cout << "Success: test_nargs_0\n"; - } - - // Test case for nargs = 1 - void test_nargs_1() - { - std::cout << "[Running Test: test_nargs_1]\n"; - - // Valid case: 1 argument - const std::vector valid_args = {"./program", "arg1"}; - BLT_ASSERT(parse_arguments(valid_args, 1) && "nargs=1: Should accept exactly 1 argument"); - - // Invalid case: 0 arguments - const std::vector invalid_args_0 = {"./program"}; - BLT_ASSERT(!parse_arguments(invalid_args_0, 1) && "nargs=1: Should not accept 0 arguments"); - - // Invalid case: 2 arguments - const std::vector invalid_args_2 = {"./program", "arg1", "arg2"}; - BLT_ASSERT(!parse_arguments(invalid_args_2, 1) && "nargs=1: Should not accept more than 1 argument"); - - std::cout << "Success: test_nargs_1\n"; - } - - // Test case for nargs = 2 - void test_nargs_2() - { - std::cout << "[Running Test: test_nargs_2]\n"; - - // Valid case: 2 arguments - const std::vector valid_args = {"./program", "arg1", "arg2"}; - BLT_ASSERT(!parse_arguments(valid_args, 2) && "nargs=2: Should fail as action is store"); - - // Invalid case: 0 arguments - const std::vector invalid_args_0 = {"./program"}; - BLT_ASSERT(!parse_arguments(invalid_args_0, 2) && "nargs=2: Should not accept 0 arguments"); - - // Invalid case: 1 argument - const std::vector invalid_args_1 = {"./program", "arg1"}; - BLT_ASSERT(!parse_arguments(invalid_args_1, 2) && "nargs=2: Should not accept less than 2 arguments"); - - // Invalid case: 3 arguments - const std::vector invalid_args_3 = {"./program", "arg1", "arg2", "arg3"}; - BLT_ASSERT(!parse_arguments(invalid_args_3, 2) && "nargs=2: Should not accept more than 2 arguments"); - - std::cout << "Success: test_nargs_2\n"; - } - - void test_nargs_all() - { - std::cout << "[Running Test: test_nargs_all]\n"; - - // Valid case: No arguments - const std::vector valid_args_0 = {"./program"}; - BLT_ASSERT(!parse_arguments(valid_args_0, argparse::nargs_t::ALL) && - "nargs=ALL: No arguments present. Should fail.)"); - - // Valid case: Multiple arguments - const std::vector valid_args_2 = {"./program", "arg1", "arg2"}; - BLT_ASSERT(parse_arguments(valid_args_2, argparse::nargs_t::ALL) && - "nargs=ALL: Should accept all remaining arguments"); - - // Valid case: Many arguments - const std::vector valid_args_many = {"./program", "arg1", "arg2", "arg3", "arg4"}; - BLT_ASSERT(parse_arguments(valid_args_many, argparse::nargs_t::ALL) && - "nargs=ALL: Should accept all remaining arguments"); - - std::cout << "Success: test_nargs_all\n"; - } - - // Test case for nargs_t::ALL_AT_LEAST_ONE - void test_nargs_all_at_least_one() - { - std::cout << "[Running Test: test_nargs_all_at_least_one]\n"; - - // Valid case: 1 argument - const std::vector valid_args_1 = {"./program", "arg1"}; - BLT_ASSERT(parse_arguments(valid_args_1, argparse::nargs_t::ALL_AT_LEAST_ONE) && - "nargs=ALL_AT_LEAST_ONE: Should accept at least one argument and consume it"); - - // Valid case: Multiple arguments - const std::vector valid_args_3 = {"./program", "arg1", "arg2", "arg3"}; - BLT_ASSERT(parse_arguments(valid_args_3, argparse::nargs_t::ALL_AT_LEAST_ONE) && - "nargs=ALL_AT_LEAST_ONE: Should accept at least one argument and consume all remaining arguments"); - - // Invalid case: No arguments - const std::vector invalid_args_0 = {"./program"}; - BLT_ASSERT(!parse_arguments(invalid_args_0, argparse::nargs_t::ALL_AT_LEAST_ONE) && - "nargs=ALL_AT_LEAST_ONE: Should reject if no arguments are provided"); - - std::cout << "Success: test_nargs_all_at_least_one\n"; - } - - void run_combined_flag_test() - { - std::cout << "[Running Test: run_combined_flag_test]\n"; - argument_parser_t parser; - - parser.add_flag("-a").set_action(action_t::STORE_TRUE); - parser.add_flag("--deep").set_action(action_t::STORE_FALSE); - parser.add_flag("-b", "--combined").set_action(action_t::STORE_CONST).set_const(50); - parser.add_flag("--append").set_action(action_t::APPEND).as_type(); - parser.add_flag("--required").set_required(true); - parser.add_flag("--default").set_default("I am a default value"); - parser.add_flag("-t").set_action(action_t::APPEND_CONST).set_dest("test").set_const(5); - parser.add_flag("-g").set_action(action_t::APPEND_CONST).set_dest("test").set_const(10); - parser.add_flag("-e").set_action(action_t::APPEND_CONST).set_dest("test").set_const(15); - parser.add_flag("-f").set_action(action_t::APPEND_CONST).set_dest("test").set_const(20); - parser.add_flag("-d").set_action(action_t::APPEND_CONST).set_dest("test").set_const(25); - parser.add_flag("--end").set_action(action_t::EXTEND).set_dest("wow").as_type(); - - const auto a1 = make_arguments("-a", "--required", "hello"); - const auto r1 = parser.parse(a1); - BLT_ASSERT(r1.get("-a") == true && "Flag '-a' should store true"); - BLT_ASSERT(r1.get("--default") == "I am a default value" && "Flag '--default' should store default value"); - BLT_ASSERT(r1.get("--required") == "hello" && "Flag '--required' should store 'hello'"); - - const auto a2 = make_arguments("-a", "--deep", "--required", "soft"); - const auto r2 = parser.parse(a2); - BLT_ASSERT(r2.get("-a") == true && "Flag '-a' should store true"); - BLT_ASSERT(r2.get("--deep") == false && "Flag '--deep' should store false"); - BLT_ASSERT(r2.get("--required") == "soft" && "Flag '--required' should store 'soft'"); - - const auto a3 = make_arguments("--required", "silly", "--combined", "-t", "-f", "-e"); - const auto r3 = parser.parse(a3); - BLT_ASSERT((r3.get>("test") == std::vector{5, 20, 15}) && "Flags should add to vector of {5, 20, 15}"); - BLT_ASSERT(r3.get("-b") == 50 && "Combined flag should store const of 50"); - - const auto a4 = make_arguments("--required", "crazy", "--end", "10", "12.05", "68.11", "100.00", "200532", "-d", "-t", "-g", "-e", "-f"); - const auto r4 = parser.parse(a4); - BLT_ASSERT( - (r4.get>("test") == std::vector{25, 5, 10, 15, 20}) && - "Expected test vector to be filled with all arguments in order of flags"); - BLT_ASSERT( - (r4.get>("wow") == std::vector{10, 12.05, 68.11, 100.00, 200532}) && - "Extend vector expected to contain all elements"); - - std::cout << "Success: run_combined_flag_test\n"; - } - - void run_choice_test() - { - std::cout << "[Running Test: run_choice_test]\n"; - argument_parser_t parser; - - parser.add_flag("--hello").set_choices("silly", "crazy", "soft"); - parser.add_positional("iam").set_choices("different", "choices", "for", "me"); - - const auto a1 = make_arguments("--hello", "crazy", "different"); - const auto r1 = parser.parse(a1); - BLT_ASSERT(r1.get("--hello") == "crazy" && "Flag '--hello' should store 'crazy'"); - BLT_ASSERT(r1.get("iam") == "different" && "Positional 'iam' should store 'different'"); - - const auto a2 = make_arguments("--hello", "not_an_option", "different"); - try - { - parser.parse(a2); - BLT_ASSERT(false && "Parsing should fail due to invalid flag '--hello'"); - } - catch (...) - { - } - - const auto a3 = make_arguments("--hello", "crazy", "not_a_choice"); - try - { - parser.parse(a3); - BLT_ASSERT(false && "Parsing should fail due to invalid positional 'iam'"); - } - catch (...) - { - } - - std::cout << "Success: run_choice_test\n"; - } - - void run_subparser_test() - { - std::cout << "[Running Test: run_subparser_test]\n"; - argument_parser_t parser; - - parser.add_flag("--open").make_flag(); - - const auto subparser = parser.add_subparser("mode"); - - const auto n1 = subparser->add_parser("n1"); - n1->add_flag("--silly").make_flag(); - n1->add_positional("path"); - - const auto n2 = subparser->add_parser("n2"); - n2->add_flag("--crazy").make_flag(); - n2->add_positional("path"); - n2->add_positional("output"); - - const auto n3 = subparser->add_parser("n3"); - n3->add_flag("--deep").make_flag(); - - const auto a1 = make_arguments("n1", "--silly"); - try - { - parser.parse(a1); - BLT_ASSERT(false && "Subparser should throw an error when positional not supplied"); - } - catch (...) - { - } - - const auto a2 = make_arguments("--open"); - try - { - parser.parse(a2); - BLT_ASSERT(false && "Subparser should throw an error when no subparser is supplied"); - } - catch (...) - { - } - - const auto a3 = make_arguments("n1", "--silly", "path_n1"); - const auto r3 = parser.parse(a3); - BLT_ASSERT(r3.get("--open") == false && "Flag '--open' should default to false"); - BLT_ASSERT(r3.get("mode") == "n1" && "Subparser should store 'n1'"); - BLT_ASSERT(r3.get("path") == "path_n1" && "Subparser path should be 'path'"); - - const auto a4 = make_arguments("n2", "--crazy", "path"); - - try - { - parser.parse(a4); - BLT_ASSERT(false && "Subparser should throw an error when second positional is not supplied"); - } - catch (...) - { - } - - const auto a5 = make_arguments("--open", "n2", "path_n2", "output_n2"); - const auto r5 = parser.parse(a5); - BLT_ASSERT(r5.get("--open") == true && "Flag '--open' should store true"); - BLT_ASSERT(r5.get("mode") == "n2" && "Subparser should store 'n2'"); - BLT_ASSERT(r5.get("path") == "path_n2" && "Subparser path should be 'path'"); - BLT_ASSERT(r5.get("output") == "output_n2" && "Subparser output should be 'output'"); - - const auto a6 = make_arguments("not_an_option", "silly"); - - try - { - parser.parse(a6); - BLT_ASSERT(false && "Subparser should throw an error when first positional is not a valid subparser"); - } - catch (const std::exception&) - { - } - - const auto a7 = make_arguments("n3"); - const auto r7 = parser.parse(a7); - BLT_ASSERT(r7.get("mode") == "n3" && "Subparser should store 'n3'"); - - std::cout << "Success: run_subparser_test\n"; - } - - void run_argparse_flag_tests() - { - test_single_flag_prefixes(); - test_invalid_flag_prefixes(); - test_compound_flags(); - test_combination_of_valid_and_invalid_flags(); - test_flags_with_different_actions(); - run_combined_flag_test(); - run_choice_test(); - run_subparser_test(); - } - - void run_all_nargs_tests() - { - test_nargs_0(); - test_nargs_1(); - test_nargs_2(); - test_nargs_all(); - test_nargs_all_at_least_one(); - std::cout << "All nargs tests passed successfully.\n"; - } - - void test() - { - run_all_tests_argument_string_t(); - test_argparse_empty(); - run_argparse_flag_tests(); - run_all_nargs_tests(); - } - - [[nodiscard]] std::string subparse_error::error_string() const - { - std::string message = "Subparser Error: "; - message += m_found_string; - message += " is not a valid command. Allowed commands are: {"; - for (const auto [i, allowed_string] : enumerate(m_allowed_strings)) - { - if (allowed_string.size() > 1) - message += '['; - for (const auto [j, alias] : enumerate(allowed_string)) - { - message += alias; - if (static_cast(j) < static_cast(allowed_string.size()) - 2) - message += ", "; - else if (static_cast(j) < static_cast(allowed_string.size()) - 1) - message += ", or "; - } - if (allowed_string.size() > 1) - message += ']'; - if (i != m_allowed_strings.size() - 1) - message += ", "; - } - message += "}"; - return message; - } - } + } + } + + if (!m_flag_arguments.empty()) + { + help += "Options:"; + help.newline(); + hashmap_t> same_flags; + for (const auto& [key, value] : m_flag_arguments) + same_flags[value].emplace_back(key); + auto mark = help.mark(); + for (const auto& [builder, flag_list] : same_flags) + { + // find max size and align? + help += '\t'; + for (const auto& [i, flag] : enumerate(flag_list)) + { + help += flag; + if (i != flag_list.size() - 1) + help += ", "; + } + const argument_string_t arg{flag_list.front(), allowed_flag_prefixes}; + auto metavar = builder->m_metavar.value_or(string::toUpperCase(arg.get_name())); + auto lambda = [&]() { + help += ' '; + help += metavar; + }; + std::visit(lambda_visitor{ + [&](const nargs_t type) { + lambda(); + switch (type) + { + case nargs_t::IF_POSSIBLE: + break; + case nargs_t::ALL: + case nargs_t::ALL_AT_LEAST_ONE: + help += "..."; + break; + } + }, + [&](const int argc) { + if (argc == 0) + return; + lambda(); + if (argc > 1) + { + help += "... x"; + help += std::to_string(argc); + } + } + }, builder->m_nargs); + help.newline(); + } + mark.align(4); + for (auto zipped_flags : mark.iter().zip(same_flags)) + { + auto& str = std::get<0>(zipped_flags); + auto& [builder, flag_list] = std::get<1>(zipped_flags); + str += builder->m_help.value_or(""); + if (builder->m_default_value && !(builder->m_action == action_t::STORE_TRUE || builder->m_action == action_t::STORE_FALSE)) + { + if (!std::isblank(str.str().back())) + str += " "; + str += "(Default: '"; + std::visit(detail::arg_meta_type_helper_t::make_visitor([&](auto& value) { + str += value; + }, [&](auto& vec) { + if constexpr (!std::is_same_v>, std::vector>) + { + str += '['; + for (const auto& [i, v] : enumerate(vec)) + { + str += v; + if (i != vec.size() - 1) + str += ", "; + } + str += ']'; + } + }), *builder->m_default_value); + str += "')"; + } + if (builder->m_choices) + { + if (!std::isblank(str.str().back())) + str += " "; + str += "(Choices: "; + for (const auto& [i, v] : enumerate(*builder->m_choices)) + { + str += '\''; + str += v; + str += '\''; + if (i != builder->m_choices->size() - 1) + str += ", "; + } + str += ')'; + } + if (builder->m_required) + { + if (!std::isblank(str.str().back())) + str += " "; + str += "(Required)"; + } + } + } + + std::cout << help.str() << std::endl; + } + + void argument_parser_t::print_usage() + { + if (!m_usage) + { + aligned_printer_t aligner; + aligner += m_name.value_or(""); + aligner += ' '; + + auto parent = m_parent; + while (parent != nullptr) + { + if (!parent->m_last_parsed_parser) + throw detail::missing_value_error( + "Error: Help called on subparser but unable to find parser chain. This condition should be impossible."); + aligner += parent->m_last_parsed_parser.value(); + aligner += ' '; + parent = parent->m_parent->m_parent; + } + + for (const auto& [key, _] : m_subparsers) + { + aligner += '{'; + aligner += key; + aligner += '}'; + aligner += ' '; + } + + hashmap_t> singleFlags; + std::vector> compoundFlags; + + for (const auto& [key, value] : m_flag_arguments) + { + const argument_string_t arg{key, allowed_flag_prefixes}; + if (arg.get_flag().size() == 1) + { + if (std::holds_alternative(value->m_nargs) && std::get(value->m_nargs) == 0) + singleFlags[arg.get_flag()].emplace_back(arg.get_name()); + else + compoundFlags.emplace_back(arg, value); + } else + compoundFlags.emplace_back(arg, value); + } + + for (const auto& [i, kv] : enumerate(singleFlags)) + { + const auto& [key, value] = kv; + aligner += '['; + aligner += key; + for (const auto& name : value) + aligner += name; + aligner += ']'; + aligner += ' '; + } + + for (const auto& [i, kv] : enumerate(compoundFlags)) + { + const auto& name = kv.first; + const auto& builder = kv.second; + aligner += builder->m_required ? '<' : '['; + aligner += name.get_argument(); + auto lambda = [&]() { + aligner += ' '; + aligner += builder->m_metavar.value_or(string::toUpperCase(name.get_name())); + }; + std::visit(lambda_visitor{ + [&](const nargs_t type) { + lambda(); + switch (type) + { + case nargs_t::IF_POSSIBLE: + break; + case nargs_t::ALL: + case nargs_t::ALL_AT_LEAST_ONE: + aligner += "..."; + break; + } + }, + [&](const int argc) { + for (int j = 0; j < argc; j++) + lambda(); + } + }, builder->m_nargs); + aligner += builder->m_required ? '>' : ']'; + aligner += ' '; + } + + for (const auto& [i, pair] : enumerate(m_positional_arguments)) + { + const auto& [name, _] = pair; + aligner += '<'; + aligner += name; + aligner += '>'; + if (i != m_positional_arguments.size() - 1) + aligner += ' '; + } + + m_usage = aligner.str(); + } + std::cout << "Usage: " << *m_usage << std::endl; + } + + void argument_parser_t::print_version() const + { + std::cout << m_name.value_or("NO NAME") << " " << m_version.value_or("NO VERSION") << std::endl; + } + + void argument_parser_t::handle_compound_flags(hashset_t& found_flags, argument_storage_t& parsed_args, argument_consumer_t& consumer, + const argument_string_t& arg) + { + // i kinda hate this, TODO? + std::vector compound_flags; + if (arg.get_flag().size() == 1) + { + for (const auto c : arg.get_name()) + compound_flags.emplace_back(std::string{arg.get_flag()} + c); + } else + { + if (arg.get_flag().size() > 2) + throw detail::bad_flag(make_string("Error: Flag '", arg.get_argument(), "' is too long!")); + compound_flags.emplace_back(arg.get_argument()); + } + + for (const auto& flag_key : compound_flags) + { + const auto flag = m_flag_arguments.find(flag_key); + if (flag == m_flag_arguments.end()) + throw detail::bad_flag(make_string("Error: Unknown flag: ", flag_key)); + found_flags.insert(flag_key); + parse_flag(parsed_args, consumer, flag_key); + } + } + + void argument_parser_t::parse_flag(argument_storage_t& parsed_args, argument_consumer_t& consumer, const std::string_view arg) + { + auto flag = m_flag_arguments.find(arg)->second; + const auto dest = flag->m_dest.value_or(std::string{arg}); + std::visit(lambda_visitor{ + [&parsed_args, &consumer, &dest, &flag, arg](const nargs_t arg_enum) { + switch (arg_enum) + { + case nargs_t::IF_POSSIBLE: + if (consumer.can_consume() && !consumer.peek().is_flag()) + flag->m_dest_func(dest, parsed_args, consumer.consume().get_argument()); + else + { + if (flag->m_const_value) + parsed_args.m_data.insert({dest, *flag->m_const_value}); + } + break; + case nargs_t::ALL_AT_LEAST_ONE: + if (!consumer.can_consume()) + throw detail::missing_argument_error( + make_string("Error expected at least one argument to be consumed by '", arg, '\'')); + [[fallthrough]]; + case nargs_t::ALL: + auto result = consume_until_flag_or_end(consumer, flag->m_choices ? &*flag->m_choices : nullptr); + if (!result) + throw detail::bad_choice_error(make_string('\'', consumer.peek().get_argument(), + "' is not a valid choice for argument '", arg, + "'! Expected one of ", result.error())); + flag->m_dest_vec_func(dest, parsed_args, result.value()); + break; + } + }, + [&parsed_args, &consumer, &dest, &flag, arg, this](const i32 argc) { + const auto args = consume_argc(argc, consumer, flag->m_choices ? &*flag->m_choices : nullptr, arg); + + switch (flag->m_action) + { + case action_t::STORE: + if (argc == 0) + throw detail::missing_argument_error( + make_string("Argument '", arg, "'s action is store but takes in no arguments?")); + if (argc == 1) + flag->m_dest_func(dest, parsed_args, args.front()); + else + throw detail::unexpected_argument_error(make_string("Argument '", arg, + "'s action is store but takes in more than one argument. " + "Did you mean to use action_t::APPEND or action_t::EXTEND?")); + break; + case action_t::APPEND: + case action_t::EXTEND: + if (argc == 0) + throw detail::missing_argument_error( + make_string("Argument '", arg, "'s action is append or extend but takes in no arguments.")); + flag->m_dest_vec_func(dest, parsed_args, args); + break; + case action_t::APPEND_CONST: + if (argc != 0) + throw detail::unexpected_argument_error( + make_string("Argument '", arg, "'s action is append const but takes in arguments.")); + if (!flag->m_const_value) + { + throw detail::missing_value_error(make_string( + "Append const chosen as an action but const value not provided for argument '", arg, '\'')); + } + if (parsed_args.contains(dest)) + { + auto& data = parsed_args.m_data[dest]; + std::visit(detail::arg_meta_type_helper_t::make_visitor([arg](auto& primitive) { + throw detail::type_error(make_string("Invalid type for argument '", arg, "' expected list type, found '", + blt::type_string(), "' with value ", primitive)); + }, [&flag, arg](auto& vec) { + using type = typename meta::remove_cvref_t::value_type; + if (!std::holds_alternative(*flag->m_const_value)) + { + throw detail::type_error(make_string("Constant value for argument '", arg, + "' type doesn't match values already present! Expected to be of type '", + blt::type_string(), "'!")); + } + vec.push_back(std::get(*flag->m_const_value)); + }), data); + } else + { + std::visit(detail::arg_meta_type_helper_t::make_visitor([&parsed_args, &dest](auto& primitive) { + std::vector> vec; + vec.emplace_back(primitive); + parsed_args.m_data.emplace(dest, std::move(vec)); + }, [](auto&) { + throw detail::type_error("Append const should not be a list type!"); + }), *flag->m_const_value); + } + break; + case action_t::STORE_CONST: + if (argc != 0) + { + print_usage(); + throw detail::unexpected_argument_error( + make_string("Argument '", arg, "' is store const but called with an argument.")); + } + if (!flag->m_const_value) + throw detail::missing_value_error( + make_string("Argument '", arg, "' is store const, but const storage has no value.")); + parsed_args.m_data.emplace(dest, *flag->m_const_value); + break; + case action_t::STORE_TRUE: + if (argc != 0) + { + print_usage(); + throw detail::unexpected_argument_error("Store true flag called with an argument."); + } + parsed_args.m_data.emplace(dest, true); + break; + case action_t::STORE_FALSE: + if (argc != 0) + { + print_usage(); + throw detail::unexpected_argument_error("Store false flag called with an argument."); + } + parsed_args.m_data.insert({dest, false}); + break; + case action_t::COUNT: + if (parsed_args.m_data.contains(dest)) + { + auto visitor = detail::arg_meta_type_helper_t::make_visitor([](auto& primitive) -> detail::arg_data_t { + using type = meta::remove_cvref_t; + if constexpr (std::is_convertible_v) + { + return primitive + static_cast(1); + } else + throw detail::type_error("Error: count called but stored type is " + blt::type_string()); + }, [](auto&) -> detail::arg_data_t { + throw detail::type_error( + "List present on count. This condition doesn't make any sense! " + "(How did we get here, please report this!)"); + }); + parsed_args.m_data[dest] = std::visit(visitor, parsed_args.m_data[dest]); + } else // I also hate this! + flag->m_dest_func(dest, parsed_args, "1"); + break; + case action_t::HELP: + print_help(); + std::exit(0); + case action_t::VERSION: + print_version(); + std::exit(0); + } + } + }, flag->m_nargs); + } + + void argument_parser_t::parse_positional(argument_storage_t& parsed_args, argument_consumer_t& consumer, argument_positional_storage_t& storage, + const std::string_view arg) + { + if (!storage.has_positional()) + throw detail::missing_argument_error(make_string("Error: '", arg, "' positional argument does not match any defined for this parser")); + auto& positional = storage.next(); + const auto dest = positional.m_dest.value_or(std::string{arg}); + std::visit(lambda_visitor{ + [&consumer, &positional, &dest, &parsed_args, arg](const nargs_t arg_enum) { + switch (arg_enum) + { + case nargs_t::IF_POSSIBLE: + throw detail::bad_positional( + "Positional argument asked to consume if possible. We do not consider this to be a valid ask."); + case nargs_t::ALL_AT_LEAST_ONE: + if (!consumer.can_consume()) + throw detail::missing_argument_error( + make_string("Error expected at least one argument to be consumed by '", arg, '\'')); + [[fallthrough]]; + case nargs_t::ALL: + auto result = consume_until_flag_or_end(consumer, positional.m_choices ? &*positional.m_choices : nullptr); + if (!result) + throw detail::bad_choice_error(make_string('\'', consumer.peek().get_argument(), + "' is not a valid choice for argument '", arg, + "'! Expected one of ", result.error())); + positional.m_dest_vec_func(dest, parsed_args, result.value()); + break; + } + }, + [this, &consumer, &positional, &dest, &parsed_args, arg](const i32 argc) { + const auto args = consume_argc(argc, consumer, positional.m_choices ? &*positional.m_choices : nullptr, arg); + + switch (positional.m_action) + { + case action_t::STORE: + if (argc == 0) + throw detail::missing_argument_error( + make_string("Argument '", arg, "'s action is store but takes in no arguments?")); + if (argc == 1) + positional.m_dest_func(dest, parsed_args, args.front()); + else + throw detail::unexpected_argument_error(make_string("Argument '", arg, + "'s action is store but takes in more than one argument. " + "Did you mean to use action_t::APPEND or action_t::EXTEND?")); + break; + case action_t::APPEND: + case action_t::EXTEND: + if (argc == 0) + throw detail::missing_argument_error( + make_string("Argument '", arg, "'s action is append or extend but takes in no arguments.")); + positional.m_dest_vec_func(dest, parsed_args, args); + break; + case action_t::APPEND_CONST: + throw detail::bad_positional("action_t::APPEND_CONST does not make sense for positional arguments"); + case action_t::STORE_CONST: + throw detail::bad_positional("action_t::STORE_CONST does not make sense for positional arguments"); + case action_t::STORE_TRUE: + throw detail::bad_positional("action_t::STORE_TRUE does not make sense for positional arguments"); + case action_t::STORE_FALSE: + throw detail::bad_positional("action_t::STORE_FALSE does not make sense for positional arguments"); + case action_t::COUNT: + throw detail::bad_positional("action_t::COUNT does not make sense for positional arguments"); + case action_t::HELP: + print_help(); + std::exit(0); + case action_t::VERSION: + print_version(); + std::exit(0); + } + } + }, positional.m_nargs); + } + + void argument_parser_t::handle_missing_and_default_args(hashmap_t& arguments, + const hashset_t& found, argument_storage_t& parsed_args, + const std::string_view type) + { + for (const auto& [key, value] : arguments) + { + if (!found.contains(key)) + { + if (value->m_required) + throw detail::missing_argument_error(make_string("Error: ", type, " argument '", key, + "' was not found but is required by the program")); + auto dest = value->m_dest.value_or(std::string{key}); + if (value->m_default_value && !parsed_args.contains(dest)) + parsed_args.m_data.emplace(dest, *value->m_default_value); + } + } + } + + expected, std::string> argument_parser_t::consume_until_flag_or_end(argument_consumer_t& consumer, + hashset_t* allowed_choices) + { + std::vector args; + while (consumer.can_consume() && !consumer.peek().is_flag()) + { + if (allowed_choices != nullptr && !allowed_choices->contains(consumer.peek().get_argument())) + { + std::string valid_choices = "{"; + for (const auto& [i, choice] : enumerate(*allowed_choices)) + { + valid_choices += choice; + if (i != allowed_choices->size() - 1) + valid_choices += ", "; + } + valid_choices += "}"; + return unexpected(valid_choices); + } + args.emplace_back(consumer.consume().get_argument()); + } + return args; + } + + std::vector argument_parser_t::consume_argc(const int argc, argument_consumer_t& consumer, hashset_t* allowed_choices, + const std::string_view arg) + { + std::vector args; + for (i32 i = 0; i < argc; ++i) + { + if (!consumer.can_consume()) + { + throw detail::missing_argument_error(make_string("Expected ", argc, " arguments to be consumed by '", arg, "' but found ", i)); + } + if (consumer.peek().is_flag()) + { + std::cout << "Warning: arg '" << arg << "' expects " << argc << " arguments to be consumed but we found a flag '" << consumer.peek(). + get_argument() + << "'. We will comply as this may be desired if this argument is a file." << std::endl; + } + if (allowed_choices != nullptr && !allowed_choices->contains(consumer.peek().get_argument())) + { + std::string valid_choices = "{"; + for (const auto& [i, choice] : enumerate(*allowed_choices)) + { + valid_choices += choice; + if (i != allowed_choices->size() - 1) + valid_choices += ", "; + } + valid_choices += "}"; + throw detail::bad_choice_error(make_string('\'', consumer.peek().get_argument(), "' is not a valid choice for argument '", arg, + "'! Expected one of ", valid_choices)); + } + args.emplace_back(consumer.consume().get_argument()); + } + if (args.size() != static_cast(argc)) + { + throw std::runtime_error( + "This error condition should not be possible. " "Args consumed didn't equal the arguments requested and previous checks didn't fail. " + "Please report as an issue on the GitHub"); + } + return args; + } + + std::pair argument_subparser_t::parse(argument_consumer_t& consumer) + { + if (!consumer.can_consume()) + throw detail::missing_argument_error("Subparser requires an argument."); + const auto key = consumer.consume(); + if (key.is_flag()) + throw detail::subparse_error(key.get_argument(), to_vec(get_allowed_strings())); + const auto it = m_aliases.find(key.get_name()); + if (it == m_aliases.end()) + throw detail::subparse_error(key.get_argument(), to_vec(get_allowed_strings())); + it->second->m_name = m_parent->m_name; + m_last_parsed_parser = key.get_name(); + return {key, it->second->parse(consumer)}; + } + + hashmap_t> argument_subparser_t::get_allowed_strings() const + { + hashmap_t> map; + for (const auto& [key, value] : m_aliases) + map[value].emplace_back(key); + return map; + } + + std::vector> argument_subparser_t::to_vec(const hashmap_t>& map) + { + std::vector> vec; + for (const auto& [key, value] : map) + vec.push_back(value); + return vec; + } + + namespace detail + { + // Unit Tests for class argument_string_t + // Test Case 1: Ensure the constructor handles flags correctly + void test_argument_string_t_flag_basic(const hashset_t& prefixes) + { + const argument_string_t arg("-f", prefixes); + BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); + BLT_ASSERT(arg.value() == "f" && "Flag value should match the input string."); + } + + // Test Case 2: Ensure the constructor handles long flags correctly + void test_argument_string_t_long_flag(const hashset_t& prefixes) + { + const argument_string_t arg("--file", prefixes); + BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); + BLT_ASSERT(arg.value() == "file" && "Long flag value should match the input string."); + } + + // Test Case 3: Ensure positional arguments are correctly identified + void test_argument_string_t_positional_argument(const hashset_t& prefixes) + { + const argument_string_t arg("filename.txt", prefixes); + BLT_ASSERT(!arg.is_flag() && "Expected argument to be identified as positional."); + BLT_ASSERT(arg.value() == "filename.txt" && "Positional argument value should match the input string."); + } + + // Test Case 5: Handle an empty string + void test_argument_string_t_empty_input(const hashset_t& prefixes) + { + const argument_string_t arg("", prefixes); + BLT_ASSERT(!arg.is_flag() && "Expected an empty input to be treated as positional, not a flag."); + BLT_ASSERT(arg.value().empty() && "Empty input should have an empty value."); + } + + // Test Case 6: Handle edge case of a single hyphen (`-`) which might be ambiguous + void test_argument_string_t_single_hyphen(const hashset_t& prefixes) + { + const argument_string_t arg("-", prefixes); + BLT_ASSERT(arg.is_flag() && "Expected single hyphen (`-`) to be treated as a flag."); + BLT_ASSERT(arg.value().empty() && "Single hyphen flag should have empty value."); + BLT_ASSERT(arg.get_flag() == "-" && "Single hyphen flag should match the input string."); + } + + // Test Case 8: Handle arguments with prefix only (like "--") + void test_argument_string_t_double_hyphen(const hashset_t& prefixes) + { + const argument_string_t arg("--", prefixes); + BLT_ASSERT(arg.is_flag() && "Double hyphen ('--') should be treated as a flag."); + BLT_ASSERT(arg.value().empty() && "Double hyphen flag should have empty value."); + BLT_ASSERT(arg.get_flag() == "--" && "Double hyphen value should match the input string."); + } + + // Test Case 9: Validate edge case of an argument with spaces + void test_argument_string_t_with_spaces(const hashset_t& prefixes) + { + const argument_string_t arg(" ", prefixes); + BLT_ASSERT(!arg.is_flag() && "Arguments with spaces should not be treated as flags."); + BLT_ASSERT(arg.value() == " " && "Arguments with spaces should match the input string."); + } + + // Test Case 10: Validate arguments with numeric characters + void test_argument_string_t_numeric_flag(const hashset_t& prefixes) + { + const argument_string_t arg("-123", prefixes); + BLT_ASSERT(arg.is_flag() && "Numeric flags should still be treated as flags."); + BLT_ASSERT(arg.value() == "123" && "Numeric flag value should match the input string."); + } + + // Test Case 11: Ensure the constructor handles '+' flag correctly + void test_argument_string_t_plus_flag_basic(const hashset_t& prefixes) + { + const argument_string_t arg("+f", prefixes); + BLT_ASSERT(arg.is_flag() && "Expected argument to be identified as a flag."); + BLT_ASSERT(arg.value() == "f" && "Plus flag value should match the input string."); + } + + // Test Case 13: Handle edge case of a single plus (`+`) which might be ambiguous + void test_argument_string_t_single_plus(const hashset_t& prefixes) + { + const argument_string_t arg("+", prefixes); + BLT_ASSERT(arg.is_flag() && "Expected single plus (`+`) to be treated as a flag."); + BLT_ASSERT(arg.value().empty() && "Single plus flag should have empty value."); + BLT_ASSERT(arg.get_flag() == "+" && "Single plus flag should match the input string."); + } + + // Test Case 14: Handle arguments with prefix only (like '++') + void test_argument_string_t_double_plus(const hashset_t& prefixes) + { + const argument_string_t arg("++", prefixes); + BLT_ASSERT(arg.is_flag() && "Double plus ('++') should be treated as a flag."); + BLT_ASSERT(arg.value().empty() && "Double plus flag should have empty value."); + BLT_ASSERT(arg.get_flag() == "++" && "Double plus value should match the input string."); + } + + void run_all_tests_argument_string_t() + { + const hashset_t prefixes = {'-', '+'}; + test_argument_string_t_flag_basic(prefixes); + test_argument_string_t_long_flag(prefixes); + test_argument_string_t_positional_argument(prefixes); + test_argument_string_t_empty_input(prefixes); + test_argument_string_t_single_hyphen(prefixes); + test_argument_string_t_double_hyphen(prefixes); + test_argument_string_t_with_spaces(prefixes); + test_argument_string_t_numeric_flag(prefixes); + test_argument_string_t_plus_flag_basic(prefixes); + test_argument_string_t_single_plus(prefixes); + test_argument_string_t_double_plus(prefixes); + } + + void test_argparse_empty() + { + const std::vector argv{"./program"}; + argument_parser_t parser; + const auto args = parser.parse(argv); + BLT_ASSERT(args.size() == 0 && "Empty argparse should have no args on output"); + } + + void test_single_flag_prefixes() + { + argument_parser_t parser; + parser.add_flag("-a").set_action(action_t::STORE_TRUE); + parser.add_flag("+b").set_action(action_t::STORE_FALSE); + parser.add_flag("/c").as_type().set_action(action_t::STORE); + + const std::vector args = {"./program", "-a", "+b", "/c", "42"}; + const auto parsed_args = parser.parse(args); + + BLT_ASSERT(parsed_args.get("-a") == true && "Flag '-a' should store `true`"); + BLT_ASSERT(parsed_args.get("+b") == false && "Flag '+b' should store `false`"); + BLT_ASSERT(parsed_args.get("/c") == 42 && "Flag '/c' should store the value 42"); + } + + // Test: Invalid flag prefixes + void test_invalid_flag_prefixes() + { + argument_parser_t parser; + parser.add_flag("-a"); + parser.add_flag("+b"); + parser.add_flag("/c"); + + const std::vector args = {"./program", "!d", "-a"}; + try + { + parser.parse(args); + BLT_ASSERT(false && "Parsing should fail with invalid flag prefix '!'"); + } catch (...) + { + BLT_ASSERT(true && "Correctly threw on bad flag prefix"); + } + } + + void test_compound_flags() + { + argument_parser_t parser; + parser.add_flag("-v").as_type().set_action(action_t::COUNT); + + const std::vector args = {"./program", "-vvv"}; + const auto parsed_args = parser.parse(args); + + BLT_ASSERT(parsed_args.get("-v") == 3 && "Flag '-v' should count occurrences in compound form"); + } + + void test_combination_of_valid_and_invalid_flags() + { + using namespace argparse; + + argument_parser_t parser; + parser.add_flag("-x").as_type(); + parser.add_flag("/y").as_type(); + + const std::vector args = {"./program", "-x", "10", "!z", "/y", "value"}; + try + { + parser.parse(args); + BLT_ASSERT(false && "Parsing should fail due to invalid flag '!z'"); + } catch (...) + { + BLT_ASSERT(true && "Correctly threw an exception for invalid flag"); + } + } + + void test_flags_with_different_actions() + { + using namespace argparse; + + argument_parser_t parser; + parser.add_flag("-k").as_type().set_action(action_t::STORE); // STORE action + parser.add_flag("-t").as_type().set_action(action_t::STORE_CONST).set_const(999); // STORE_CONST action + parser.add_flag("-f").set_action(action_t::STORE_FALSE); // STORE_FALSE action + parser.add_flag("-c").set_action(action_t::STORE_TRUE); // STORE_TRUE action + + const std::vector args = {"./program", "-k", "100", "-t", "-f", "-c"}; + const auto parsed_args = parser.parse(args); + + BLT_ASSERT(parsed_args.get("-k") == 100 && "Flag '-k' should store 100"); + BLT_ASSERT(parsed_args.get("-t") == 999 && "Flag '-t' should store a const value of 999"); + BLT_ASSERT(parsed_args.get("-f") == false && "Flag '-f' should store `false`"); + BLT_ASSERT(parsed_args.get("-c") == true && "Flag '-c' should store `true`"); + } + + // Helper function to simulate argument parsing for nargs tests + bool parse_arguments(const std::vector& args, const nargs_v expected_nargs) + { + argument_parser_t parser; + + std::vector arg_strings; + arg_strings.reserve(args.size()); + for (const auto& arg : args) + arg_strings.emplace_back(arg, parser.get_allowed_flag_prefixes()); + argument_consumer_t consumer{arg_strings}; + + parser.add_positional("positional").set_nargs(expected_nargs); + try + { + auto parsed_args = parser.parse(consumer); + return consumer.remaining() == 0; + } catch (const std::exception&) + { + return false; + } + } + + // Test case for nargs = 0 + void test_nargs_0() + { + std::cout << "[Running Test: test_nargs_0]\n"; + + // Valid case: No arguments + const std::vector valid_args = {"./program"}; + BLT_ASSERT(!parse_arguments(valid_args, 0) && "nargs=0: Should fail"); + + // Invalid case: 1 argument + const std::vector invalid_args = {"./program", "arg1"}; + BLT_ASSERT(!parse_arguments(invalid_args, 0) && "nargs=0: Should not accept any arguments"); + + std::cout << "Success: test_nargs_0\n"; + } + + // Test case for nargs = 1 + void test_nargs_1() + { + std::cout << "[Running Test: test_nargs_1]\n"; + + // Valid case: 1 argument + const std::vector valid_args = {"./program", "arg1"}; + BLT_ASSERT(parse_arguments(valid_args, 1) && "nargs=1: Should accept exactly 1 argument"); + + // Invalid case: 0 arguments + const std::vector invalid_args_0 = {"./program"}; + BLT_ASSERT(!parse_arguments(invalid_args_0, 1) && "nargs=1: Should not accept 0 arguments"); + + // Invalid case: 2 arguments + const std::vector invalid_args_2 = {"./program", "arg1", "arg2"}; + BLT_ASSERT(!parse_arguments(invalid_args_2, 1) && "nargs=1: Should not accept more than 1 argument"); + + std::cout << "Success: test_nargs_1\n"; + } + + // Test case for nargs = 2 + void test_nargs_2() + { + std::cout << "[Running Test: test_nargs_2]\n"; + + // Valid case: 2 arguments + const std::vector valid_args = {"./program", "arg1", "arg2"}; + BLT_ASSERT(!parse_arguments(valid_args, 2) && "nargs=2: Should fail as action is store"); + + // Invalid case: 0 arguments + const std::vector invalid_args_0 = {"./program"}; + BLT_ASSERT(!parse_arguments(invalid_args_0, 2) && "nargs=2: Should not accept 0 arguments"); + + // Invalid case: 1 argument + const std::vector invalid_args_1 = {"./program", "arg1"}; + BLT_ASSERT(!parse_arguments(invalid_args_1, 2) && "nargs=2: Should not accept less than 2 arguments"); + + // Invalid case: 3 arguments + const std::vector invalid_args_3 = {"./program", "arg1", "arg2", "arg3"}; + BLT_ASSERT(!parse_arguments(invalid_args_3, 2) && "nargs=2: Should not accept more than 2 arguments"); + + std::cout << "Success: test_nargs_2\n"; + } + + void test_nargs_all() + { + std::cout << "[Running Test: test_nargs_all]\n"; + + // Valid case: No arguments + const std::vector valid_args_0 = {"./program"}; + BLT_ASSERT(!parse_arguments(valid_args_0, argparse::nargs_t::ALL) && "nargs=ALL: No arguments present. Should fail.)"); + + // Valid case: Multiple arguments + const std::vector valid_args_2 = {"./program", "arg1", "arg2"}; + BLT_ASSERT(parse_arguments(valid_args_2, argparse::nargs_t::ALL) && "nargs=ALL: Should accept all remaining arguments"); + + // Valid case: Many arguments + const std::vector valid_args_many = {"./program", "arg1", "arg2", "arg3", "arg4"}; + BLT_ASSERT(parse_arguments(valid_args_many, argparse::nargs_t::ALL) && "nargs=ALL: Should accept all remaining arguments"); + + std::cout << "Success: test_nargs_all\n"; + } + + // Test case for nargs_t::ALL_AT_LEAST_ONE + void test_nargs_all_at_least_one() + { + std::cout << "[Running Test: test_nargs_all_at_least_one]\n"; + + // Valid case: 1 argument + const std::vector valid_args_1 = {"./program", "arg1"}; + BLT_ASSERT(parse_arguments(valid_args_1, argparse::nargs_t::ALL_AT_LEAST_ONE) && + "nargs=ALL_AT_LEAST_ONE: Should accept at least one argument and consume it"); + + // Valid case: Multiple arguments + const std::vector valid_args_3 = {"./program", "arg1", "arg2", "arg3"}; + BLT_ASSERT(parse_arguments(valid_args_3, argparse::nargs_t::ALL_AT_LEAST_ONE) && + "nargs=ALL_AT_LEAST_ONE: Should accept at least one argument and consume all remaining arguments"); + + // Invalid case: No arguments + const std::vector invalid_args_0 = {"./program"}; + BLT_ASSERT(!parse_arguments(invalid_args_0, argparse::nargs_t::ALL_AT_LEAST_ONE) && + "nargs=ALL_AT_LEAST_ONE: Should reject if no arguments are provided"); + + std::cout << "Success: test_nargs_all_at_least_one\n"; + } + + void run_combined_flag_test() + { + std::cout << "[Running Test: run_combined_flag_test]\n"; + argument_parser_t parser; + + parser.add_flag("-a").set_action(action_t::STORE_TRUE); + parser.add_flag("--deep").set_action(action_t::STORE_FALSE); + parser.add_flag("-b", "--combined").set_action(action_t::STORE_CONST).set_const(50); + parser.add_flag("--append").set_action(action_t::APPEND).as_type(); + parser.add_flag("--required").set_required(true); + parser.add_flag("--default").set_default("I am a default value"); + parser.add_flag("-t").set_action(action_t::APPEND_CONST).set_dest("test").set_const(5); + parser.add_flag("-g").set_action(action_t::APPEND_CONST).set_dest("test").set_const(10); + parser.add_flag("-e").set_action(action_t::APPEND_CONST).set_dest("test").set_const(15); + parser.add_flag("-f").set_action(action_t::APPEND_CONST).set_dest("test").set_const(20); + parser.add_flag("-d").set_action(action_t::APPEND_CONST).set_dest("test").set_const(25); + parser.add_flag("--end").set_action(action_t::EXTEND).set_dest("wow").as_type(); + + const auto a1 = make_arguments("-a", "--required", "hello"); + const auto r1 = parser.parse(a1); + BLT_ASSERT(r1.get("-a") == true && "Flag '-a' should store true"); + BLT_ASSERT(r1.get("--default") == "I am a default value" && "Flag '--default' should store default value"); + BLT_ASSERT(r1.get("--required") == "hello" && "Flag '--required' should store 'hello'"); + + const auto a2 = make_arguments("-a", "--deep", "--required", "soft"); + const auto r2 = parser.parse(a2); + BLT_ASSERT(r2.get("-a") == true && "Flag '-a' should store true"); + BLT_ASSERT(r2.get("--deep") == false && "Flag '--deep' should store false"); + BLT_ASSERT(r2.get("--required") == "soft" && "Flag '--required' should store 'soft'"); + + const auto a3 = make_arguments("--required", "silly", "--combined", "-t", "-f", "-e"); + const auto r3 = parser.parse(a3); + BLT_ASSERT((r3.get>("test") == std::vector{5, 20, 15}) && "Flags should add to vector of {5, 20, 15}"); + BLT_ASSERT(r3.get("-b") == 50 && "Combined flag should store const of 50"); + + const auto a4 = make_arguments("--required", "crazy", "--end", "10", "12.05", "68.11", "100.00", "200532", "-d", "-t", "-g", "-e", "-f"); + const auto r4 = parser.parse(a4); + BLT_ASSERT( + (r4.get>("test") == std::vector{25, 5, 10, 15, 20}) && + "Expected test vector to be filled with all arguments in order of flags"); + BLT_ASSERT( + (r4.get>("wow") == std::vector{10, 12.05, 68.11, 100.00, 200532}) && + "Extend vector expected to contain all elements"); + + std::cout << "Success: run_combined_flag_test\n"; + } + + void run_choice_test() + { + std::cout << "[Running Test: run_choice_test]\n"; + argument_parser_t parser; + + parser.add_flag("--hello").set_choices("silly", "crazy", "soft"); + parser.add_positional("iam").set_choices("different", "choices", "for", "me"); + + const auto a1 = make_arguments("--hello", "crazy", "different"); + const auto r1 = parser.parse(a1); + BLT_ASSERT(r1.get("--hello") == "crazy" && "Flag '--hello' should store 'crazy'"); + BLT_ASSERT(r1.get("iam") == "different" && "Positional 'iam' should store 'different'"); + + const auto a2 = make_arguments("--hello", "not_an_option", "different"); + try + { + parser.parse(a2); + BLT_ASSERT(false && "Parsing should fail due to invalid flag '--hello'"); + } catch (...) + {} + + const auto a3 = make_arguments("--hello", "crazy", "not_a_choice"); + try + { + parser.parse(a3); + BLT_ASSERT(false && "Parsing should fail due to invalid positional 'iam'"); + } catch (...) + {} + + std::cout << "Success: run_choice_test\n"; + } + + void run_subparser_test() + { + std::cout << "[Running Test: run_subparser_test]\n"; + argument_parser_t parser; + + parser.add_flag("--open").make_flag(); + + const auto subparser = parser.add_subparser("mode"); + + const auto n1 = subparser->add_parser("n1"); + n1->add_flag("--silly").make_flag(); + n1->add_positional("path"); + + const auto n2 = subparser->add_parser("n2"); + n2->add_flag("--crazy").make_flag(); + n2->add_positional("path"); + n2->add_positional("output"); + + const auto n3 = subparser->add_parser("n3"); + n3->add_flag("--deep").make_flag(); + + const auto a1 = make_arguments("n1", "--silly"); + try + { + parser.parse(a1); + BLT_ASSERT(false && "Subparser should throw an error when positional not supplied"); + } catch (...) + {} + + const auto a2 = make_arguments("--open"); + try + { + parser.parse(a2); + BLT_ASSERT(false && "Subparser should throw an error when no subparser is supplied"); + } catch (...) + {} + + const auto a3 = make_arguments("n1", "--silly", "path_n1"); + const auto r3 = parser.parse(a3); + BLT_ASSERT(r3.get("--open") == false && "Flag '--open' should default to false"); + BLT_ASSERT(r3.get("mode") == "n1" && "Subparser should store 'n1'"); + BLT_ASSERT(r3.get("path") == "path_n1" && "Subparser path should be 'path'"); + + const auto a4 = make_arguments("n2", "--crazy", "path"); + + try + { + parser.parse(a4); + BLT_ASSERT(false && "Subparser should throw an error when second positional is not supplied"); + } catch (...) + {} + + const auto a5 = make_arguments("--open", "n2", "path_n2", "output_n2"); + const auto r5 = parser.parse(a5); + BLT_ASSERT(r5.get("--open") == true && "Flag '--open' should store true"); + BLT_ASSERT(r5.get("mode") == "n2" && "Subparser should store 'n2'"); + BLT_ASSERT(r5.get("path") == "path_n2" && "Subparser path should be 'path'"); + BLT_ASSERT(r5.get("output") == "output_n2" && "Subparser output should be 'output'"); + + const auto a6 = make_arguments("not_an_option", "silly"); + + try + { + parser.parse(a6); + BLT_ASSERT(false && "Subparser should throw an error when first positional is not a valid subparser"); + } catch (const std::exception&) + {} + + const auto a7 = make_arguments("n3"); + const auto r7 = parser.parse(a7); + BLT_ASSERT(r7.get("mode") == "n3" && "Subparser should store 'n3'"); + + std::cout << "Success: run_subparser_test\n"; + } + + void run_argparse_flag_tests() + { + test_single_flag_prefixes(); + test_invalid_flag_prefixes(); + test_compound_flags(); + test_combination_of_valid_and_invalid_flags(); + test_flags_with_different_actions(); + run_combined_flag_test(); + run_choice_test(); + run_subparser_test(); + } + + void run_all_nargs_tests() + { + test_nargs_0(); + test_nargs_1(); + test_nargs_2(); + test_nargs_all(); + test_nargs_all_at_least_one(); + std::cout << "All nargs tests passed successfully.\n"; + } + + void test() + { + run_all_tests_argument_string_t(); + test_argparse_empty(); + run_argparse_flag_tests(); + run_all_nargs_tests(); + } + + [[nodiscard]] std::string subparse_error::error_string() const + { + std::string message = "Subparser Error: "; + message += m_found_string; + message += " is not a valid command. Allowed commands are: {"; + for (const auto [i, allowed_string] : enumerate(m_allowed_strings)) + { + if (allowed_string.size() > 1) + message += '['; + for (const auto [j, alias] : enumerate(allowed_string)) + { + message += alias; + if (static_cast(j) < static_cast(allowed_string.size()) - 2) + message += ", "; + else if (static_cast(j) < static_cast(allowed_string.size()) - 1) + message += ", or "; + } + if (allowed_string.size() > 1) + message += ']'; + if (i != m_allowed_strings.size() - 1) + message += ", "; + } + message += "}"; + return message; + } + } }