Преглед на файлове

A new hand-rolled argument parsing library, of course

default_compile_flags
vector-of-bool преди 3 години
родител
ревизия
88c92f1589
променени са 10 файла, в които са добавени 1022 реда и са изтрити 1 реда
  1. +2
    -1
      library.jsonc
  2. +1
    -0
      package.jsonc
  3. +101
    -0
      src/debate/argument.cpp
  4. +111
    -0
      src/debate/argument.hpp
  5. +484
    -0
      src/debate/argument_parser.cpp
  6. +130
    -0
      src/debate/argument_parser.hpp
  7. +85
    -0
      src/debate/argument_parser.test.cpp
  8. +3
    -0
      src/debate/debate.hpp
  9. +50
    -0
      src/debate/enum.hpp
  10. +55
    -0
      src/debate/error.hpp

+ 2
- 1
library.jsonc Целия файл

@@ -20,6 +20,7 @@
// Explicit zlib link is required due to linker input order bug.
// Can be removed after alpha.5
"zlib/zlib",
"neo/compress"
"neo/compress",
"neargye/magic_enum",
]
}

+ 1
- 0
package.jsonc Целия файл

@@ -21,6 +21,7 @@
"neo-http^0.1.0",
"neo-io^0.1.1",
"boost.leaf~0.3.0",
"magic_enum+0.0.0",
],
"test_driver": "Catch-Main"
}

+ 101
- 0
src/debate/argument.cpp Целия файл

@@ -0,0 +1,101 @@
#include "./argument.hpp"

#include <fmt/color.h>

using namespace debate;

using strv = std::string_view;

using namespace std::literals;

strv argument::try_match_short(strv given) const noexcept {
for (auto& cand : short_spellings) {
if (given.starts_with(cand)) {
return cand;
}
}
return "";
}

strv argument::try_match_long(strv given) const noexcept {
for (auto& cand : long_spellings) {
if (!given.starts_with(cand)) {
continue;
}
auto tail = given.substr(cand.size());
// We should either be empty, as in '--argument value',
// or followed by an equal, as in '--argument=value'
if (tail.empty() || tail[0] == '=') {
return cand;
}
}
return "";
}

std::string argument::preferred_spelling() const noexcept {
if (!long_spellings.empty()) {
return "--"s + long_spellings.front();
} else if (!short_spellings.empty()) {
return "-"s + short_spellings.front();
} else {
return valname;
}
}

std::string argument::syntax_string() const noexcept {
std::string ret;
if (!required) {
ret.push_back('[');
}
if (is_positional()) {
ret.append(preferred_spelling());
} else {
ret.append(preferred_spelling());
if (nargs != 0) {
auto real_valname = !valname.empty()
? valname
: (long_spellings.empty() ? "<value>" : ("<" + long_spellings[0] + ">"));
ret.append(fmt::format(" {}", valname.empty() ? "<value>" : valname));
}
}
if (can_repeat) {
ret.append(" ...");
}
if (!required) {
ret.push_back(']');
}
return ret;
}

std::string argument::help_string() const noexcept {
std::string ret;
for (auto& l : long_spellings) {
ret.append(fmt::format(fmt::emphasis::bold, "--{}", l));
if (nargs != 0) {
ret.append(
fmt::format(fmt::emphasis::italic, "={}", valname.empty() ? "<value>" : valname));
}
ret.push_back('\n');
}
for (auto& s : short_spellings) {
ret.append(fmt::format(fmt::emphasis::bold, "-{}", s));
if (nargs != 0) {
ret.append(
fmt::format(fmt::emphasis::italic, " {}", valname.empty() ? "<value>" : valname));
}
ret.push_back('\n');
}
if (is_positional()) {
ret.append(preferred_spelling() + "\n");
}
ret.append(" ");
for (auto c : help) {
ret.push_back(c);
if (c == '\n') {
ret.append(2, ' ');
}
}
ret.push_back('\n');

return ret;
}

+ 111
- 0
src/debate/argument.hpp Целия файл

@@ -0,0 +1,111 @@
#pragma once

#include "./error.hpp"

#include <boost/leaf/exception.hpp>
#include <fmt/format.h>

#include <charconv>
#include <functional>
#include <string>
#include <string_view>
#include <vector>

namespace debate {

template <typename E>
constexpr auto make_enum_putter(E& dest) noexcept;

template <typename T>
class argument_value_putter {
T& _dest;

public:
explicit argument_value_putter(T& dest) noexcept
: _dest(dest) {}

void operator()(std::string_view value, std::string_view) { _dest = T(value); }
};

template <typename Int>
class integer_putter {
Int& _dest;

public:
explicit integer_putter(Int& d)
: _dest(d) {}

void operator()(std::string_view value, std::string_view spelling) {
auto res = std::from_chars(value.data(), value.data() + value.size(), _dest);
if (res.ec != std::errc{} || res.ptr != value.data() + value.size()) {
throw boost::leaf::exception(invalid_arguments(
"Invalid value given for integral argument"),
e_arg_spelling{std::string(spelling)},
e_invalid_arg_value{std::string(value)});
}
}
};

template <typename T>
constexpr auto make_argument_putter(T& dest) {
if constexpr (std::is_enum_v<T>) {
return make_enum_putter(dest); /// !! README: Include <debate/enum.hpp> to use enums here
} else if constexpr (std::is_integral_v<T>) {
return integer_putter(dest);
} else {
return argument_value_putter{dest};
}
}

constexpr inline auto store_value = [](auto& dest, auto val) {
return [&dest, val](std::string_view = {}, std::string_view = {}) { dest = val; };
};

constexpr inline auto store_true = [](auto& dest) { return store_value(dest, true); };
constexpr inline auto store_false = [](auto& dest) { return store_value(dest, false); };

constexpr inline auto put_into = [](auto& dest) { return make_argument_putter(dest); };

constexpr inline auto push_back_onto = [](auto& dest) {
return [&dest](std::string_view value, std::string_view = {}) { dest.emplace_back(value); };
};

struct argument {
std::vector<std::string> long_spellings{};
std::vector<std::string> short_spellings{};

std::string help{};
std::string valname{};

bool required = false;
int nargs = 1;
bool can_repeat = false;

std::function<void(std::string_view, std::string_view)> action;

// This member variable makes this strunct noncopyable, and has no other purpose
std::unique_ptr<int> _make_noncopyable{};
std::string_view try_match_short(std::string_view arg) const noexcept;
std::string_view try_match_long(std::string_view arg) const noexcept;
std::string preferred_spelling() const noexcept;
std::string syntax_string() const noexcept;
std::string help_string() const noexcept;
bool is_positional() const noexcept {
return long_spellings.empty() && short_spellings.empty();
}

argument dup() const noexcept {
return argument{
.long_spellings = long_spellings,
.short_spellings = short_spellings,
.help = help,
.valname = valname,
.required = required,
.nargs = nargs,
.can_repeat = can_repeat,
.action = action,
};
}
};

} // namespace debate

+ 484
- 0
src/debate/argument_parser.cpp Целия файл

@@ -0,0 +1,484 @@
#include "./argument_parser.hpp"

#include <boost/leaf/error.hpp>
#include <boost/leaf/exception.hpp>
#include <boost/leaf/on_error.hpp>

#include <fmt/color.h>
#include <fmt/format.h>

#include <set>

using strv = std::string_view;

using namespace debate;

namespace {

struct parse_engine {
debate::detail::parser_state& state;
const argument_parser* bottom_parser;

// Keep track of how many positional arguments we have seen
int positional_index = 0;

// Keep track of what we've seen
std::set<const argument*> seen{};

auto current_arg() const noexcept { return state.current_arg(); }
auto at_end() const noexcept { return state.at_end(); }
void shift() noexcept { return state.shift(); }

void see(const argument& arg) {
auto did_insert = seen.insert(&arg).second;
if (!did_insert && !arg.can_repeat) {
throw boost::leaf::exception(invalid_repitition("Invalid repitition"));
}
}

void run() {
auto _ = boost::leaf::on_error([this] { return e_argument_parser{*bottom_parser}; });
while (!at_end()) {
parse_another();
}
// Parsed everything successfully. Cool.
finalize();
}

void parse_another() {
auto given = current_arg();
auto did_parse = try_parse_given(given);
if (!did_parse) {
throw boost::leaf::exception(unrecognized_argument("Unrecognized argument"),
e_arg_spelling{std::string(given)});
}
}

bool try_parse_given(const strv given) {
if (given.size() < 2 || given[0] != '-') {
if (try_parse_positional(given)) {
return true;
}
return try_parse_subparser(given);
} else if (given[1] == '-') {
// Two hyphens is a long argument
return try_parse_long(given.substr(2), given);
} else {
// A single hyphen, shorthand argument(s)
return try_parse_short(given.substr(1), given);
}
}

/*
## ####### ## ## ######
## ## ## ### ## ## ##
## ## ## #### ## ##
## ## ## ## ## ## ## ####
## ## ## ## #### ## ##
## ## ## ## ### ## ##
######## ####### ## ## ######
*/

bool try_parse_long(strv tail, const strv given) {
if (tail == "help") {
throw boost::leaf::exception(help_request());
}
auto argset = bottom_parser;
while (argset) {
if (try_parse_long_1(*argset, tail, given)) {
return true;
}
argset = argset->parent().pointer();
}
return false;
}

bool try_parse_long_1(const argument_parser& argset, strv tail, const strv) {
for (const argument& cand : argset.arguments()) {
auto matched = cand.try_match_long(tail);
if (matched.empty()) {
continue;
}
tail.remove_prefix(matched.size());
shift();
auto long_arg = fmt::format("--{}", matched);
auto _ = boost::leaf::on_error(e_argument{cand}, e_arg_spelling{long_arg});
see(cand);
return dispatch_long(cand, tail, long_arg);
}

// None of the arguments matched
return false;
}

bool dispatch_long(const argument& arg, strv tail, strv given) {
if (arg.nargs == 0) {
if (!tail.empty()) {
// We should not have a value
throw boost::leaf::exception(invalid_arguments("Argument does not expect a value"),
e_wrong_val_num{1});
}
// Just a switch. Dispatch
arg.action(given, given);
return true;
}
// We expect at least one value
neo_assert(invariant,
tail.empty() || tail[0] == '=',
"Invalid argparsing state",
tail,
given);
if (!tail.empty()) {
// Given with an '=', as in: '--long-option=value'
tail.remove_prefix(1);
// The remainder is a single value
if (arg.nargs > 1) {
throw boost::leaf::exception(invalid_arguments("Invalid number of values"),
e_wrong_val_num{1});
}
arg.action(tail, given);
} else {
// Trailing words are arguments
for (auto i = 0; i < arg.nargs; ++i) {
if (at_end()) {
throw boost::leaf::exception(invalid_arguments(
"Invalid number of argument values"),
e_wrong_val_num{i});
}
arg.action(current_arg(), given);
shift();
}
}
return true;
}

/*
###### ## ## ####### ######## ########
## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ##
###### ######### ## ## ######## ##
## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ##
###### ## ## ####### ## ## ##
*/

bool try_parse_short(strv tail, const strv given) {
if (tail == "h") {
throw boost::leaf::exception(help_request());
}
auto argset = bottom_parser;
while (argset) {
auto new_tail = try_parse_short_1(*argset, tail, given);
if (new_tail == tail) {
// No characters were consumed...
argset = argset->parent().pointer();
} else {
// Got one argument. Re-seek back to the bottom-most active parser
argset = bottom_parser;
tail = new_tail;
}
if (tail.empty()) {
// We parsed the full group
return true;
}
}
// Did not match anything...
return false;
}

strv try_parse_short_1(const argument_parser& argset, const strv tail, const strv) {
for (const argument& cand : argset.arguments()) {
auto matched = cand.try_match_short(tail);
if (matched.empty()) {
continue;
}
auto short_tail = tail.substr(matched.size());
std::string short_arg = fmt::format("-{}", matched);
auto _ = boost::leaf::on_error(e_argument{cand}, e_arg_spelling{short_arg});
see(cand);
return dispatch_short(cand, short_tail, short_arg);
}
// Didn't match anything. Return the original group unmodified
return tail;
}

strv dispatch_short(const argument& arg, strv tail, strv spelling) {
if (!arg.nargs) {
// Just a switch. Consume a single character
arg.action("", spelling);
return tail;
} else if (arg.nargs == 1) {
// Want one value
if (tail.empty()) {
// The next argument is the value
shift();
if (at_end()) {
throw boost::leaf::exception(invalid_arguments("Expected a value"));
}
arg.action(current_arg(), spelling);
shift();
// We consumed the whole group, so return empty as the remaining:
return "";
} else {
// Consume the remainder of the argument as the value
arg.action(tail, spelling);
shift();
return "";
}
} else {
// Consume the next arguments
if (!tail.empty()) {
throw boost::leaf::exception(invalid_arguments(
"Wrong number of argument values given"),
e_wrong_val_num{1});
}
shift();
for (auto i = 0; i < arg.nargs; ++i) {
if (at_end()) {
throw boost::leaf::exception(invalid_arguments(
"Wrong number of argument values"),
e_wrong_val_num{i});
}
arg.action(current_arg(), spelling);
shift();
}
return "";
}
}

/*
######## ####### ###### #### ######## #### ####### ## ## ### ##
## ## ## ## ## ## ## ## ## ## ## ### ## ## ## ##
## ## ## ## ## ## ## ## ## ## #### ## ## ## ##
######## ## ## ###### ## ## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## #### ######### ##
## ## ## ## ## ## ## ## ## ## ## ### ## ## ##
## ####### ###### #### ## #### ####### ## ## ## ## ########
*/

bool try_parse_positional(strv given) {
int pos_idx = 0;
for (auto& arg : bottom_parser->arguments()) {
if (!arg.is_positional()) {
continue;
}

if (pos_idx != this->positional_index) {
// Not yet
++pos_idx;
continue;
}
// We've found the next one that needs a value
neo_assert(expects,
arg.nargs == 1,
"Positional arguments must have their nargs=1. For more than one "
"positional, use multiple positional arguments objects.",
arg.nargs,
given,
positional_index);
// Just invoke the action
auto _ = boost::leaf::on_error(e_arg_spelling{arg.preferred_spelling()});
see(arg);
arg.action(given, given);
if (!arg.can_repeat) {
// This argument isn't repeatable. Advance past it
++this->positional_index;
// If an arg is repeatable, it will always be the "next positional" to parse,
// and subsequent positionals are inherently unreachable.
}
shift();
return true;
}
// No one accepted the value. We do not follow the chain of subcommands for positionals
return false;
}

/*
###### ## ## ######## ######## ### ######## ###### ######## ########
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
###### ## ## ######## ######## ## ## ######## ###### ###### ########
## ## ## ## ## ## ######### ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
###### ####### ######## ## ## ## ## ## ###### ######## ## ##
*/

bool try_parse_subparser(const strv given) {
if (!bottom_parser->subparsers()) {
return false;
}
auto& group = *bottom_parser->subparsers();
for (auto& cand : group._p_subparsers) {
if (cand.name == given) {
// This parser is now the bottom of the chain
if (group.action) {
group.action(given, group.valname);
}
if (cand.action) {
cand.action();
}
bottom_parser = &cand._p_parser;
positional_index = 0;
shift();
return true;
}
}
return false;
}

/*
######## #### ## ## ### ## #### ######## ########
## ## ### ## ## ## ## ## ## ##
## ## #### ## ## ## ## ## ## ##
###### ## ## ## ## ## ## ## ## ## ######
## ## ## #### ######### ## ## ## ##
## ## ## ### ## ## ## ## ## ##
## #### ## ## ## ## ######## #### ######## ########
*/

void finalize() {
auto argset = bottom_parser;
while (argset) {
finalize(*argset);
argset = argset->parent().pointer();
}
if (bottom_parser->subparsers() && bottom_parser->subparsers()->required) {
throw boost::leaf::exception(missing_required("Expected a subcommand"));
}
}

void finalize(const argument_parser& argset) {
for (auto& arg : argset.arguments()) {
if (arg.required && !seen.contains(&arg)) {
throw boost::leaf::exception(missing_required("Required argument is missing"),
e_argument{arg});
}
}
}
};

} // namespace

void debate::detail::parser_state::run(const argument_parser& bottom) {
parse_engine{*this, &bottom}.run();
}

argument& argument_parser::add_argument(argument arg) noexcept {
_arguments.push_back(std::move(arg));
return _arguments.back();
}

subparser_group& argument_parser::add_subparsers(subparser_group grp) noexcept {
_subparsers.emplace(std::move(grp));
_subparsers->_p_parent_ = this;
return *_subparsers;
}

argument_parser& subparser_group::add_parser(subparser sub) {
_p_subparsers.push_back(std::move(sub));
auto& p = _p_subparsers.back()._p_parser;
p._parent = _p_parent_;
return p;
}

std::string argument_parser::usage_string(std::string_view progname) const noexcept {
std::string subcommand_suffix;
auto tail_parser = this;
while (tail_parser) {
for (auto& arg : tail_parser->arguments()) {
if (arg.is_positional() && arg.required && tail_parser != this) {
subcommand_suffix = " " + arg.preferred_spelling() + subcommand_suffix;
}
}
if (!tail_parser->_name.empty()) {
subcommand_suffix = " " + tail_parser->_name + subcommand_suffix;
}
tail_parser = tail_parser->_parent.pointer();
}
auto ret = fmt::format("Usage: {}{}", progname, subcommand_suffix);
auto indent = ret.size() + 1;
if (indent > 40) {
ret.push_back('\n');
indent = 10;
ret.append(indent, ' ');
}

std::size_t col = indent;
for (auto& arg : _arguments) {
auto synstr = arg.syntax_string();
if (col + synstr.size() > 79 && col > indent) {
ret.append("\n");
ret.append(indent - 1, ' ');
col = indent - 1;
}
ret.append(" " + synstr);
col += synstr.size() + 1;
}

if (subparsers()) {
std::string subcommand_str = " {";
auto& subs = subparsers()->_p_subparsers;
for (auto it = subs.cbegin(); it != subs.cend();) {
subcommand_str.append(it->name);
++it;
if (it != subs.cend()) {
subcommand_str.append(",");
}
}
subcommand_str.append("}");
if (col + subcommand_str.size() > 79 && col > indent) {
ret.append("\n");
ret.append(indent - 1, ' ');
}
ret.append(subcommand_str);
}
return ret;
}

std::string argument_parser::help_string(std::string_view progname) const noexcept {
std::string ret;
ret = usage_string(progname);
ret.append("\n\n");
if (!_description.empty()) {
ret.append(_description);
ret.append("\n\n");
}
bool any_required = false;
for (auto& arg : arguments()) {
if (!arg.required) {
continue;
}
if (!any_required) {
ret.append("required arguments:\n\n");
}
any_required = true;
ret.append(arg.help_string());
ret.append("\n");
}
bool any_non_required = false;
for (auto& arg : arguments()) {
if (arg.required) {
continue;
}
if (!any_non_required) {
ret.append("optional arguments:\n\n");
}
any_non_required = true;
ret.append(arg.help_string());
ret.append("\n");
}

if (subparsers()) {
ret.append("Subcommands:\n\n");
if (!subparsers()->description.empty()) {
ret.append(fmt::format(" {}\n\n", subparsers()->description));
}
for (auto& sub : subparsers()->_p_subparsers) {
ret.append(fmt::format(fmt::emphasis::bold, "{}", sub.name));
ret.append("\n ");
ret.append(sub.help);
ret.append("\n\n");
}
}
return ret;
}

+ 130
- 0
src/debate/argument_parser.hpp Целия файл

@@ -0,0 +1,130 @@
#pragma once

#include "./argument.hpp"

#include <fmt/format.h>
#include <neo/assert.hpp>
#include <neo/opt_ref.hpp>

#include <cassert>
#include <exception>
#include <functional>
#include <list>

namespace debate {

class argument_parser;

namespace detail {

struct parser_state {
void run(const argument_parser& bottom_parser);

virtual std::string_view current_arg() const noexcept = 0;
virtual bool at_end() const noexcept = 0;
virtual void shift() noexcept = 0;
};

template <typename Iter, typename Stop>
struct parser_state_impl : parser_state {
Iter arg_it;
Stop arg_stop;

parser_state_impl(Iter it, Stop st)
: arg_it(it)
, arg_stop(st) {}

bool at_end() const noexcept override { return arg_it == arg_stop; }
std::string_view current_arg() const noexcept override {
neo_assert(invariant, !at_end(), "Get argument past the final argumetn?");
return *arg_it;
}
void shift() noexcept override {
neo_assert(invariant, !at_end(), "Advancing argv parser past the end.");
++arg_it;
}
};

} // namespace detail

struct subparser;

struct subparser_group {
std::string valname = "<subcommand>";

std::string description{};

bool required = true;

std::function<void(std::string_view, std::string_view)> action{};

const argument_parser* _p_parent_ = nullptr;
std::list<subparser> _p_subparsers{};

argument_parser& add_parser(subparser);
};

class argument_parser {
friend struct subparser_group;
std::list<argument> _arguments;
std::optional<subparser_group> _subparsers;
std::string _name;
std::string _description;
// The parent of this argumetn parser, if it was attached using a subparser_group
neo::opt_ref<const argument_parser> _parent;

using strv = std::string_view;
using str_iter = strv::iterator;

template <typename R>
void _parse_argv(R&& range) const {
auto arg_it = std::cbegin(range);
auto arg_stop = std::cend(range);
// Instantiate a complete parser, and go!
detail::parser_state_impl state{arg_it, arg_stop};
state.run(*this);
}

public:
argument_parser() = default;

explicit argument_parser(std::string description)
: _description(std::move(description)) {}

explicit argument_parser(std::string name, std::string description)
: _name(std::move(name))
, _description(std::move(description)) {}

argument& add_argument(argument arg) noexcept;

subparser_group& add_subparsers(subparser_group grp = {}) noexcept;

std::string usage_string(std::string_view progname) const noexcept;

std::string help_string(std::string_view progname) const noexcept;

template <typename T>
void parse_argv(T&& range) const {
return _parse_argv(range);
}

template <typename T>
void parse_argv(std::initializer_list<T> ilist) const {
return _parse_argv(ilist);
}

auto parent() const noexcept { return _parent; }
auto& arguments() const noexcept { return _arguments; }
auto& subparsers() const noexcept { return _subparsers; }
};

struct subparser {
std::string name;
std::string help;

std::function<void()> action{};

argument_parser _p_parser{name, help};
};

} // namespace debate

+ 85
- 0
src/debate/argument_parser.test.cpp Целия файл

@@ -0,0 +1,85 @@
#include "./debate.hpp"

#include "./enum.hpp"

#include <catch2/catch.hpp>

TEST_CASE("Create an argument parser") {
enum log_level {
_invalid,
info,
warning,
error,
};
log_level level;

std::string file;

debate::argument_parser parser;
parser.add_argument(debate::argument{
.long_spellings = {"log-level"},
.short_spellings = {"l"},
.help = "Set the log level",
.valname = "<level>",
.action = debate::put_into(level),
});
parser.add_argument(debate::argument{
.help = "A file to read",
.valname = "<file>",
.action = debate::put_into(file),
});
parser.parse_argv({"--log-level=info"});
CHECK(level == log_level::info);
parser.parse_argv({"--log-level=warning"});
CHECK(level == log_level::warning);
parser.parse_argv({"--log-level", "info"});
CHECK(level == log_level::info);
parser.parse_argv({"-lerror"});
CHECK(level == log_level::error);
CHECK_THROWS_AS(parser.parse_argv({"-lerror", "--log-level=info"}), std::runtime_error);

parser.parse_argv({"-l", "info"});
CHECK(level == log_level::info);

parser.parse_argv({"-lwarning", "my-file.txt"});
CHECK(level == log_level::warning);
CHECK(file == "my-file.txt");
}

TEST_CASE("Subcommands") {
std::optional<bool> do_eat;
std::optional<bool> scramble_eggs;
std::string_view subcommand;

debate::argument_parser parser;
parser.add_argument({
.long_spellings = {"eat"},
.nargs = 0,
.action = debate::store_true(do_eat),
});

auto& sub = parser.add_subparsers(debate::subparser_group{.valname = "<food>"});
auto& egg_parser
= sub.add_parser(debate::subparser{.name = "egg",
.help = "It's an egg",
.action = debate::store_value(subcommand, "egg")});
egg_parser.add_argument(
{.long_spellings = {"scramble"}, .nargs = 0, .action = debate::store_true(scramble_eggs)});

parser.parse_argv({"egg"});
parser.parse_argv({"--eat", "egg"});
// Missing the subcommand:
CHECK_THROWS_AS(parser.parse_argv({"--eat"}), std::runtime_error);
CHECK_FALSE(scramble_eggs);
parser.parse_argv({"egg", "--scramble"});
CHECK(scramble_eggs);
CHECK(subcommand == "egg");

do_eat.reset();
scramble_eggs.reset();
subcommand = {};
parser.parse_argv({"egg", "--scramble", "--eat"});
CHECK(do_eat);
CHECK(scramble_eggs);
CHECK(subcommand == "egg");
}

+ 3
- 0
src/debate/debate.hpp Целия файл

@@ -0,0 +1,3 @@
#pragma once

#include "./argument_parser.hpp"

+ 50
- 0
src/debate/enum.hpp Целия файл

@@ -0,0 +1,50 @@
#pragma once

#include "./argument_parser.hpp"
#include "./error.hpp"

#include <boost/leaf/error.hpp>
#include <boost/leaf/exception.hpp>
#include <fmt/core.h>
#include <magic_enum.hpp>

#include <type_traits>

namespace debate {

template <typename E>
class enum_putter {
E* _dest;

public:
constexpr explicit enum_putter(E& e)
: _dest(&e) {}

void operator()(std::string_view given, std::string_view full_arg) const {
std::optional<std::string> normalized_str;
std::string_view normalized_view = given;
if (given.find('-') != given.npos) {
// We should normalize it
normalized_str.emplace(given);
for (char& c : *normalized_str) {
c = c == '-' ? '_' : c;
}
normalized_view = *normalized_str;
}
auto val = magic_enum::enum_cast<E>(normalized_view);
if (!val) {
throw boost::leaf::
exception(invalid_arguments("Invalid argument value given for enum-bound argument"),
e_invalid_arg_value{std::string(given)},
e_arg_spelling{std::string(full_arg)});
}
*_dest = *val;
}
};

template <typename E>
constexpr auto make_enum_putter(E& dest) noexcept {
return enum_putter<E>(dest);
}

} // namespace debate

+ 55
- 0
src/debate/error.hpp Целия файл

@@ -0,0 +1,55 @@
#pragma once

#include <exception>
#include <stdexcept>
#include <string>

namespace debate {

class argument;
class argument_parser;
class subparser;

struct help_request : std::exception {};

struct invalid_arguments : std::runtime_error {
using runtime_error::runtime_error;
};

struct unrecognized_argument : invalid_arguments {
using invalid_arguments::invalid_arguments;
};

struct missing_required : invalid_arguments {
using invalid_arguments::invalid_arguments;
};

struct invalid_repitition : invalid_arguments {
using invalid_arguments::invalid_arguments;
};

struct e_argument {
const debate::argument& argument;
};

struct e_argument_parser {
const debate::argument_parser& parser;
};

struct e_invalid_arg_value {
std::string given;
};

struct e_wrong_val_num {
int n_given;
};

struct e_arg_spelling {
std::string spelling;
};

struct e_did_you_mean {
std::string candidate;
};

} // namespace debate

Loading…
Отказ
Запис