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