@@ -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", | |||
] | |||
} |
@@ -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" | |||
} |
@@ -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; | |||
} |
@@ -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 |
@@ -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; | |||
} |
@@ -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 |
@@ -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"); | |||
} |
@@ -0,0 +1,3 @@ | |||
#pragma once | |||
#include "./argument_parser.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 |
@@ -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 |