PoC: better argument building.

Add a few more ideas to todo.
This commit is contained in:
2025-10-26 08:21:34 +04:00
parent 44bc63d17d
commit 516c6fa732
7 changed files with 392 additions and 92 deletions

10
TODO.md
View File

@@ -38,3 +38,13 @@ Configure CMAKE to ensure the library can be built as a library and can be insta
# TODO 5: Display help
Display help doesn't reflect the conventions right now. Also it should come automatically, and should be allowed to overriden by user.
# TODO 6: Accumulate repeated calls
Add support to letting users accumulate repeated calls to a flag. If the flag is called x times, the result should be x items stored in a vector,
instead of an action doing it.
# TODO 7: Defaults/Implicits
If given, an arguments default store value could be changed. If nothing was given use that value instead.
# TODO 8: Validators
If given, validate the argument before passing to the storage or action. If fail, let user decide fail loud or fail skip.

View File

@@ -360,8 +360,6 @@ namespace argument_parser {
friend class macos_parser;
friend class fake_parser;
};
}
#endif // ARGUMENT_PARSER_HPP

View File

@@ -10,8 +10,10 @@
namespace argument_parser {
class fake_parser : public base_parser {
public:
fake_parser(std::string const& program_name, std::vector<std::string> const& arguments) {
this->program_name = program_name;
fake_parser() = default;
fake_parser(std::string program_name, std::vector<std::string> const& arguments) {
this->program_name = std::move(program_name);
parsed_arguments = arguments;
}
@@ -22,6 +24,14 @@ namespace argument_parser {
fake_parser(std::string const& program_name, std::initializer_list<std::string> const& arguments) :
fake_parser(program_name, std::vector<std::string>(arguments)) {}
void set_program_name(std::string const& program_name) {
this->program_name = program_name;
}
void set_parsed_arguments(std::vector<std::string> const& parsed_arguments) {
this->parsed_arguments = parsed_arguments;
}
};
}

View File

@@ -0,0 +1,249 @@
#pragma once
#include <argument_parser.hpp>
#include <cstdlib>
#include <exception>
#include <fake_parser.hpp>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <variant>
#include <vector>
namespace argument_parser::v2 {
namespace internal {
static inline fake_parser fake_parser{};
}
enum class add_argument_flags {
ShortArgument,
LongArgument,
HelpText,
Action,
Required
};
namespace flags {
constexpr static inline add_argument_flags ShortArgument = add_argument_flags::ShortArgument;
constexpr static inline add_argument_flags LongArgument = add_argument_flags::LongArgument;
constexpr static inline add_argument_flags HelpText = add_argument_flags::HelpText;
constexpr static inline add_argument_flags Action = add_argument_flags::Action;
constexpr static inline add_argument_flags Required = add_argument_flags::Required;
}
class base_parser : private argument_parser::base_parser {
public:
template<typename T>
using typed_flag_value = std::variant<std::string, parametered_action<T>, bool>;
using non_typed_flag_value = std::variant<std::string, non_parametered_action, bool>;
template<typename T>
using typed_argument_pair = std::pair<add_argument_flags, typed_flag_value<T>>;
using non_typed_argument_pair = std::pair<add_argument_flags, non_typed_flag_value>;
template<typename T>
void add_argument(std::unordered_map<add_argument_flags, typed_flag_value<T>> const& argument_pairs) {
std::unordered_map<extended_add_argument_flags, bool> found_params {{
extended_add_argument_flags::IsTyped, true
}};
std::string short_arg, long_arg, help_text;
std::unique_ptr<action_base> action;
bool required = false;
if (argument_pairs.contains(add_argument_flags::ShortArgument)) {
found_params[extended_add_argument_flags::ShortArgument] = true;
short_arg = get_or_throw<std::string>(argument_pairs.at(add_argument_flags::ShortArgument), "short");
}
if (argument_pairs.contains(add_argument_flags::LongArgument)) {
found_params[extended_add_argument_flags::LongArgument] = true;
long_arg = get_or_throw<std::string>(argument_pairs.at(add_argument_flags::LongArgument), "long");
if (short_arg.empty()) short_arg = long_arg;
} else {
if (!short_arg.empty()) long_arg = short_arg;
}
if (argument_pairs.contains(add_argument_flags::Action)) {
found_params[extended_add_argument_flags::Action] = true;
action = get_or_throw<parametered_action<T>>(argument_pairs.at(add_argument_flags::Action), "action").clone();
}
if (argument_pairs.contains(add_argument_flags::HelpText)) {
help_text = get_or_throw<std::string>(argument_pairs.at(add_argument_flags::HelpText), "help");
} else {
help_text = short_arg + ", " + long_arg;
}
if (argument_pairs.contains(add_argument_flags::Required) && get_or_throw<bool>(argument_pairs.at(add_argument_flags::Required), "required")) {
required = true;
}
auto suggested_add = suggest_candidate(found_params);
if (suggested_add == candidate_type::unknown) {
throw std::runtime_error("Could not match any add argument overload to given parameters. Are you missing some required parameter?");
}
switch (suggested_add) {
case candidate_type::typed_action:
base::add_argument(short_arg, long_arg, help_text, *static_cast<parametered_action<T>*>(&(*action)), required);
break;
case candidate_type::store_other:
base::add_argument(short_arg, long_arg, help_text, required);
break;
default:
throw std::runtime_error("Could not match the arguments against any overload.");
}
}
template<typename T>
void add_argument(std::initializer_list<typed_argument_pair<T>> const& pairs) {
std::unordered_map<add_argument_flags, typed_flag_value<T>> args;
for (auto& [k, v]: pairs) {
args[k] = v;
}
add_argument<T>(args);
}
void add_argument(std::initializer_list<non_typed_argument_pair> const& pairs) {
std::unordered_map<add_argument_flags, non_typed_flag_value> args;
for (auto& [k, v] : pairs) {
args[k] = v;
}
add_argument(args);
}
void add_argument(std::unordered_map<add_argument_flags, non_typed_flag_value> const& argument_pairs) {
std::unordered_map<extended_add_argument_flags, bool> found_params {{
extended_add_argument_flags::IsTyped, false
}};
std::string short_arg, long_arg, help_text;
std::unique_ptr<action_base> action;
bool required = false;
if (argument_pairs.contains(add_argument_flags::ShortArgument)) {
found_params[extended_add_argument_flags::ShortArgument] = true;
short_arg = get_or_throw<std::string>(argument_pairs.at(add_argument_flags::ShortArgument), "short");
}
if (argument_pairs.contains(add_argument_flags::LongArgument)) {
found_params[extended_add_argument_flags::LongArgument] = true;
long_arg = get_or_throw<std::string>(argument_pairs.at(add_argument_flags::LongArgument), "long");
if (short_arg.empty()) short_arg = long_arg;
} else {
if (!short_arg.empty()) long_arg = short_arg;
}
if (argument_pairs.contains(add_argument_flags::Action)) {
found_params[extended_add_argument_flags::Action] = true;
action = get_or_throw<non_parametered_action>(argument_pairs.at(add_argument_flags::Action), "action").clone();
}
if (argument_pairs.contains(add_argument_flags::HelpText)) {
help_text = get_or_throw<std::string>(argument_pairs.at(add_argument_flags::HelpText), "help");
} else {
help_text = short_arg + ", " + long_arg;
}
if (argument_pairs.contains(add_argument_flags::Required) && get_or_throw<bool>(argument_pairs.at(add_argument_flags::Required), "required")) {
required = true;
}
auto suggested_add = suggest_candidate(found_params);
if (suggested_add == candidate_type::unknown) {
throw std::runtime_error("Could not match any add argument overload to given parameters. Are you missing some required parameter?");
}
switch (suggested_add) {
case candidate_type::non_typed_action:
base::add_argument(short_arg, long_arg, help_text, *static_cast<non_parametered_action*>(&(*action)), required);
break;
case candidate_type::store_boolean:
base::add_argument(short_arg, long_arg, help_text, required);
break;
default:
throw std::runtime_error("Could not match the arguments against any overload. The suggested candidate was: " + std::to_string((int(suggested_add))));
}
}
argument_parser::base_parser& to_v1() {
return *this;
}
void handle_arguments(std::initializer_list<conventions::convention const* const> convention_types) {
base::handle_arguments(convention_types);
}
template<typename T>
std::optional<T> get_optional(std::string const& arg) {
return base::get_optional<T>(arg);
}
void on_complete(std::function<void(argument_parser::base_parser const&)> const& action) {
base::on_complete(action);
}
protected:
void set_program_name(std::string p) {
base::program_name = std::move(p);
}
std::vector<std::string>& ref_parsed_args() {
return base::parsed_arguments;
}
private:
using base = argument_parser::base_parser;
enum class extended_add_argument_flags {
ShortArgument,
LongArgument,
Action,
IsTyped
};
enum class candidate_type {
store_boolean,
store_other,
typed_action,
non_typed_action,
unknown
};
template<typename T, size_t S>
bool satisfies_at_least_one(std::array<T, S> const& arr, std::unordered_map<T, bool> const& map) {
for (const auto& req: arr) {
if (map.contains(req)) return true;
}
return false;
}
candidate_type suggest_candidate(std::unordered_map<extended_add_argument_flags, bool> const& available_vars) {
auto constexpr required_at_least_one = std::array<extended_add_argument_flags, 2> { extended_add_argument_flags::ShortArgument, extended_add_argument_flags::LongArgument };
if (!satisfies_at_least_one(required_at_least_one, available_vars)) return candidate_type::unknown;
if (available_vars.contains(extended_add_argument_flags::Action)) {
if (available_vars.at(extended_add_argument_flags::IsTyped))
return candidate_type::typed_action;
else return candidate_type::non_typed_action;
}
if (available_vars.at(extended_add_argument_flags::IsTyped)) return candidate_type::store_other;
return candidate_type::store_boolean;
}
template<typename T, typename I>
T get_or_throw(typed_flag_value<I> const& v, std::string_view key) {
if (auto p = std::get_if<T>(&v)) return *p;
throw std::invalid_argument(std::string("variant type mismatch for key: ") + std::string(key));
}
template<typename T>
T get_or_throw(non_typed_flag_value const& v, std::string_view key) {
if (auto p = std::get_if<T>(&v)) return *p;
throw std::invalid_argument(std::string("variant type mismatch for key: ") + std::string(key));
}
};
}

View File

@@ -5,6 +5,7 @@
#include <argument_parser.hpp>
#include <crt_externs.h>
#include <parser_v2.hpp>
#include <string>
namespace argument_parser {
@@ -13,13 +14,28 @@ namespace argument_parser {
macos_parser() {
const int argc = *_NSGetArgc();
if (char **argv = *_NSGetArgv(); argc > 0 && argv != nullptr && argv[0] != nullptr) {
program_name = argv[0];
program_name = (argv[0]);
for (int i = 1; i < argc; ++i) {
if (argv[i] != nullptr) parsed_arguments.emplace_back(argv[i]);
}
}
}
};
namespace v2 {
class macos_parser : public v2::base_parser {
public:
macos_parser() {
const int argc = *_NSGetArgc();
if (char **argv = *_NSGetArgv(); argc > 0 && argv != nullptr && argv[0] != nullptr) {
set_program_name(argv[0]);
for (int i = 1; i < argc; ++i) {
if (argv[i] != nullptr) ref_parsed_args().emplace_back(argv[i]);
}
}
}
};
}
}
#endif

View File

@@ -1,98 +1,61 @@
#pragma once
#ifdef _WIN32
#ifndef WINDOWS_PARSER_HPP
// compiler bitches if you don't use lean and mean and remove shit ton of other macros that comes preloaded with the windows api.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
// I'm speaking to you Microsoft, STOP MAKING EVERYTHING A FUCKING MACRO. IT BREAKES CPP!
#define NOGDI
#define NOHELP
#define NOMCX
#define NOIME
#define NOCOMM
#define NOKANJI
#define NOSERVICE
#define NOMDI
#define NOSOUND
// also this fixes error about no architecture being targeted (somehow)
#if defined(_M_AMD64) && !defined(_AMD64_)
# define _AMD64_
#endif
#if defined(_M_IX86) && !defined(_X86_)
# define _X86_
#endif
#if defined(_M_ARM64) && !defined(_ARM64_)
# define _ARM64_
#endif
#include <argument_parser.hpp>
#include <fstream>
#include <memory>
#include <string>
// THIS HAS TO BE THE FIRST. DON'T CHANGE THEIR ORDER.
#include <windows.h>
#include <processenv.h>
#include <shellapi.h>
namespace argument_parser {
class windows_parser : public base_parser {
public:
windows_parser() {
int nArgs;
LPWSTR commandLineA = GetCommandLineW();
auto argvW = std::unique_ptr<LPWSTR, local_free_deleter> { CommandLineToArgvW(commandLineA, &nArgs) };
if (argvW == nullptr) {
throw std::runtime_error("Failed to get command line arguments.");
class windows_parser : public base_parser {
public:
windows_parser() {
int nArgs = 0;
LPWSTR* raw = ::CommandLineToArgvW(::GetCommandLineW(), &nArgs);
if (!raw) {
throw std::runtime_error("CommandLineToArgvW failed (" +
std::to_string(::GetLastError()) + ")");
}
std::unique_ptr<LPWSTR, void(*)(LPWSTR*)> argvW{ raw, [](LPWSTR* p){ if (p) ::LocalFree(p); } };
if (nArgs <= 0) {
throw std::runtime_error("No command line arguments found, including program name.");
throw std::runtime_error("No command line arguments found.");
}
program_name = utf8_from_wstring(argvW.get()[0]);
parsed_arguments.reserve(static_cast<size_t>(nArgs - 1));
{
std::wstring w0 = argvW.get()[0];
auto pos = w0.find_last_of(L"\\/");
std::wstring base = (pos == std::wstring::npos) ? w0 : w0.substr(pos + 1);
program_name = utf8_from_wstring(base);
}
parsed_arguments.reserve(static_cast<size_t>(nArgs > 0 ? nArgs - 1 : 0));
for (int i = 1; i < nArgs; ++i) {
parsed_arguments.emplace_back(utf8_from_wstring(argvW.get()[i]));
}
}
private:
struct local_free_deleter {
void operator()(HLOCAL ptr) const noexcept {
if (ptr != nullptr) ::LocalFree(ptr);
}
};
private:
static std::string utf8_from_wstring(const std::wstring& w) {
if (w.empty()) return {};
int needed = ::WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, nullptr, 0, nullptr, nullptr);
if (needed == 0) {
throw std::runtime_error("WideCharToMultiByte sizing failed ("
+ std::to_string(GetLastError()) + ")");
int needed = ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
w.c_str(), -1, nullptr, 0, nullptr, nullptr);
if (needed <= 0) {
throw std::runtime_error("WideCharToMultiByte sizing failed (" +
std::to_string(::GetLastError()) + ")");
}
std::string out;
out.resize(static_cast<size_t>(needed - 1));
int written = ::WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1,
out.data(), needed, nullptr, nullptr);
if (written == 0) {
throw std::runtime_error("WideCharToMultiByte convert failed ("
+ std::to_string(GetLastError()) + ")");
int written = ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
w.c_str(), -1, out.data(), needed - 1, nullptr, nullptr);
if (written <= 0) {
throw std::runtime_error("WideCharToMultiByte convert failed (" +
std::to_string(::GetLastError()) + ")");
}
return out;
}
};
};
using parser = windows_parser;
}
#endif
#endif

View File

@@ -1,7 +1,10 @@
#include "macos_parser.hpp"
#include <string>
#define ALLOW_DASH_FOR_WINDOWS 0
#include <parser_v2.hpp>
#include <argparse>
#include <cstdlib>
#include <expected>
#include <iostream>
#include <fstream>
#include <regex>
@@ -135,7 +138,9 @@ void run_grep(argument_parser::base_parser const& parser) {
}
}
int main() {
int v1Examples() {
auto parser = argument_parser::parser{};
parser.add_argument("e", "echo", "echoes given variable", echo, false);
@@ -174,6 +179,55 @@ int main() {
std::cout << item << std::endl;
}
}
return 0;
}
int v2Examples() {
using namespace argument_parser::v2::flags;
argument_parser::v2::macos_parser parser;
parser.add_argument<std::string>({
{ ShortArgument, "e" },
{ LongArgument, "echo" },
{ Action, echo }
});
parser.add_argument<Point>({
{ ShortArgument, "ep" },
{ LongArgument, "echo-point" },
{ Action, echo_point }
});
parser.add_argument<std::string>({ // stores string for f/file flag
{ ShortArgument, "f" },
{ LongArgument, "file"},
// if no action, falls to store operation with given type.
});
parser.add_argument<std::regex>({ // stores string for g/grep flag
{ ShortArgument, "g" },
{ LongArgument, "grep" },
// same as 'file' flag
});
parser.add_argument<std::string>({
{ ShortArgument, "c" },
{ LongArgument, "cat" },
{ Action, cat }
});
parser.add_argument<Point>({
// { ShortArgument, "sp" }, // now if ShortArgument or LongArgument is missing, it will use it for the other.
{ LongArgument, "store-point" },
{ Required, true } // makes this flag required
});
parser.on_complete(::run_grep);
parser.handle_arguments(conventions);
return 0;
}
int main() {
return v2Examples();
}