diff --git a/include/Arg.hpp b/include/Arg.hpp index 4809ff1..96c0cba 100644 --- a/include/Arg.hpp +++ b/include/Arg.hpp @@ -17,6 +17,7 @@ class Arg { Arg& help(const std::string& help); Arg& required(bool is_required); Arg& is_flag(); + Arg& accepts_many(); Arg& default_value(const std::string& default_val); Arg& from_env(const char* env_var_name); Arg& auto_env(); @@ -33,6 +34,7 @@ class Arg { std::string help_; bool is_required_; bool is_flag_; + bool accepts_many_; std::string env_name_; bool auto_env_; // std::string auto_env_name_; @@ -80,7 +82,7 @@ class Arg { this->is_required_ = is_required; } - // takes_value_ + // is_flag_ [[nodiscard]] inline bool get__is_flag() const { return this->is_flag_; } @@ -88,6 +90,14 @@ class Arg { this->is_flag_ = takes_value; } + // accepts_many_ + [[nodiscard]] inline bool get__accepts_many() const { + return this->accepts_many_; + } + inline void set__accepts_many(const bool& accepts_many) { + this->accepts_many_ = accepts_many; + } + // env_name_ [[nodiscard]] inline const std::string& get__env_name() const { return this->env_name_; @@ -107,7 +117,7 @@ class Arg { // auto_env_name_ // [[nodiscard]] inline const std::string get__auto_env_name() const { // std::string env_name = PROGRAM_NAME() + '_' + this->get__name(); - // std::transform(env_name.begin(), env_name.end(), env_name.begin(), [](const unsigned char& c) { return std::toupper(c); }); + // to_upper(env_name); // return env_name; // } diff --git a/include/Parser.hpp b/include/Parser.hpp index fc8d483..dd7b0c7 100644 --- a/include/Parser.hpp +++ b/include/Parser.hpp @@ -3,46 +3,108 @@ #include "Arg.hpp" #include "utils.hpp" +#include +#include +#include +#include +#include +#include #include #include #include -#include #include +template +struct Parse { + static std::optional parse(std::string_view s) { + if constexpr (std::is_integral_v) { + T value; + auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value); + if (ec == std::errc()) return value; + return std::nullopt; + } + else if constexpr (std::is_same_v) { + char* end = nullptr; + float value = std::strtof(s.data(), &end); + if (end == s.data() + s.size()) return value; + return std::nullopt; + } + else if constexpr (std::is_same_v) { + char* end = nullptr; + double value = std::strtod(s.data(), &end); + if (end == s.data() + s.size()) return value; + return std::nullopt; + } + else if constexpr (std::is_same_v) { + char* end = nullptr; + long double value = std::strtold(s.data(), &end); + if (end == s.data() + s.size()) return value; + return std::nullopt; + } + else { + static_assert(sizeof(T) == 0, "No Parse specialization defined for this type"); + } + } +}; + +template<> +struct Parse { + static std::optional parse(std::string_view s) { + return std::string(s); + } +}; + +template<> +struct Parse { + static std::optional parse(std::string_view s) { + auto as_int = Parse::parse(s).value(); + return as_int; + } +}; + +template +concept Parseable = requires(std::string_view s) { + { Parse::parse(s) } -> std::convertible_to>; +}; + class ClapParser { public: void add_arg(const Arg& arg); void parse(const int& argc, char* argv[]); void print_help() const; - template inline std::optional get_one_as(const std::string& name) { + template + requires Parseable + inline std::optional get_one_as(const std::string& name) { Arg* arg = ok_or(ClapParser::find_arg(*this, "--" + name), []{ return std::nullopt; }); - if (auto arg_value = arg->get__value(); arg_value) { - T value; - std::istringstream(*arg_value) >> value; - return value; - } - return std::nullopt; + // if (auto arg_value = arg->get__value(); arg_value) { + // T value; + // std::istringstream(*arg_value) >> value; + // return value; + // } + // return std::nullopt; + + return Parse::parse(arg->get__value().value()); } static void print_parser(std::ostream& os, const ClapParser& parser, int indent); friend std::ostream& operator<<(std::ostream& os, const ClapParser& parser); private: std::vector args_; - std::unordered_map values_; std::string program_name_; // Helper methods - inline bool is_option(const std::string& token) const ; - inline bool is_long_option(const std::string& token) const ; - inline bool is_short_option(const std::string& token) const ; + static bool is_option(const std::string& token); + static bool is_long_option(const std::string& token); + static bool is_short_option(const std::string& token); static std::optional find_arg(ClapParser& parser, const std::string& name); std::vector get_positional_args() const; void apply_defaults(); - void parse_options(const std::vector& args); + void parse_cli_args(const std::vector& args); void check_env(); void parse_positional_args(const std::vector& args); + static void parse_value_for_non_flag(Arg* arg, size_t& cli_index, const std::vector& args); void handle_missing_positional(const Arg& arg); }; diff --git a/include/utils.hpp b/include/utils.hpp index cfd0dec..387a618 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -1,9 +1,10 @@ #pragma once +#include #include +#include #include #include -#include template inline T ok_or(std::optional opt, E&& err) { @@ -44,3 +45,7 @@ inline void print_indent(std::ostream& os, int indent_level) { for (int i = 0; i < indent_level; ++i) os << '\t'; } + +inline void to_upper(std::string &s) { + std::ranges::transform(s, s.begin(), [](const unsigned char& c) { return std::toupper(c); }); +} diff --git a/src/Arg.cpp b/src/Arg.cpp index b7766e9..ac18a58 100644 --- a/src/Arg.cpp +++ b/src/Arg.cpp @@ -12,6 +12,7 @@ Arg::Arg(const std::string& name) : help_(""), is_required_(false), is_flag_(false), + accepts_many_(false), env_name_(""), auto_env_(false), default_value_(""), @@ -31,11 +32,15 @@ Arg& Arg::required(bool is_required) { is_required_ = is_required; return *this; } -Arg& Arg::is_flag() { +Arg& Arg::is_flag() { is_flag_ = true; default_value_ = "0"; return *this; } +Arg& Arg::accepts_many() { + accepts_many_ = true; + return *this; +} Arg& Arg::default_value(const std::string& default_value) { default_value_ = default_value; return *this; @@ -69,6 +74,7 @@ void Arg::print_arg(std::ostream& os, const Arg& arg, int indent) { print_indent(os, indent + 1); os << "help: \"" << arg.help_ << "\",\n"; print_indent(os, indent + 1); os << "required: " << std::boolalpha << arg.is_required_ << ",\n"; print_indent(os, indent + 1); os << "is_flag: " << std::boolalpha << arg.is_flag_ << ",\n"; + print_indent(os, indent + 1); os << "accepts_many: " << std::boolalpha << arg.accepts_many_ << ",\n"; print_indent(os, indent + 1); os << "default: \"" << arg.default_value_ << "\",\n"; print_indent(os, indent + 1); os << "value: "; if (arg.value_) diff --git a/src/Parser.cpp b/src/Parser.cpp index dcdfc44..a4cc87f 100644 --- a/src/Parser.cpp +++ b/src/Parser.cpp @@ -3,11 +3,13 @@ #include "utils.hpp" #include +#include #include #include #include #include #include +#include void ClapParser::parse(const int& argc, char* argv[]) { const std::string& raw_program_name = argv[0]; @@ -22,8 +24,7 @@ void ClapParser::parse(const int& argc, char* argv[]) { this->apply_defaults(); this->check_env(); - this->parse_options(args); // parse from cli (argc, argv) - // parse_positional_args(args); + this->parse_cli_args(args); // parse from cli (argc, argv) // Validate all arguments that need values received them for (const auto& arg : args_) { @@ -36,7 +37,7 @@ void ClapParser::parse(const int& argc, char* argv[]) { void ClapParser::add_arg(const Arg& arg) { args_.emplace_back(arg); } -void ClapParser::parse_options(const std::vector& args) { +void ClapParser::parse_cli_args(const std::vector& args) { for (size_t i = 0; i < args.size(); ++i) { const auto& token = args.at(i); @@ -46,24 +47,38 @@ void ClapParser::parse_options(const std::vector& args) { } auto arg = ok_or_throw_str(ClapParser::find_arg(*this, token), "unknown option: \'" + token); + if (!arg->get__is_flag()) { - if (i + 1 < args.size() && !is_option(args[i + 1])) { - arg->set__value(args.at(i + 1)); - i++; // Skip the value in the next iteration - } else { - throw std::runtime_error("option '" + token + "' requires a value but none was provided"); - } + ClapParser::parse_value_for_non_flag(arg, i, args); } else { arg->set__value("1"); } } } +void ClapParser::parse_value_for_non_flag(Arg* arg, size_t& cli_index, const std::vector& args) { + if (cli_index + 1 < args.size() && !is_option(args.at(cli_index + 1))) { + if (arg->get__accepts_many()) { + std::string value; + while (cli_index + 1 < args.size() && !is_option(args.at(cli_index + 1))) { + value += args.at(cli_index + 1) + ' '; + cli_index++; + } + arg->set__value(value); + } else { + arg->set__value(args.at(cli_index + 1)); + cli_index++; // Skip the value in the next iteration + } + } else { + throw std::runtime_error("option '" + arg->get__name() + "' requires a value but none was provided"); + } +} + void ClapParser::check_env() { for (auto& arg : args_) { if (arg.get__auto_env()) { std::string env_name = this->program_name_ + '_' + arg.get__name(); - std::transform(env_name.begin(), env_name.end(), env_name.begin(), [](const unsigned char& c) { return std::toupper(c); }); + to_upper(env_name); auto value_from_env = std::getenv(env_name.c_str()); if (value_from_env) { arg.set__value(value_from_env); @@ -78,45 +93,17 @@ void ClapParser::check_env() { } }; -// void ClapParser::parse_positional_args(const std::vector& args) { -// std::vector positional_args; - -// // Collect positional arguments (tokens not starting with '-') -// for (const auto& token : args) { -// if (!is_option(token)) { -// positional_args.push_back(token); -// } -// } - -// // Assign positional arguments to their respective slots -// auto positional_specs = get_positional_args(); -// for (size_t j = 0; j < positional_specs.size(); ++j) { -// if (j < positional_args.size()) { -// values_[positional_specs[j].name()] = positional_specs[j].convert(positional_args[j]); -// } else { -// handle_missing_positional(positional_specs[j]); -// } -// } -// } - -void ClapParser::handle_missing_positional(const Arg& arg) { - if (arg.get__is_required()) { - throw std::runtime_error("missing required positional argument: " + arg.get__name()); - } - if (arg.has_default()) { - values_[arg.get__name()] = std::string(arg.get__default_value()); - } +bool ClapParser::is_option(const std::string& token) { + return token.substr(0, 2) == "--" || (token[0] == '-' && token.size() > 1); } - - inline bool ClapParser::is_option(const std::string& token) const { - return token.substr(0, 2) == "--" || (token[0] == '-' && token.size() > 1); - } - inline bool ClapParser::is_long_option(const std::string& token) const { return token.substr(0, 2) == "--"; } +bool ClapParser::is_long_option(const std::string& token) { + return token.substr(0, 2) == "--"; +} - inline bool ClapParser::is_short_option(const std::string& token) const { - return token[0] == '-' && token.size() > 1 && token[1] != '-'; - } +bool ClapParser::is_short_option(const std::string& token) { + return token[0] == '-' && token.size() > 1 && token[1] != '-'; +} void ClapParser::print_help() const { std::cout << "Usage: " << this->program_name_ << " [OPTIONS]"; @@ -202,10 +189,6 @@ void ClapParser::print_parser(std::ostream& os, const ClapParser& parser, int in } print_indent(os, indent + 1); os << "],\n"; - print_indent(os, indent + 1); os << "values: {\n"; - for (const auto& [key, val] : parser.values_) { - print_indent(os, indent + 2); os << "\"" << key << "\": \"" << val << "\",\n"; - } print_indent(os, indent + 1); os << "}\n"; print_indent(os, indent); os << "}"; diff --git a/tests/test_combo.cpp b/tests/test_combo.cpp index b07170d..9aa7ab0 100644 --- a/tests/test_combo.cpp +++ b/tests/test_combo.cpp @@ -27,6 +27,7 @@ int main(int argc, char* argv[]) { auto actual_val = ok_or_throw_str(p.get_one_as("val"), "test argument: 'val' is missing"); auto actual_boolean = ok_or_throw_str(p.get_one_as("flag"), "test argument: 'flag' is missing"); + std::cerr << p << '\n'; assert(actual_val == expected_val); assert(actual_boolean == expected_boolean); return 0; diff --git a/tests/test_priority.cpp b/tests/test_priority.cpp index 49d3c89..722a4a6 100644 --- a/tests/test_priority.cpp +++ b/tests/test_priority.cpp @@ -2,7 +2,6 @@ #include "Parser.hpp" #include "Arg.hpp" #include "utils.hpp" -#include #include #include #include