diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b59e5c..cfb847b 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.6) +set(BLT_VERSION 4.0.7) set(BLT_TARGET BLT) diff --git a/include/blt/parse/argparse_v2.h b/include/blt/parse/argparse_v2.h index 5c227b6..e72fc0f 100644 --- a/include/blt/parse/argparse_v2.h +++ b/include/blt/parse/argparse_v2.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -39,8 +40,32 @@ namespace blt::argparse class argument_consumer_t; class argument_parser_t; class argument_subparser_t; - class parsed_argset_t; class argument_builder_t; + class argument_storage_t; + + enum class action_t + { + STORE, + STORE_CONST, + STORE_TRUE, + STORE_FALSE, + APPEND, + APPEND_CONST, + COUNT, + HELP, + VERSION, + EXTEND, + SUBCOMMAND + }; + + enum class nargs_t + { + IF_POSSIBLE, + ALL, + ALL_AT_LEAST_ONE + }; + + using nargs_v = std::variant; namespace detail { @@ -56,15 +81,24 @@ namespace blt::argparse } }; + class missing_argument_error final : public std::runtime_error + { + public: + explicit missing_argument_error(const std::string& message): std::runtime_error(message) + { + } + } + class subparse_error final : public std::exception { public: - explicit subparse_error(const std::string_view found_string, std::vector allowed_strings): m_found_string(found_string), + explicit subparse_error(const std::string_view found_string, std::vector> allowed_strings): + m_found_string(found_string), m_allowed_strings(std::move(allowed_strings)) { } - [[nodiscard]] const std::vector& get_allowed_strings() const + [[nodiscard]] const std::vector>& get_allowed_strings() const { return m_allowed_strings; } @@ -81,7 +115,18 @@ namespace blt::argparse message += " is not a valid command. Allowed commands are: {"; for (const auto [i, allowed_string] : enumerate(m_allowed_strings)) { - message += allowed_string; + 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 += ' '; } @@ -96,7 +141,7 @@ namespace blt::argparse private: std::string_view m_found_string; - std::vector m_allowed_strings; + std::vector> m_allowed_strings; }; template @@ -106,7 +151,7 @@ namespace blt::argparse using arg_list_data_t = std::variant...>; }; - using data_helper_t = arg_data_helper_t; + 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; @@ -117,7 +162,8 @@ namespace blt::argparse { static T convert(const std::string_view value) { - static_assert(std::is_arithmetic_v, "Type must be arithmetic!"); + static_assert(std::is_arithmetic_v || std::is_same_v || std::is_same_v, + "Type must be arithmetic, string_view or string!"); const std::string temp{value}; if constexpr (std::is_same_v) @@ -136,6 +182,14 @@ namespace blt::argparse { return static_cast(std::stoll(temp)); } + else if constexpr (std::is_same_v) + { + return value; + } + else if constexpr (std::is_same_v) + { + return std::string(value); + } BLT_UNREACHABLE; } }; @@ -234,18 +288,67 @@ namespace blt::argparse i32 m_forward_index = 0; }; - class argument_builder_t + class argument_storage_t { + friend argument_parser_t; + friend argument_subparser_t; + friend argument_builder_t; + public: + template + const T& get(const std::string_view key) + { + return std::get(m_data[key]); + } + + std::string_view get(const std::string_view key) + { + return std::get(m_data[key]); + } + + bool contains(const std::string_view key) + { + return m_data.find(key) != m_data.end(); + } private: + hashmap_t m_data; }; - class parsed_argset_t + class argument_builder_t { + friend argument_parser_t; + public: + argument_builder_t() + { + dest_func = [](const std::string_view dest, argument_storage_t& storage, std::string_view value) + { + storage.m_data[dest] = value; + }; + } + + template + argument_builder_t& as_type() + { + 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); + }; + 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; + // dest, storage, value input + std::function dest_func; }; class argument_parser_t @@ -259,14 +362,69 @@ namespace blt::argparse { } + template + argument_builder_t& add_flag(const std::string_view arg, Aliases... aliases) + { + static_assert( + std::conjunction_v, std::is_constructible< + 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[std::string_view{aliases}] = &m_argument_builders.back()), ...); + return m_argument_builders.back(); + } + + argument_builder_t& add_positional(const std::string_view arg) + { + m_argument_builders.emplace_back(); + m_positional_arguments[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 + + void print_help(); + argument_parser_t& set_name(const std::string_view name) { m_name = name; return *this; } - void parse(argument_consumer_t& consumer) // NOLINT + argument_parser_t& set_usage(const std::string_view usage) { + m_usage = usage; + return *this; + } + + [[nodiscard]] const std::optional& get_usage() const + { + return m_usage; + } + + argument_parser_t& set_description(const std::string_view description) + { + m_description = description; + return *this; + } + + [[nodiscard]] const std::optional& get_description() const + { + return m_description; + } + + argument_parser_t& set_epilogue(const std::string_view epilogue) + { + m_epilogue = epilogue; + return *this; + } + + [[nodiscard]] const std::optional& get_epilogue() const + { + return m_epilogue; } private: @@ -274,26 +432,28 @@ namespace blt::argparse std::optional m_usage; std::optional m_description; std::optional m_epilogue; + std::vector> m_subparsers; + std::vector m_argument_builders; + hashmap_t m_flag_arguments; + hashmap_t m_positional_arguments; }; class argument_subparser_t { public: - explicit argument_subparser_t(const argument_parser_t& parent): m_parent(&parent), m_usage(parent.m_usage), - m_description(parent.m_description), m_epilogue(parent.m_epilogue) + explicit argument_subparser_t(const argument_parser_t& parent): m_parent(&parent) { } - explicit argument_subparser_t(const argument_parser_t& parent, const std::optional usage = {}, - const std::optional description = {}, - const std::optional epilogue = {}): m_parent(&parent), m_usage(usage), - m_description(description), m_epilogue(epilogue) + template + argument_parser_t& add_parser(const std::string_view name, Aliases... aliases) { - } - - argument_parser_t& add_parser(const std::string_view name) - { - m_parsers.emplace(name, {}, m_usage, m_description, m_epilogue); + static_assert( + std::conjunction_v, std::is_constructible< + std::string_view, Aliases>>...>, + "Arguments must be of type string_view, convertible to string_view or be string_view constructable"); + m_parsers.emplace(name); + ((m_aliases[std::string_view{aliases}] = &m_parsers[name]), ...); return m_parsers[name]; } @@ -310,32 +470,14 @@ namespace blt::argparse * * @throws detail::subparse_error If the argument is a flag or does not match any known parser. */ - void parse(argument_consumer_t& consumer) // NOLINT - { - 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()); - if (it == m_parsers.end()) - throw detail::subparse_error(key.get_argument(), get_allowed_strings()); - it->second.m_name = m_parent->m_name; - it->second.parse(consumer); - } + argument_string_t parse(argument_consumer_t& consumer); // NOLINT private: - [[nodiscard]] std::vector get_allowed_strings() const - { - std::vector vec; - for (const auto& [key, value] : m_parsers) - vec.push_back(key); - return vec; - } + [[nodiscard]] std::vector> get_allowed_strings() const; const argument_parser_t* m_parent; - std::optional m_usage; - std::optional m_description; - std::optional m_epilogue; hashmap_t m_parsers; + hashmap_t m_aliases; }; } 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 new file mode 100644 index 0000000..6ce898c --- /dev/null +++ b/src/blt/parse/argparse_v2 (conflicted copy 2025-02-13 150255).cpp @@ -0,0 +1,165 @@ +/* + * + * Copyright (C) 2025 Brett Terpstra + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include + +namespace blt::argparse +{ + 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); + } + + 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)) + { + message += allowed_string; + if (i != m_allowed_strings.size() - 1) + message += ' '; + } + message += "}"; + return message; + } + + void test() + { + run_all_tests_argument_string_t(); + } + } + + void argument_string_t::process_argument() + { + size_t start = 0; + for (; start < m_argument.size() && allowed_flag_prefix->contains(m_argument[start]); start++) + { + } + + m_flag_section = {m_argument.data(), start}; + m_name_section = {m_argument.data() + start, m_argument.size() - start}; + } +} diff --git a/src/blt/parse/argparse_v2.cpp b/src/blt/parse/argparse_v2.cpp index 02cb76b..dd1b45d 100644 --- a/src/blt/parse/argparse_v2.cpp +++ b/src/blt/parse/argparse_v2.cpp @@ -15,6 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#include #include #include @@ -121,6 +122,8 @@ namespace blt::argparse BLT_ASSERT(arg.get_flag() == "++" && "Double plus value should match the input string."); } + + void run_all_tests_argument_string_t() { const hashset_t prefixes = {'-', '+'}; @@ -142,4 +145,67 @@ namespace blt::argparse run_all_tests_argument_string_t(); } } + + 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; + } + + void argument_parser_t::parse(argument_consumer_t& consumer) + { + if (!consumer.has_next()) + { + if (m_subparsers.size() > 0) + { + std::cout << "" + print_help(); + return; + } + } + } + + void argument_parser_t::print_help() + { + + } + + argument_string_t argument_subparser_t::parse(argument_consumer_t& consumer) + { + if (!consumer.has_next()) + 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()); + 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; + } + it->second.m_name = m_parent->m_name; + it->second.parse(consumer); + return key; + } + + std::vector> argument_subparser_t::get_allowed_strings() const + { + std::vector> vec; + for (const auto& [key, value] : m_parsers) + { + std::vector aliases; + aliases.push_back(key); + for (const auto& [alias, parser] : m_aliases) + { + if (parser == &value) + aliases.push_back(alias); + } + vec.emplace_back(std::move(aliases)); + } + return vec; + } }