From 56611d5aeffdf4df51dd8d7a404737d98c3d9b47 Mon Sep 17 00:00:00 2001 From: Brett Laptop Date: Wed, 19 Feb 2025 02:26:31 -0500 Subject: [PATCH] begin testing, flag work. something is really broken though --- CMakeLists.txt | 2 +- include/blt/parse/argparse_v2.h | 151 ++++++++++++++++--- src/blt/parse/argparse_v2.cpp | 250 +++++++++++++++++++++++--------- 3 files changed, 318 insertions(+), 85 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b3bef34..139afa8 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.14) +set(BLT_VERSION 4.0.15) set(BLT_TARGET BLT) diff --git a/include/blt/parse/argparse_v2.h b/include/blt/parse/argparse_v2.h index 64c5544..b11b3d0 100644 --- a/include/blt/parse/argparse_v2.h +++ b/include/blt/parse/argparse_v2.h @@ -19,6 +19,7 @@ #ifndef BLT_PARSE_ARGPARSE_V2_H #define BLT_PARSE_ARGPARSE_V2_H +#include #include #include #include @@ -89,6 +90,30 @@ namespace blt::argparse } }; + class missing_value_error final : public std::runtime_error + { + public: + explicit missing_value_error(const std::string& message): std::runtime_error(message) + { + } + }; + + class type_error final : public std::runtime_error + { + public: + explicit type_error(const std::string& message): std::runtime_error(message) + { + } + }; + + class unexpected_argument_error final : public std::runtime_error + { + public: + explicit unexpected_argument_error(const std::string& message): std::runtime_error(message) + { + } + }; + class subparse_error final : public std::exception { public: @@ -120,13 +145,6 @@ namespace blt::argparse std::vector> m_allowed_strings; }; - template - constexpr auto invalid_option_lambda = [](const T) - { - std::cerr << "Invalid type - expected list type, found '" << blt::type_string() << "'" << std::endl; - std::exit(1); - }; - template struct arg_data_helper_t { @@ -134,6 +152,9 @@ namespace blt::argparse using arg_t = meta::arg_helper; using arg_vec_t = meta::arg_helper...>; + template + constexpr static bool is_type_stored_v = std::disjunction_v...>; + template static auto make_visitor(const DefaultPrimitiveAction& primitive_action, const DefaultListAction& list_action) { @@ -196,14 +217,13 @@ namespace blt::argparse class argument_string_t { public: - explicit argument_string_t(const char* input, const hashset_t& allowed_flag_prefix): m_argument(input), - m_allowed_flag_prefix(&allowed_flag_prefix) + explicit argument_string_t(const std::string_view input, const hashset_t& allowed_flag_prefix): m_argument(input), + m_allowed_flag_prefix(&allowed_flag_prefix) { - if (input == nullptr) - throw detail::bad_flag("Argument cannot be null!"); process_argument(); } + [[nodiscard]] std::string_view get_flag() const { return m_flag_section; @@ -325,14 +345,14 @@ namespace blt::argparse public: template - const T& get(const std::string_view key) + [[nodiscard]] const T& get(const std::string_view key) const { - return std::get(m_data[key]); + return std::get(m_data.at(key)); } - std::string_view get(const std::string_view key) + [[nodiscard]] std::string_view get(const std::string_view key) const { - return std::get(m_data[key]); + return std::get(m_data.at(key)); } bool contains(const std::string_view key) @@ -340,6 +360,11 @@ namespace blt::argparse return m_data.find(key) != m_data.end(); } + [[nodiscard]] size_t size() const + { + return m_data.size(); + } + private: void add(const argument_storage_t& values) { @@ -370,6 +395,7 @@ namespace blt::argparse template argument_builder_t& as_type() { + static_assert(detail::arg_data_helper_t::template is_type_stored_v, "Type is not valid to be stored/converted as an argument"); m_dest_func = [](const std::string_view dest, argument_storage_t& storage, std::string_view value) { storage.m_data.insert({dest, detail::arg_string_converter_t::convert(value)}); @@ -381,9 +407,8 @@ namespace blt::argparse auto& data = storage.m_data[dest]; if (!std::holds_alternative>(data)) { - std::cerr << "Invalid type conversion. Trying to add type " << blt::type_string() << - " but this does not match existing type index '" << data.index() << "'!" << std::endl; - std::exit(1); + throw detail::type_error("Invalid type conversion. Trying to add type " + blt::type_string() + + " but this does not match existing type index '" + std::to_string(data.index()) + "'!"); } std::vector& converted_values = std::get>(data); for (const auto& value : values) @@ -400,6 +425,75 @@ namespace blt::argparse return *this; } + 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; + default: + break; + } + return *this; + } + + argument_builder_t& set_required(const bool required) + { + m_required = required; + return *this; + } + + argument_builder_t& set_nargs(const nargs_v nargs) + { + m_nargs = nargs; + return *this; + } + + argument_builder_t& set_metavar(const std::string& metavar) + { + m_metavar = metavar; + return *this; + } + + argument_builder_t& set_help(const std::string& help) + { + m_help = help; + return *this; + } + + argument_builder_t& set_choices(const std::vector& choices) + { + m_choices = choices; + return *this; + } + + argument_builder_t& set_default(const detail::arg_data_t& default_value) + { + m_default_value = default_value; + return *this; + } + + argument_builder_t& set_const(const detail::arg_data_t& const_value) + { + m_const_value = const_value; + return *this; + } + + argument_builder_t& set_dest(const std::string& dest) + { + m_dest = dest; + return *this; + } + private: action_t m_action = action_t::STORE; bool m_required = false; // do we require this argument to be provided as an argument? @@ -436,7 +530,7 @@ namespace blt::argparse "Arguments must be of type string_view, convertible to string_view or be string_view constructable"); m_argument_builders.emplace_back(); m_flag_arguments.insert({arg, &m_argument_builders.back()}); - ((m_flag_arguments[std::string_view{aliases}] = &m_argument_builders.back()), ...); + (m_flag_arguments.insert({std::string_view{aliases}, &m_argument_builders.back()}), ...); return m_argument_builders.back(); } @@ -451,6 +545,24 @@ namespace blt::argparse argument_storage_t parse(argument_consumer_t& consumer); // NOLINT + argument_storage_t parse(const std::vector& args) + { + std::vector arg_strings; + for (const auto& arg : args) + arg_strings.emplace_back(arg, allowed_flag_prefixes); + argument_consumer_t consumer{arg_strings}; + return parse(consumer); + } + + argument_storage_t parse(const int argc, const char** argv) + { + std::vector arg_strings; + for (int i = 0; i < argc; ++i) + arg_strings.emplace_back(argv[i], allowed_flag_prefixes); + argument_consumer_t consumer{arg_strings}; + return parse(consumer); + } + void print_help(); void print_usage(); @@ -511,6 +623,7 @@ namespace blt::argparse std::vector m_argument_builders; hashmap_t m_flag_arguments; hashmap_t m_positional_arguments; + hashset_t allowed_flag_prefixes = {'-', '+', '/'}; }; class argument_subparser_t diff --git a/src/blt/parse/argparse_v2.cpp b/src/blt/parse/argparse_v2.cpp index 2e82a04..d6b7f8e 100644 --- a/src/blt/parse/argparse_v2.cpp +++ b/src/blt/parse/argparse_v2.cpp @@ -19,9 +19,49 @@ #include #include #include +#include namespace blt::argparse { + 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 + auto ensure_is_string(T&& t) + { + if constexpr (std::is_arithmetic_v>) + return std::to_string(std::forward(t)); + else + return std::forward(t); + } + + template + std::string make_string(Strings&&... strings) + { + std::string out; + out.reserve((get_const_char_size(strings) + ...)); + ((out += ensure_is_string(std::forward(strings))), ...); + return out; + } + namespace detail { // Unit Tests for class argument_string_t @@ -134,9 +174,113 @@ namespace blt::argparse test_argument_string_t_double_plus(prefixes); } + void test_argparse_empty() + { + std::vector const argv; + 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 = {"-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 = {"!d", "-a"}; + try + { + parser.parse(args); + BLT_ASSERT(false && "Parsing should fail with invalid flag prefix '!'"); + } + catch (const bad_flag& _) + { + 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 = {"-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 = {"-x", "10", "!z", "/y", "value"}; + try + { + parser.parse(args); + BLT_ASSERT(false && "Parsing should fail due to invalid flag '!z'"); + } + catch (const bad_flag& _) + { + 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 = {"-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`"); + } + + 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(); + } + void test() { run_all_tests_argument_string_t(); + test_argparse_empty(); + run_argparse_flag_tests(); } [[nodiscard]] std::string subparse_error::error_string() const @@ -183,10 +327,7 @@ namespace blt::argparse const auto key = consumer.consume(); 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); - } + throw detail::bad_flag(make_string("Error: Unknown flag: ", key.get_argument())); found_flags.insert(key.get_argument()); parse_flag(parsed_args, consumer, key.get_argument()); } @@ -255,7 +396,7 @@ namespace blt::argparse 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 flag = m_flag_arguments[arg]; 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) @@ -273,7 +414,8 @@ namespace blt::argparse break; case nargs_t::ALL_AT_LEAST_ONE: if (!consumer.can_consume()) - std::cerr << "Error expected at least one argument to be consumed by '" << arg << '\'' << std::endl; + throw detail::missing_argument_error( + make_string("Error expected at least one argument to be consumed by '", arg, '\'')); [[fallthrough]]; case nargs_t::ALL: std::vector args; @@ -290,68 +432,56 @@ namespace blt::argparse { if (!consumer.can_consume()) { - std::cerr << "Error expected " << argc << " arguments to be consumed by '" << arg << "' but found " << i << - std::endl; - std::exit(1); + throw detail::missing_argument_error( + make_string("Expected ", argc, " arguments to be consumed by '", arg, "' but found ", i)); } if (consumer.peek().is_flag()) { - std::cerr << "Error expected " << argc << " arguments to be consumed by '" << arg << "' but found a flag '" << - consumer.peek().get_argument() << "' instead!" << std::endl; - std::exit(1); + throw detail::unexpected_argument_error(make_string( + "Expected ", argc, " arguments to be consumed by '", arg, "' but found a flag '", + consumer.peek().get_argument(), "' instead!" + )); } args.push_back(consumer.consume().get_argument()); } if (args.size() != static_cast(argc)) { - std::cerr << + 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" - << std::endl; - std::exit(1); + "Please report as an issue on the GitHub"); } + BLT_TRACE("Running action %d on dest %s", static_cast(flag->m_action), dest.c_str()); + switch (flag->m_action) { case action_t::STORE: if (argc == 0) - { - std::cerr << "Error: argument '" << arg << - "' action is store but takes in no arguments. This condition is invalid!" << std::endl; - std::exit(1); - } + 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 - { - std::cerr << "Error: argument '" << arg << "' action is store but takes in more than one argument. " << - "This condition is invalid, did you mean to use action_t::APPEND or action_t::EXTEND?" << std::endl; - std::exit(1); - } + 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) - { - std::cerr << "Error: argument '" << arg << - "' action is append or extend but takes in no arguments. This condition is invalid!" << std::endl; - std::exit(1); - } + 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) - { - std::cerr << "Error: argument '" << arg << "' action is append const but takes in arguments. " - "This condition is invalid!" << std::endl; - std::exit(1); - } + throw detail::unexpected_argument_error( + make_string("Argument '", arg, "'s action is append const but takes in arguments.")); if (flag->m_const_value) { - std::cerr << "Append const chosen as an action but const value not provided for argument '" << arg << '\'' << - std::endl; - std::exit(1); + 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)) { @@ -359,19 +489,17 @@ namespace blt::argparse auto visitor = detail::arg_meta_type_helper_t::make_visitor( [arg](auto& primitive) { - std::cerr << "Invalid type for argument '" << arg << "' expected list type, found '" - << blt::type_string() << "' with value " << primitive << std::endl; - std::exit(1); + 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)) { - std::cerr << "Constant value for argument '" << arg << - "' type doesn't match values already present! Expected to be of type '" << - blt::type_string() << "'!" << std::endl; - std::exit(1); + 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)); }); @@ -388,8 +516,7 @@ namespace blt::argparse }, [](auto&) { - std::cerr << "Append const should not be a list type!" << std::endl; - std::exit(1); + throw detail::type_error("Append const should not be a list type!"); }); std::visit(visitor, *flag->m_const_value); } @@ -397,32 +524,29 @@ namespace blt::argparse case action_t::STORE_CONST: if (argc != 0) { - std::cerr << "Store const flag called with an argument. This condition doesn't make sense." << std::endl; print_usage(); - std::exit(1); + throw detail::unexpected_argument_error("Store const flag called with an argument."); } if (!flag->m_const_value) - { - std::cerr << "Store const flag called with no value. This condition doesn't make sense." << std::endl; - std::exit(1); - } + throw detail::missing_value_error("Store const flag called with no value. "); parsed_args.m_data.insert({dest, *flag->m_const_value}); + break; case action_t::STORE_TRUE: if (argc != 0) { - std::cerr << "Store true flag called with an argument. This condition doesn't make sense." << std::endl; print_usage(); - std::exit(1); + throw detail::unexpected_argument_error("Store true flag called with an argument."); } parsed_args.m_data.insert({dest, true}); + break; case action_t::STORE_FALSE: if (argc != 0) { - std::cerr << "Store false flag called with an argument. This condition doesn't make sense." << std::endl; print_usage(); - std::exit(1); + 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)) { @@ -435,16 +559,12 @@ namespace blt::argparse return primitive + static_cast(args.size()); } else - { - std::cerr << "Error: count called but stored type is " << blt::type_string() << std::endl; - std::exit(1); - } + throw detail::type_error("Error: count called but stored type is " + blt::type_string()); }, [](auto&) -> detail::arg_data_t { - std::cerr << - "List present on count. This condition doesn't make any sense! (How did we get here, please report this!)"; - std::exit(1); + 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]);