Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions include/Arg.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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_;
Expand Down Expand Up @@ -80,14 +82,22 @@ class Arg {
this->is_required_ = is_required;
}

// takes_value_
// is_flag_
[[nodiscard]] inline bool get__is_flag() const {
return this->is_flag_;
}
inline void set__is_flag(const bool& takes_value) {
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_;
Expand All @@ -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;
// }

Expand Down
88 changes: 75 additions & 13 deletions include/Parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,108 @@
#include "Arg.hpp"
#include "utils.hpp"

#include <optional>
#include <string_view>
#include <type_traits>
#include <system_error>
#include <cstdlib>
#include <charconv>
#include <iostream>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

template<typename T>
struct Parse {
static std::optional<T> parse(std::string_view s) {
if constexpr (std::is_integral_v<T>) {
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<T, float>) {
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<T, double>) {
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<T, long double>) {
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<T> specialization defined for this type");
}
}
};

template<>
struct Parse<std::string> {
static std::optional<std::string> parse(std::string_view s) {
return std::string(s);
}
};

template<>
struct Parse<bool> {
static std::optional<bool> parse(std::string_view s) {
auto as_int = Parse<int>::parse(s).value();
return as_int;
}
};

template<typename T>
concept Parseable = requires(std::string_view s) {
{ Parse<T>::parse(s) } -> std::convertible_to<std::optional<T>>;
};

class ClapParser {
public:
void add_arg(const Arg& arg);
void parse(const int& argc, char* argv[]);
void print_help() const;

template <typename T> inline std::optional<T> get_one_as(const std::string& name) {
template <typename T>
requires Parseable<T>
inline std::optional<T> 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<T>::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<Arg> args_;
std::unordered_map<std::string, std::string> 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<Arg*> find_arg(ClapParser& parser, const std::string& name);
std::vector<Arg> get_positional_args() const;
void apply_defaults();

void parse_options(const std::vector<std::string>& args);
void parse_cli_args(const std::vector<std::string>& args);
void check_env();
void parse_positional_args(const std::vector<std::string>& args);
static void parse_value_for_non_flag(Arg* arg, size_t& cli_index, const std::vector<std::string>& args);
void handle_missing_positional(const Arg& arg);
};
7 changes: 6 additions & 1 deletion include/utils.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#pragma once

#include <algorithm>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <sstream>

template <typename T, typename E>
inline T ok_or(std::optional<T> opt, E&& err) {
Expand Down Expand Up @@ -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); });
}
8 changes: 7 additions & 1 deletion src/Arg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_(""),
Expand All @@ -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;
Expand Down Expand Up @@ -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_)
Expand Down
83 changes: 33 additions & 50 deletions src/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
#include "utils.hpp"

#include <cctype>
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <algorithm>
#include <vector>

void ClapParser::parse(const int& argc, char* argv[]) {
const std::string& raw_program_name = argv[0];
Expand All @@ -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_) {
Expand All @@ -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<std::string>& args) {
void ClapParser::parse_cli_args(const std::vector<std::string>& args) {
for (size_t i = 0; i < args.size(); ++i) {
const auto& token = args.at(i);

Expand All @@ -46,24 +47,38 @@ void ClapParser::parse_options(const std::vector<std::string>& 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<std::string>& 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);
Expand All @@ -78,45 +93,17 @@ void ClapParser::check_env() {
}
};

// void ClapParser::parse_positional_args(const std::vector<std::string>& args) {
// std::vector<std::string> 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]";
Expand Down Expand Up @@ -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 << "}";
Expand Down
1 change: 1 addition & 0 deletions tests/test_combo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ int main(int argc, char* argv[]) {
auto actual_val = ok_or_throw_str(p.get_one_as<int>("val"), "test argument: 'val' is missing");
auto actual_boolean = ok_or_throw_str(p.get_one_as<bool>("flag"), "test argument: 'flag' is missing");

std::cerr << p << '\n';
assert(actual_val == expected_val);
assert(actual_boolean == expected_boolean);
return 0;
Expand Down
1 change: 0 additions & 1 deletion tests/test_priority.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#include "Parser.hpp"
#include "Arg.hpp"
#include "utils.hpp"
#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <iostream>
Expand Down