diff --git a/.github/workflows/meson-test.yml b/.github/workflows/meson-test.yml new file mode 100644 index 0000000..a1943fc --- /dev/null +++ b/.github/workflows/meson-test.yml @@ -0,0 +1,41 @@ +name: Meson Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Meson tests (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Meson and Ninja + run: | + python -m pip install --upgrade pip + pip install meson ninja + + - name: Setup build directory + run: meson setup build + + - name: Build + run: meson compile -C build + + - name: Run tests + run: meson test -C build --print-errorlogs + + - name: Print test logs + run: cat build/meson-logs/testlog.txt || echo "No meson logs found" diff --git a/include/Arg.hpp b/include/Arg.hpp index f988cfb..4809ff1 100644 --- a/include/Arg.hpp +++ b/include/Arg.hpp @@ -1,55 +1,146 @@ #pragma once + +#include "utils.hpp" + +#include #include #include -#include -#include #include #include -class ClapParser; - class Arg { public: - explicit Arg(std::string name); + Arg(const std::string& name); - Arg& short_name(const std::string& s); - Arg& long_name(const std::string& l); + Arg& short_name(const std::string& short_name); + Arg& long_name(const std::string& long_name); Arg& help(const std::string& help); Arg& required(bool is_required); - Arg& takes_value(bool takes); + Arg& is_flag(); Arg& default_value(const std::string& default_val); Arg& from_env(const char* env_var_name); - Arg& try_env(); - void set_try_env_name(const std::string& s); - - // Getters - [[nodiscard]] const std::string& short_name() const; - [[nodiscard]] const std::string& long_name() const; - [[nodiscard]] const std::string& help() const; - [[nodiscard]] const std::string& default_value() const; - [[nodiscard]] bool is_required() const; - [[nodiscard]] bool requires_value() const; + Arg& auto_env(); + static void print_arg(std::ostream& os, const Arg& arg, int indent); friend std::ostream& operator<<(std::ostream& os, const Arg& arg); private: friend class ClapParser; std::string name_; - std::string short_; - std::string long_; + std::string short_name_; + std::string long_name_; std::string help_; - bool required_; - bool takes_value_; - bool has_env_; + bool is_required_; + bool is_flag_; std::string env_name_; - bool try_env_; - std::string try_env_name_; - std::string default_; + bool auto_env_; + // std::string auto_env_name_; + std::string default_value_; std::optional value_; - [[nodiscard]] bool has_default() const; - [[nodiscard]] bool has_env() const; - [[nodiscard]] bool takes_value() const; - [[nodiscard]] const std::string& name() const; + // ----| Getters & Setters |---- + // name_ + [[nodiscard]] inline const std::string& get__name() const { + return this->name_; + } + inline void set__name(const std::string& name) { + this->name_ = name; + } + + // short_ + [[nodiscard]] inline const std::string& get__short_name() const { + return this->short_name_; + } + inline void set__short_name(const std::string& short_name) { + this->short_name_ = short_name; + } + + // long_ + [[nodiscard]] inline const std::string& get__long_name() const { + return this->long_name_; + } + inline void set__long_name(const std::string& long_name) { + this->long_name_ = long_name; + } + + // help_ + [[nodiscard]] inline const std::string& get__help() const { + return this->help_; + } + inline void set__help(const std::string& help) { + this->help_ = help; + } + + // required_ + [[nodiscard]] inline bool get__is_required() const { + return this->is_required_; + } + inline void set__is_required(const bool& is_required) { + this->is_required_ = is_required; + } + + // takes_value_ + [[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; + } + + // env_name_ + [[nodiscard]] inline const std::string& get__env_name() const { + return this->env_name_; + } + inline void set__env_name(const std::string& env_name) { + this->env_name_ = env_name; + } + + // auto_env_ + [[nodiscard]] inline bool get__auto_env() const { + return this->auto_env_; + } + inline void set__auto_env(const bool& auto_env) { + this->auto_env_ = auto_env; + } + + // 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); }); + // return env_name; + // } + + // default_ + [[nodiscard]] inline const std::string& get__default_value() const { + return this->default_value_; + } + inline void set__default_value(const std::string& default_value) { + this->default_value_ = default_value; + } + + // value_ + [[nodiscard]] inline const std::optional get__value() const { + return this->value_; + } + inline void set__value(const std::string& value) { + this->value_ = value; + } + + // ----| Checkers |---- + // has_env_ + [[nodiscard]] inline bool has_env() const { + return !this->env_name_.empty(); + } + + // has_default_ + [[nodiscard]] inline bool has_default() const { + return !this->default_value_.empty(); + } + + // has_value_ + [[nodiscard]] inline bool has_value() const { + return this->value_.has_value(); + } + }; diff --git a/include/Parser.hpp b/include/Parser.hpp index fec0746..fc8d483 100644 --- a/include/Parser.hpp +++ b/include/Parser.hpp @@ -1,9 +1,10 @@ #pragma once + #include "Arg.hpp" +#include "utils.hpp" + #include #include -#include -#include #include #include #include @@ -11,12 +12,21 @@ class ClapParser { public: void add_arg(const Arg& arg); - void parse(int argc, char* argv[]); + void parse(const int& argc, char* argv[]); void print_help() const; - template std::optional get_one_as(const std::string& name) const; - bool has(const std::string& name) const; + template 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; + } + + 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_; @@ -27,31 +37,12 @@ class ClapParser { 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 ; - const Arg* find_option(const std::string& name) const; + 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_positional_args(const std::vector& args); - void check_required_args(); void check_env(); + void parse_positional_args(const std::vector& args); void handle_missing_positional(const Arg& arg); - - size_t handle_long_option(const std::string& token, const std::vector& args, size_t i); - size_t handle_short_option(const std::string& token, const std::vector& args, size_t i); - size_t handle_option_with_value(const Arg* arg, const std::vector& args, size_t i, - const std::string& token); - }; - -template std::optional ClapParser::get_one_as(const std::string& name) const { - auto it = values_.find(name); - if (it == values_.end()) { - return std::nullopt; - } - - T value; - std::istringstream(it->second) >> value; - return value; - // return std::any_cast(it->second); -} diff --git a/src/utils.cpp b/include/utils.hpp similarity index 55% rename from src/utils.cpp rename to include/utils.hpp index ab9cf8b..cfd0dec 100644 --- a/src/utils.cpp +++ b/include/utils.hpp @@ -1,35 +1,46 @@ +#pragma once + #include #include #include #include template -T ok_or(std::optional opt, E&& err) { +inline T ok_or(std::optional opt, E&& err) { if (!opt) std::forward(err)(); return *opt; } template -T ok_or_throw_str(std::optional opt, const std::string& err) { +inline T ok_or_throw_str(std::optional opt, const std::string& err) { if (!opt) throw std::runtime_error(err); return *opt; } template -T ptr_ok_or_throw_str(T pointer, const std::string& err) { +inline T ptr_ok_or_throw_str(T pointer, const std::string& err) { if (!pointer) throw std::runtime_error(err); return pointer; } template -E ptr_unwrap_or(P pointer, const E other) { +inline E ptr_unwrap_or(P pointer, const E other) { if (!pointer) return other; } // variadic template function to concatenate any number of arguments -template std::string concat(Args&&... args) { +template inline std::string concat(Args&&... args) { std::ostringstream oss; (void)std::initializer_list{ (oss << std::forward(args), 0)...}; // using initializer_list for fold-like behavior return oss.str(); } + +inline const std::string quote(const std::string& name) { + return '\'' + name + '\''; +} + +inline void print_indent(std::ostream& os, int indent_level) { + for (int i = 0; i < indent_level; ++i) + os << '\t'; +} diff --git a/meson.build b/meson.build index ec4fc4e..ee36240 100644 --- a/meson.build +++ b/meson.build @@ -1,41 +1,87 @@ project('claplusplus', 'cpp', - version : '0.0.3', - default_options : [ + default_options: [ 'cpp_std=c++23', 'warning_level=3', - 'werror=true', + # 'werror=true', 'optimization=g', 'debug=true', 'b_ndebug=if-release' ] ) -# Include directory +# Add include directory for headers include_dir = include_directories('include') -# Library -source_dir = files( - 'src/Arg.cpp', - 'src/Parser.cpp', - 'src/utils.cpp' -) - -claplusplus_lib = static_library('claplusplus', - sources: source_dir, +# Build executable directly from all necessary sources +executable('claPlusPlus', + sources: [ + 'src/example.cpp', + 'src/Arg.cpp', + 'src/Parser.cpp' + ], include_directories: include_dir, - install: true -) - -# Install headers -install_headers( - 'include/Arg.hpp', - 'include/Parser.hpp', - subdir: 'claplusplus' + install: false ) -executable('app', - sources : 'src/old_main.cpp', +parser_lib = static_library('clap_parser', + sources: ['src/Parser.cpp', 'src/Arg.cpp'], include_directories: include_dir, - link_with: claplusplus_lib, - install : false + install: false ) + +tests = [ + 'test_priority.cpp', + 'test_flag.cpp', + 'test_combo.cpp', +] + +test_bins = [] +foreach test : tests + name = test.replace('.cpp', '') + bin = executable( + name, + 'tests/' + test, + link_with: parser_lib, + include_directories: include_dir, + install: false + ) + test_bins += [bin] +endforeach + +# register a bunch of test cases +# TODO basic tests for constructing stuff +# 1) priority tests (default value '1') +test('priority_default', test_bins[0], + args: [], + env: ['EXPECTED=1']) +test('priority_auto_env', test_bins[0], + args: [], + env: ['TEST_PRIORITY_VAL=2', 'EXPECTED=2']) +test('priority_from_env', test_bins[0], + args: [], + env: ['VAL=3', 'TEST_PRIORITY_VAL=2', 'EXPECTED=3']) +test('priority_manual', test_bins[0], + args: ['--val', '4'], + env: ['VAL=3', 'TEST_PRIORITY_VAL=2', 'EXPECTED=4']) + +# 2) flag tests +test('flag_absent', test_bins[1], + args: [], + env: ['EXPECTED=0']) +test('flag_present', test_bins[1], + args: ['--opt'], + env: ['EXPECTED=1']) + +# 3) combo tests +test('combo_default', test_bins[2], + args: [], + env: ['EXPECTED_V=10', 'EXPECTED_B=0']) +test('combo_flag_first', test_bins[2], + args: ['--flag', '--val', '7'], + env: ['EXPECTED_V=7', 'EXPECTED_B=1']) +test('combo_value_first', test_bins[2], + args: ['--val', '8', '--flag'], + env: ['EXPECTED_V=8', 'EXPECTED_B=1']) +test('combo_env_and_manual', test_bins[2], + args: ['--val', '9'], + env: ['VAL=4', 'TEST_COMBO_VAL=5', 'EXPECTED_V=9', 'EXPECTED_B=0']) diff --git a/src/Arg.cpp b/src/Arg.cpp index 0f08f6a..b7766e9 100644 --- a/src/Arg.cpp +++ b/src/Arg.cpp @@ -1,84 +1,86 @@ +#include "Arg.hpp" + #include #include #include #include -#include - -#include "../include/Arg.hpp" -#include "../src/utils.cpp" -Arg::Arg(std::string name) : name_(std::move(name)), long_(name_), required_(false), takes_value_(true), has_env_(false), try_env_(false), value_(std::nullopt) {} +Arg::Arg(const std::string& name) : + name_(name), + short_name_(""), + long_name_(this->name_), + help_(""), + is_required_(false), + is_flag_(false), + env_name_(""), + auto_env_(false), + default_value_(""), + value_(std::nullopt) +{} // Setters -Arg& Arg::short_name(const std::string& s) { - short_ = s; +Arg& Arg::short_name(const std::string& short_name) { + short_name_ = short_name; return *this; } -Arg& Arg::help(const std::string& h) { - help_ = h; +Arg& Arg::help(const std::string& help) { + help_ = help; return *this; } Arg& Arg::required(bool is_required) { - required_ = is_required; + is_required_ = is_required; return *this; } -Arg& Arg::takes_value(bool takes) { - takes_value_ = takes; +Arg& Arg::is_flag() { + is_flag_ = true; + default_value_ = "0"; return *this; } -Arg& Arg::default_value(const std::string& default_val) { - default_ = default_val; +Arg& Arg::default_value(const std::string& default_value) { + default_value_ = default_value; return *this; } -bool Arg::requires_value() const { return takes_value_; } Arg& Arg::from_env(const char* env_var_name) { - this->has_env_ = true; this->env_name_ = env_var_name; // std::string value_from_env = ptr_unwrap_or(std::getenv(env_var_name), concat("value \'", env_var_name, "\' not present in env for: ", this->name_)); // std::optional value_from_env = ptr_unwrap_or(std::getenv(env_var_name), std::nullopt); // this->value_ = ptr_unwrap_or(std::getenv(env_var_name), std::nullopt); +/* auto ptr = std::getenv(env_var_name); if (!ptr) { this->value_ = std::nullopt; } else { this->value_ = ptr; } +*/ return *this; }; -Arg& Arg::try_env() { - this -> try_env_ = true; +Arg& Arg::auto_env() { + this -> auto_env_ = true; return *this; }; -// Getters -const std::string& Arg::name() const { return name_; } -const std::string& Arg::short_name() const { return short_; } -const std::string& Arg::long_name() const { return long_; } -const std::string& Arg::help() const { return help_; } -bool Arg::is_required() const { return required_; } -bool Arg::takes_value() const { return takes_value_; } -bool Arg::has_default() const { return !default_.empty(); } -bool Arg::has_env() const { return has_env_; } -const std::string& Arg::default_value() const { return default_; } +void Arg::print_arg(std::ostream& os, const Arg& arg, int indent) { + print_indent(os, indent); os << "Arg {\n"; -std::ostream& operator<<(std::ostream& os, const Arg& arg) { - os << "Arg {\n" - << " name: \"" << arg.name_ << "\",\n" - << " short: \"" << arg.short_ << "\",\n" - << " long: \"" << arg.long_ << "\",\n" - << " help: \"" << arg.help_ << "\",\n" - << " required: " << std::boolalpha << arg.required_ << ",\n" - << " takes_value: " << std::boolalpha << arg.takes_value_ << ",\n" - << " default: \"" << arg.default_ << "\",\n" - << " value: "; + print_indent(os, indent + 1); os << "name: \"" << arg.name_ << "\",\n"; + print_indent(os, indent + 1); os << "short: \"" << arg.short_name_ << "\",\n"; + print_indent(os, indent + 1); os << "long: \"" << arg.long_name_ << "\",\n"; + 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 << "default: \"" << arg.default_value_ << "\",\n"; + print_indent(os, indent + 1); os << "value: "; if (arg.value_) os << "\"" << arg.value_.value() << "\""; else - os << "nullopt"; - os << "\n}"; - return os; + os << "std::nullopt"; + os << '\n'; + + print_indent(os, indent); os << "}"; } -void Arg::set_try_env_name(const std::string& s){ - this->try_env_name_ = s; +std::ostream& operator<<(std::ostream& os, const Arg& arg) { + Arg::print_arg(os, arg, 0); + return os; } diff --git a/src/Parser.cpp b/src/Parser.cpp index 5047260..dcdfc44 100644 --- a/src/Parser.cpp +++ b/src/Parser.cpp @@ -1,66 +1,79 @@ -#include "../include/Parser.hpp" +#include "Parser.hpp" #include "Arg.hpp" +#include "utils.hpp" + #include #include -#include #include +#include #include -#include - -void ClapParser::parse(int argc, char* argv[]) { - program_name_ = argv[0]; +#include + +void ClapParser::parse(const int& argc, char* argv[]) { + const std::string& raw_program_name = argv[0]; +#ifdef _WIN32 + std::string parsed_name = raw_program_name.substr(raw_program_name.find_last_of('\\') + 1); + parsed_name.erase(parsed_name.size() - 4); +#else + std::string parsed_name = raw_program_name.substr(raw_program_name.find_last_of('/') + 1); +#endif + this->program_name_ = parsed_name; std::vector args(argv + 1, argv + argc); - std::unordered_set args_with_values; - apply_defaults(); - check_env(); - parse_options(args); + this->apply_defaults(); + this->check_env(); + this->parse_options(args); // parse from cli (argc, argv) // parse_positional_args(args); // Validate all arguments that need values received them for (const auto& arg : args_) { - if (arg.takes_value() && args_with_values.count(arg.name()) == 0) { - if (arg.is_required() && !arg.has_default()) { - throw std::runtime_error("Argument '" + arg.name() + "' requires a value"); - } + // std::cerr << arg << "\n\n\n"; + if (arg.get__is_required() && !arg.has_value()) { + throw std::runtime_error("argument '" + arg.get__name() + "' is required"); } } - - check_required_args(); } -void ClapParser::add_arg(const Arg& arg) { args_.push_back(arg); } +void ClapParser::add_arg(const Arg& arg) { args_.emplace_back(arg); } void ClapParser::parse_options(const std::vector& args) { for (size_t i = 0; i < args.size(); ++i) { - const std::string& token = args[i]; + const auto& token = args.at(i); + + if (token == "--help" || token == "-h") { + print_help(); + exit(0); + } - if (is_long_option(token)) { - i = handle_long_option(token, args, i); - } else if (is_short_option(token)) { - i = handle_short_option(token, args, i); + 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"); + } } else { - // Positional arguments are handled separately - break; + arg->set__value("1"); } } } void ClapParser::check_env() { for (auto& arg : args_) { - if (arg.try_env_) { - std::string program_name = this->program_name_.substr(this->program_name_.rfind('/') + 1); - std::string env_name = program_name + '_' + arg.name(); + 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); }); - // std::cerr << env_name << "\n"; - arg.set_try_env_name(env_name); auto value_from_env = std::getenv(env_name.c_str()); if (value_from_env) { - values_[arg.name()] = value_from_env; + arg.set__value(value_from_env); } } - if (arg.has_env() && arg.value_.has_value()) { - values_[arg.name()] = arg.value_.value(); + if (arg.has_env()) { + auto value_from_env = std::getenv(arg.get__env_name().c_str()); + if (value_from_env) { + arg.set__value(value_from_env); + } } } }; @@ -86,77 +99,12 @@ void ClapParser::check_env() { // } // } -void ClapParser::check_required_args() { - for (const auto& arg : args_) { - if (arg.is_required() && values_.find(arg.name()) == values_.end()) { - throw std::runtime_error("missing required argument: " + arg.name()); - } - } -} - -size_t ClapParser::handle_long_option(const std::string& token, const std::vector& args, size_t i) { - std::string opt_name = token.substr(2); - if (opt_name == "help") { - print_help(); - exit(0); - } - const Arg* arg = find_option(opt_name); - if (arg == nullptr) { - throw std::runtime_error("Unknown option: " + token); - } - - if (arg->takes_value()) { - i = handle_option_with_value(arg, args, i, token); - } else { - values_[arg->name()] = true; // Boolean flag - } - - return i; -} - -size_t ClapParser::handle_short_option(const std::string& token, const std::vector& args, size_t i) { - std::string opt_name = token.substr(1); - if (opt_name == "h") { - print_help(); - exit(0); - } - const Arg* arg = find_option(opt_name); - if (arg == nullptr) { - throw std::runtime_error("unknown option: " + token); - } - - if (arg->takes_value()) { - i = handle_option_with_value(arg, args, i, token); - } else { - values_[arg->name()] = true; // Boolean flag - } - - return i; -} - -size_t ClapParser::handle_option_with_value(const Arg* arg, const std::vector& args, size_t i, - const std::string& token) { - if (i + 1 < args.size() && !is_option(args[i + 1])) { - // Use next argument as value - values_[arg->name()] = std::string(args[i + 1]); - return i + 1; // Skip the value in the next iteration - } - if (arg->has_default()) { - // Use default value - values_[arg->name()] = std::string(arg->default_value()); - } else { - throw std::runtime_error("Option '" + token + "' requires a value but none was provided"); - } - - return i; -} - void ClapParser::handle_missing_positional(const Arg& arg) { - if (arg.is_required()) { - throw std::runtime_error("Missing required positional argument: " + arg.name()); + if (arg.get__is_required()) { + throw std::runtime_error("missing required positional argument: " + arg.get__name()); } if (arg.has_default()) { - values_[arg.name()] = std::string(arg.default_value()); + values_[arg.get__name()] = std::string(arg.get__default_value()); } } @@ -171,25 +119,27 @@ void ClapParser::handle_missing_positional(const Arg& arg) { } void ClapParser::print_help() const { - std::cout << "Usage: " << program_name_ << " [OPTIONS]"; + std::cout << "Usage: " << this->program_name_ << " [OPTIONS]"; auto positionals = get_positional_args(); for (const auto& pos : positionals) { - std::cout << " [" << pos.name() << "]"; + std::cout << " [" << pos.get__name() << "]"; } std::cout << "\n\nOptions:\n"; for (const auto& arg : args_) { - arg.short_name().empty()? std::cout << " " : std::cout << " -" << arg.short_name() << ", "; - std::cout << "--" << arg.long_name(); - std::cout << "\t" << arg.help(); + arg.get__short_name().empty()? std::cout << " " : std::cout << " -" << arg.get__short_name() << ", "; + std::cout << "--" << arg.get__long_name(); + std::cout << "\t" << arg.get__help(); if (arg.has_default()) { - std::cout << " (default: " << arg.default_value() << ")"; + std::cout << " (default: " << arg.get__default_value() << ")"; } if (arg.has_env()) { - std::cout << " [env: " << arg.env_name_ << "]"; + std::cout << " [env: " << arg.get__env_name() << "]"; } - if (arg.try_env_) { - std::cout << " [def.env: " << arg.try_env_name_ << "]"; + 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); }); + std::cout << " [def.env: " << env_name << "]"; } std::cout << "\n"; } @@ -202,28 +152,30 @@ void ClapParser::print_help() const { if (!positionals.empty()) { std::cout << "\nPositional arguments:\n"; for (const auto& pos : positionals) { - std::cout << " " << pos.name() << "\t" << pos.help(); + std::cout << " " << pos.get__name() << "\t" << pos.get__help(); if (pos.has_default()) - std::cout << " (default: " << pos.default_value() << ")"; + std::cout << " (default: " << pos.get__default_value() << ")"; std::cout << "\n"; } } } // Helper methods -const Arg* ClapParser::find_option(const std::string& name) const { - for (const auto& arg : args_) { - if (arg.long_name() == name || arg.short_name() == name) { - return &arg; - } +std::optional ClapParser::find_arg(ClapParser& parser, const std::string& arg_name) { + auto it = std::find_if(parser.args_.begin(), parser.args_.end(), [&](Arg& arg) { + return ( "--" + arg.get__long_name() == arg_name || "-" + arg.get__short_name() == arg_name ); + }); + + if (it == parser.args_.end()) { + return std::nullopt; } - return nullptr; + return &(*it); } std::vector ClapParser::get_positional_args() const { std::vector positional; for (const auto& arg : args_) { - if (arg.short_name().empty() && arg.long_name().empty()) { + if (arg.get__short_name().empty() && arg.get__long_name().empty()) { positional.push_back(arg); } } @@ -231,31 +183,35 @@ std::vector ClapParser::get_positional_args() const { } void ClapParser::apply_defaults() { - for (const auto& arg : args_) { - if (values_.find(arg.name()) == values_.end() && arg.has_default()) { - values_[arg.name()] = std::string(arg.default_value()); + for (auto& arg : args_) { + if (!arg.has_value() && arg.has_default()) { + arg.set__value(arg.get__default_value()); } } } -bool ClapParser::has(const std::string& name) const { return values_.find(name) != values_.end(); } +void ClapParser::print_parser(std::ostream& os, const ClapParser& parser, int indent) { + print_indent(os, indent); os << "ClapParser {\n"; -std::ostream& operator<<(std::ostream& os, const ClapParser& parser) { - os << "ClapParser {\n"; - os << " program_name: \"" << parser.program_name_ << "\",\n"; + print_indent(os, indent + 1); os << "program_name: \"" << parser.program_name_ << "\",\n"; - os << " args: [\n"; + print_indent(os, indent + 1); os << "args: [\n"; for (const auto& arg : parser.args_) { - os << " " << arg << ",\n"; + Arg::print_arg(os, arg, indent + 2); + os << ",\n"; } - os << " ],\n"; + print_indent(os, indent + 1); os << "],\n"; - os << " values: {\n"; + print_indent(os, indent + 1); os << "values: {\n"; for (const auto& [key, val] : parser.values_) { - os << " \"" << key << "\": \"" << val << "\",\n"; + print_indent(os, indent + 2); os << "\"" << key << "\": \"" << val << "\",\n"; } - os << " }\n"; + print_indent(os, indent + 1); os << "}\n"; - os << "}"; + print_indent(os, indent); os << "}"; +} + +std::ostream& operator<<(std::ostream& os, const ClapParser& parser) { + ClapParser::print_parser(os, parser, 0); return os; } diff --git a/src/example.cpp b/src/example.cpp new file mode 100644 index 0000000..89b609f --- /dev/null +++ b/src/example.cpp @@ -0,0 +1,42 @@ +#include "Parser.hpp" +#include "Arg.hpp" +#include "utils.hpp" + +#include +#include + +void run(ClapParser& parsed_args); + +int main(const int argc, char* argv[]) { + ClapParser arg_parser; + auto num1 = Arg("num1").from_env("ASDF").auto_env().required(true); + // std::cerr << num1 << "\n"; + arg_parser.add_arg(num1); + auto num2 = Arg("num2").short_name("N").from_env("TES").default_value("99"); + arg_parser.add_arg(num2); + + arg_parser.add_arg(Arg("test").is_flag()); + // arg_parser.add_arg(Arg("test").is_flag(true)); + + try { + arg_parser.parse(argc, argv); + // std::cerr << arg_parser; + run(arg_parser); + } + catch (const std::exception& e) { + std::cerr << "\n\n\nerror: " << e.what() << "\n\n\n"; + // arg_parser.print_help(); + } +} + +void run(ClapParser& arg_parser) { + // std::cerr << "running\n"; + auto num1 = ok_or_throw_str(arg_parser.get_one_as("num1"), "num1 not defined"); + auto num2 = ok_or_throw_str(arg_parser.get_one_as("num2"), "num2 not defined"); + + std::cout << "num1: " << num1 << '\n'; + std::cout.precision(5); + std::cout << std::fixed << "num2: " << num2 << '\n'; + + // std::cerr << arg_parser; +} diff --git a/tests/test_combo.cpp b/tests/test_combo.cpp new file mode 100644 index 0000000..b07170d --- /dev/null +++ b/tests/test_combo.cpp @@ -0,0 +1,33 @@ +// tests/test_combo.cpp +#include "Parser.hpp" +#include "Arg.hpp" +#include "utils.hpp" +#include +#include +#include + +int main(int argc, char* argv[]) { + ClapParser p; + // a value arg with full priority stack: + auto val = Arg("val").from_env("VAL").auto_env().default_value("10"); + auto boolean = Arg("flag").is_flag(); + p.add_arg(val); + p.add_arg(boolean); + p.parse(argc, argv); + + // EXPECTED_V -> any int + // EXPECTED_B -> "1" or "0" + const auto ev = std::getenv("EXPECTED_V"); + const auto eb = std::getenv("EXPECTED_B"); + assert(ev && eb && "EXPECTED_V/B must be set"); + + int expected_val = std::stoi(ev); + bool expected_boolean = std::stoi(eb); + + 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"); + + assert(actual_val == expected_val); + assert(actual_boolean == expected_boolean); + return 0; +} diff --git a/tests/test_flag.cpp b/tests/test_flag.cpp new file mode 100644 index 0000000..9c2676d --- /dev/null +++ b/tests/test_flag.cpp @@ -0,0 +1,24 @@ +// tests/test_flag.cpp +#include "Parser.hpp" +#include "Arg.hpp" +#include "utils.hpp" +#include +#include +#include + +int main(int argc, char* argv[]) { + ClapParser p; + auto f = Arg("opt").is_flag(); + p.add_arg(f); + p.parse(argc, argv); + + // EXPECTED should be "1" or "0" (present or absent) + const auto e = std::getenv("EXPECTED"); + assert(e && "EXPECTED must be set"); + bool expected = std::string(e) == "1"; + + auto actual = ok_or_throw_str(p.get_one_as("opt"), "test argument: 'opt' is missing"); + std::cerr << p << '\n'; + assert(actual == expected); + return 0; +} diff --git a/tests/test_priority.cpp b/tests/test_priority.cpp new file mode 100644 index 0000000..49d3c89 --- /dev/null +++ b/tests/test_priority.cpp @@ -0,0 +1,34 @@ +// tests/test_priority.cpp +#include "Parser.hpp" +#include "Arg.hpp" +#include "utils.hpp" +#include +#include +#include +#include +#include + +/* VALUE PRIORITY: + 1. cli arg: '--val' + 2. env var: 'VAL' + 3. auto env var: 'PRIORITY_MANUAL_VAL' + 4. default value: '1' +*/ + +int main(const int argc, char* argv[]) { + ClapParser p; + + auto a = Arg("val").from_env("VAL").auto_env().default_value("1"); + p.add_arg(a); + p.parse(argc, argv); + + // gets read from meson test env + const auto e = std::getenv("EXPECTED"); + assert(e && "EXPECTED must be set"); + int expected = std::stoi(e); + + auto actual = ok_or_throw_str(p.get_one_as("val"), "test argument: 'val' is missing"); + std::cerr << p << '\n'; + assert(actual == expected); + return 0; +}