| @@ -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 | |||