#pragma once /* * Copyright (C) 2024 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 . */ #ifndef BLT_PARSE_ARGPARSE_V2_H #define BLT_PARSE_ARGPARSE_V2_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace blt::argparse { class argument_string_t; class argument_consumer_t; class argument_parser_t; class argument_subparser_t; class argument_builder_t; class argument_storage_t; enum class action_t { STORE, STORE_CONST, STORE_TRUE, STORE_FALSE, APPEND, APPEND_CONST, EXTEND, COUNT, HELP, VERSION }; enum class nargs_t { IF_POSSIBLE, ALL, ALL_AT_LEAST_ONE }; using nargs_v = std::variant; namespace detail { class bad_flag final : public std::runtime_error { public: explicit bad_flag(const std::string& message): std::runtime_error(message) { } explicit bad_flag(const char* message): std::runtime_error(message) { } }; 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), m_allowed_strings(std::move(allowed_strings)) { } [[nodiscard]] const std::vector>& get_allowed_strings() const { return m_allowed_strings; } [[nodiscard]] std::string_view get_found_string() const { return m_found_string; } [[nodiscard]] std::string error_string() const; [[nodiscard]] const char* what() const noexcept override { return "Please use error_string() method instead of what(). This exception should *always* be caught!"; } private: std::string_view m_found_string; 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 { using variant_t = std::variant...>; using arg_t = meta::arg_helper; using arg_vec_t = meta::arg_helper...>; template static auto make_visitor(const DefaultPrimitiveAction& primitive_action, const DefaultListAction& list_action) { return lambda_visitor{ ([&primitive_action](Args& arg) { return primitive_action(arg); })..., ([&list_action](std::vector& arg_vec) { return list_action(arg_vec); })... }; } }; using arg_meta_type_helper_t = arg_data_helper_t; using arg_data_t = arg_meta_type_helper_t::variant_t; template struct arg_string_converter_t { static T convert(const std::string_view value) { 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) { return std::stof(temp); } else if constexpr (std::is_same_v) { return std::stod(temp); } else if constexpr (std::is_unsigned_v) { return static_cast(std::stoull(temp)); } else if constexpr (std::is_signed_v) { 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; } }; void test(); } 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) { if (input == nullptr) throw detail::bad_flag("Argument cannot be null!"); process_argument(); } [[nodiscard]] std::string_view get_flag() const { return m_flag_section; } [[nodiscard]] std::string_view get_name() const { return m_name_section; } [[nodiscard]] std::string_view value() const { return get_name(); } [[nodiscard]] bool is_flag() const { return !m_flag_section.empty(); } [[nodiscard]] std::string_view get_argument() const { return m_argument; } 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() && m_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}; } std::string_view m_argument; std::string_view m_flag_section; std::string_view m_name_section; const hashset_t* m_allowed_flag_prefix; }; class argument_consumer_t { public: 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_begin + offset); } [[nodiscard]] argument_string_t r_peek(const i32 offset = 0) const { return *(m_end - 1 - offset); } argument_string_t consume() { return *(m_begin++); } argument_string_t r_consume() { return *(--m_end); } [[nodiscard]] i32 remaining() const { return static_cast(size()); } [[nodiscard]] bool can_consume(const i32 amount = 0) const { return amount < remaining(); } private: [[nodiscard]] ptrdiff_t size() const { return m_end - m_begin; } argument_string_t* m_begin; argument_string_t* m_end; }; 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: void add(const argument_storage_t& values) { for (const auto& value : values.m_data) m_data.insert(value); } hashmap_t m_data; }; class argument_builder_t { friend argument_parser_t; public: argument_builder_t() { m_dest_func = [](const std::string_view dest, argument_storage_t& storage, std::string_view 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() { 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)}); }; 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 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 m_dest_func; // dest, storage, value input std::function& values)> m_dest_vec_func; }; class argument_parser_t { friend argument_subparser_t; public: explicit argument_parser_t(const std::optional name = {}, const std::optional usage = {}, const std::optional description = {}, const std::optional epilogue = {}): m_name(name), m_usage(usage), m_description(description), m_epilogue(epilogue) { } 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.insert({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.insert({arg, &m_argument_builders.back()}); return m_argument_builders.back(); } argument_subparser_t& add_subparser(std::string_view dest); argument_storage_t parse(argument_consumer_t& consumer); // NOLINT void print_help(); void print_usage(); void print_version(); argument_parser_t& set_name(const std::string_view name) { m_name = name; return *this; } 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: 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; 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) { } template argument_parser_t& add_parser(const std::string_view name, 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_parsers.emplace(name); ((m_aliases[std::string_view{aliases}] = &m_parsers[name]), ...); return m_parsers[name]; } /** * Parses the next argument using the provided argument consumer. * * This function uses an argument consumer to extract and process the next argument. * If the argument is a flag or if it cannot be matched against the available parsers, * an exception is thrown. * * @param consumer Reference to an argument_consumer_t object, which handles argument parsing. * The consumer provides the next argument to be parsed. * * @throws detail::subparse_error If the argument is a flag or does not match any known parser. */ std::pair parse(argument_consumer_t& consumer); // NOLINT private: [[nodiscard]] std::vector> get_allowed_strings() const; const argument_parser_t* m_parent; hashmap_t m_parsers; hashmap_t m_aliases; }; } #endif //BLT_PARSE_ARGPARSE_V2_H