// Explicit zlib link is required due to linker input order bug. | // Explicit zlib link is required due to linker input order bug. | ||||
// Can be removed after alpha.5 | // Can be removed after alpha.5 | ||||
"zlib/zlib", | "zlib/zlib", | ||||
"neo/compress" | |||||
"neo/compress", | |||||
"neargye/magic_enum", | |||||
] | ] | ||||
} | } |
"neo-http^0.1.0", | "neo-http^0.1.0", | ||||
"neo-io^0.1.1", | "neo-io^0.1.1", | ||||
"boost.leaf~0.3.0", | "boost.leaf~0.3.0", | ||||
"magic_enum+0.0.0", | |||||
], | ], | ||||
"test_driver": "Catch-Main" | "test_driver": "Catch-Main" | ||||
} | } |
#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; | |||||
} |
#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 |
#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; | |||||
} |
#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 |
#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"); | |||||
} |
#pragma once | |||||
#include "./argument_parser.hpp" |
#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 |
#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 |