diff --git a/CMakeLists.txt b/CMakeLists.txt index cfb847b..ae8b177 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) include(cmake/color.cmake) -set(BLT_VERSION 4.0.7) +set(BLT_VERSION 4.0.8) set(BLT_TARGET BLT) diff --git a/include/blt/parse/argparse_v2.h b/include/blt/parse/argparse_v2.h index e72fc0f..359750a 100644 --- a/include/blt/parse/argparse_v2.h +++ b/include/blt/parse/argparse_v2.h @@ -87,7 +87,7 @@ namespace blt::argparse explicit missing_argument_error(const std::string& message): std::runtime_error(message) { } - } + }; class subparse_error final : public std::exception { @@ -108,31 +108,7 @@ namespace blt::argparse return m_found_string; } - [[nodiscard]] std::string 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 (j < alias.size() - 2) - message += ", "; - else if (j < alias.size()) - message += ", or "; - } - if (allowed_string.size() > 1) - message += ']'; - if (i != m_allowed_strings.size() - 1) - message += ' '; - } - message += "}"; - return message; - } + [[nodiscard]] std::string error_string() const; [[nodiscard]] const char* what() const override { @@ -147,18 +123,13 @@ namespace blt::argparse template struct arg_data_helper_t { - using arg_primitive_data_t = std::variant; - using arg_list_data_t = std::variant...>; + using variant_t = std::variant...>; }; - using data_helper_t = arg_data_helper_t; - - using arg_primitive_data_t = data_helper_t::arg_primitive_data_t; - using arg_list_data_t = data_helper_t::arg_list_data_t; - using arg_data_t = std::variant; + using arg_data_t = arg_data_helper_t::variant_t; template - struct arg_type_t + struct arg_string_converter_t { static T convert(const std::string_view value) { @@ -201,7 +172,7 @@ namespace blt::argparse { public: explicit argument_string_t(const char* input, const hashset_t& allowed_flag_prefix): m_argument(input), - allowed_flag_prefix(&allowed_flag_prefix) + m_allowed_flag_prefix(&allowed_flag_prefix) { if (input == nullptr) throw detail::bad_flag("Argument cannot be null!"); @@ -234,10 +205,33 @@ namespace blt::argparse } private: + /** + * This function takes the command line argument represented by this class, + * stored in m_argument and converts into flag side and name side arguments. + * + * What this means is in the case of a flag provided, for example passing --foo or --bar, this function will split the argument into + * + * m_flag_section = "--" + * m_name_section = "foo" || "bar" + * + * If the argument is purely positional, meaning there is no flag prefix, + * this function will do nothing and m_flag_section will be true for .empty() + * + * For example, if you provide res/some/folder/or/file as a command line positional argument, + * this function will create the following internal state: + * + * m_flag_section = "" + * m_flag_section.empty() == true + * m_name_section = "res/some/folder/or/file" + * + * m_argument is not modified by this function + */ void process_argument() { + // user provides a list of allowed prefix characters to argument_parser_t, which is then provided to this class at construction time + // it is not the job of this class to validate flag prefixes beyond this. // TODO: requiring this pointer is a code smell. size_t start = 0; - for (; start < m_argument.size() && allowed_flag_prefix->contains(m_argument[start]); start++) + for (; start < m_argument.size() && m_allowed_flag_prefix->contains(m_argument[start]); start++) { } @@ -248,44 +242,54 @@ namespace blt::argparse std::string_view m_argument; std::string_view m_flag_section; std::string_view m_name_section; - const hashset_t* allowed_flag_prefix; + const hashset_t* m_allowed_flag_prefix; }; class argument_consumer_t { public: - explicit argument_consumer_t(const span& args): m_args(args) + explicit argument_consumer_t(const span& args): m_begin(args.data()), m_end(args.data() + args.size()) { } [[nodiscard]] argument_string_t peek(const i32 offset = 0) const { - return m_args[m_forward_index + offset]; + return *(m_begin + offset); + } + + [[nodiscard]] argument_string_t r_peek(const i32 offset = 0) const + { + return *(m_end - 1 - offset); } argument_string_t consume() { - return m_args[m_forward_index++]; + return *(m_begin++); } - [[nodiscard]] i32 position() const + argument_string_t r_consume() { - return m_forward_index; + return *(--m_end); } [[nodiscard]] i32 remaining() const { - return static_cast(m_args.size()) - m_forward_index; + return static_cast(size()); } - [[nodiscard]] bool has_next(const i32 offset = 0) const + [[nodiscard]] bool can_consume(const i32 amount = 0) const { - return (offset + m_forward_index) < m_args.size(); + return amount < remaining(); } private: - span m_args; - i32 m_forward_index = 0; + [[nodiscard]] ptrdiff_t size() const + { + return m_end - m_begin; + } + + argument_string_t* m_begin; + argument_string_t* m_end; }; class argument_storage_t @@ -312,6 +316,12 @@ namespace blt::argparse } private: + void add(const argument_storage_t& values) + { + for (const auto value : values) + m_data.insert(value); + } + hashmap_t m_data; }; @@ -322,33 +332,47 @@ namespace blt::argparse public: argument_builder_t() { - dest_func = [](const std::string_view dest, argument_storage_t& storage, std::string_view value) + m_dest_func = [](const std::string_view dest, argument_storage_t& storage, std::string_view value) { - storage.m_data[dest] = value; + storage.m_data.insert({dest, value}); + }; + m_dest_vec_func = [](const std::string_view dest, argument_storage_t& storage, const std::vector& values) + { + storage.m_data.insert({dest, values}); }; } template argument_builder_t& as_type() { - dest_func = [](const std::string_view dest, argument_storage_t& storage, std::string_view value) + m_dest_func = [](const std::string_view dest, argument_storage_t& storage, std::string_view value) { - storage.m_data[dest] = detail::arg_type_t::convert(value); + storage.m_data.insert({dest, detail::arg_string_converter_t::convert(value)}); + }; + m_dest_vec_func = [](const std::string_view dest, argument_storage_t& storage, const std::vector& values) + { + std::vector converted_values; + for (const auto& value : values) + converted_values.push_back(detail::arg_string_converter_t::convert(value)); + storage.m_data.insert({dest, converted_values}); }; return *this; } private: - action_t action = action_t::STORE; - bool required = false; // do we require this argument to be provided as an argument? - nargs_v nargs = 1; // number of arguments to consume - std::optional metavar; // variable name to be used in the help string - std::optional help; // help string to be used in the help string - std::optional> choices; // optional allowed choices for this argument - std::optional default_value; - std::optional const_value; + action_t m_action = action_t::STORE; + bool m_required = false; // do we require this argument to be provided as an argument? + nargs_v m_nargs = 1; // number of arguments to consume + std::optional m_metavar; // variable name to be used in the help string + std::optional m_help; // help string to be used in the help string + std::optional> m_choices; // optional allowed choices for this argument + std::optional m_default_value; + std::optional m_const_value; + std::optional m_dest; // dest, storage, value input - std::function dest_func; + std::function m_dest_func; + // dest, storage, value input + std::function& values)> m_dest_vec_func; }; class argument_parser_t @@ -370,7 +394,7 @@ namespace blt::argparse std::string_view, Aliases>>...>, "Arguments must be of type string_view, convertible to string_view or be string_view constructable"); m_argument_builders.emplace_back(); - m_flag_arguments[arg] = &m_argument_builders.back(); + m_flag_arguments.insert({arg, &m_argument_builders.back()}); ((m_flag_arguments[std::string_view{aliases}] = &m_argument_builders.back()), ...); return m_argument_builders.back(); } @@ -378,16 +402,18 @@ namespace blt::argparse argument_builder_t& add_positional(const std::string_view arg) { m_argument_builders.emplace_back(); - m_positional_arguments[arg] = &m_argument_builders.back(); + m_positional_arguments.insert({arg, &m_argument_builders.back()}); return m_argument_builders.back(); } argument_subparser_t& add_subparser(std::string_view dest); - void parse(argument_consumer_t& consumer); // NOLINT + argument_storage_t parse(argument_consumer_t& consumer); // NOLINT void print_help(); + void print_usage(); + argument_parser_t& set_name(const std::string_view name) { m_name = name; @@ -428,6 +454,11 @@ namespace blt::argparse } private: + void parse_flag(argument_storage_t& parsed_args, argument_consumer_t& consumer, std::string_view arg); + void parse_positional(argument_storage_t& parsed_args, argument_consumer_t& consumer, std::string_view arg); + static void handle_missing_and_default_args(hashmap_t& arguments, const hashset_t& found, + argument_storage_t& parsed_args, std::string_view type); + std::optional m_name; std::optional m_usage; std::optional m_description; @@ -470,7 +501,7 @@ namespace blt::argparse * * @throws detail::subparse_error If the argument is a flag or does not match any known parser. */ - argument_string_t parse(argument_consumer_t& consumer); // NOLINT + std::pair parse(argument_consumer_t& consumer); // NOLINT private: [[nodiscard]] std::vector> get_allowed_strings() const; diff --git a/src/blt/parse/argparse_v2 (conflicted copy 2025-02-13 150255).cpp b/src/blt/parse/argparse_v2 (conflicted copy 2025-02-13 150255).cpp index 6ce898c..1a32833 100644 --- a/src/blt/parse/argparse_v2 (conflicted copy 2025-02-13 150255).cpp +++ b/src/blt/parse/argparse_v2 (conflicted copy 2025-02-13 150255).cpp @@ -155,7 +155,7 @@ namespace blt::argparse void argument_string_t::process_argument() { size_t start = 0; - for (; start < m_argument.size() && allowed_flag_prefix->contains(m_argument[start]); start++) + for (; start < m_argument.size() && m_allowed_flag_prefix->contains(m_argument[start]); start++) { } diff --git a/src/blt/parse/argparse_v2.cpp b/src/blt/parse/argparse_v2.cpp index dd1b45d..6eb054c 100644 --- a/src/blt/parse/argparse_v2.cpp +++ b/src/blt/parse/argparse_v2.cpp @@ -21,12 +21,6 @@ namespace blt::argparse { - namespace detail - { - - } - - namespace detail { // Unit Tests for class argument_string_t @@ -122,7 +116,6 @@ namespace blt::argparse BLT_ASSERT(arg.get_flag() == "++" && "Double plus value should match the input string."); } - void run_all_tests_argument_string_t() { @@ -144,6 +137,32 @@ namespace blt::argparse { run_all_tests_argument_string_t(); } + + [[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 (j < alias.size() - 2) + message += ", "; + else if (j < alias.size()) + message += ", or "; + } + if (allowed_string.size() > 1) + message += ']'; + if (i != m_allowed_strings.size() - 1) + message += ' '; + } + message += "}"; + return message; + } } argument_subparser_t& argument_parser_t::add_subparser(const std::string_view dest) @@ -152,44 +171,162 @@ namespace blt::argparse return m_subparsers.back().second; } - void argument_parser_t::parse(argument_consumer_t& consumer) + argument_storage_t argument_parser_t::parse(argument_consumer_t& consumer) { - if (!consumer.has_next()) + hashset_t found_flags; + hashset_t found_positional; + argument_storage_t parsed_args; + // first, we consume flags which may be part of this parser + while (consumer.can_consume() && consumer.peek().is_flag()) { - if (m_subparsers.size() > 0) + const auto key = consumer.consume(); + const auto flag = m_flag_arguments.find(key.get_argument()); + if (flag == m_flag_arguments.end()) { - std::cout << "" - print_help(); - return; + std::cerr << "Error: Unknown flag: " << key.get_argument() << std::endl; + exit(1); + } + found_flags.insert(key.get_argument()); + parse_flag(parsed_args, consumer, key.get_argument()); + } + try + { + for (auto& [key, subparser] : m_subparsers) + { + auto [parsed_subparser, storage] = subparser.parse(consumer); + storage.m_data.insert({key, detail::arg_data_t{parsed_subparser.get_argument()}}); + parsed_args.add(storage); } } + catch (const detail::missing_argument_error& e) + { + std::cerr << "Error: " << e.what() << std::endl; + print_usage(); + exit(1); + } catch (const detail::subparse_error& e) + { + std::cerr << e.error_string() << std::endl; + exit(1); + } + while (consumer.can_consume()) + { + const auto key = consumer.consume(); + if (key.is_flag()) + { + const auto flag = m_flag_arguments.find(key.get_argument()); + if (flag == m_flag_arguments.end()) + { + std::cerr << "Error: Unknown flag: " << key.get_argument() << std::endl; + exit(1); + } + found_flags.insert(key.get_argument()); + parse_flag(parsed_args, consumer, key.get_argument()); + } + else + { + const auto pos = m_positional_arguments.find(key.get_argument()); + if (pos == m_positional_arguments.end()) + { + std::cerr << "Error: Unknown positional argument: " << key.get_argument() << std::endl; + exit(1); + } + found_positional.insert(key.get_argument()); + parse_positional(parsed_args, consumer, key.get_argument()); + } + } + handle_missing_and_default_args(m_flag_arguments, found_flags, parsed_args, "flag"); + handle_missing_and_default_args(m_positional_arguments, found_positional, parsed_args, "positional"); + + return parsed_args; } void argument_parser_t::print_help() { - } - argument_string_t argument_subparser_t::parse(argument_consumer_t& consumer) + void argument_parser_t::print_usage() { - if (!consumer.has_next()) + } + + 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[arg]; + auto dest = flag->m_dest.value_or(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; + [[fallthrough]] case nargs_t::ALL_AT_LEAST_ONE: + if (!consumer.can_consume()) + std::cout << "Error expected at least one argument to be consumed by '" << arg << '\'' << std::endl; + case nargs_t::ALL: + std::vector args; + while (consumer.can_consume() && !consumer.peek().is_flag()) + args.emplace_back(consumer.consume().get_argument()); + flag->m_dest_vec_func(dest, parsed_args, args); + break; + } + }, + [](const i32 argc) + { + } + }, flag->m_nargs); + } + + void argument_parser_t::parse_positional(argument_storage_t& parsed_args, argument_consumer_t& consumer, const std::string_view arg) + { + } + + 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) + { + std::cerr << "Error: " << type << " argument '" << key << "' was not found but is required by the program" << std::endl; + exit(1); + } + auto dest = value->m_dest.value_or(key); + if (value->m_default_value && !parsed_args.contains(dest)) + parsed_args.m_data.insert({dest, *value->m_default_value}); + } + } + } + + 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(), get_allowed_strings()); const auto it = m_parsers.find(key.get_name()); + argument_parser_t* parser; if (it == m_parsers.end()) { const auto it2 = m_aliases.find(key.get_name()); if (it2 == m_aliases.end()) throw detail::subparse_error(key.get_argument(), get_allowed_strings()); - it2->second->m_name = m_parent->m_name; - it2->second->parse(consumer); - return key; + parser = it2->second; } - it->second.m_name = m_parent->m_name; - it->second.parse(consumer); - return key; + else + parser = &it->second; + parser->m_name = m_parent->m_name; + return {key, parser->parse(consumer)}; } std::vector> argument_subparser_t::get_allowed_strings() const