Browse Source

Provide did-you-mean for command-line arguments

default_compile_flags
vector-of-bool 3 years ago
parent
commit
8d8853cb95
2 changed files with 79 additions and 26 deletions
  1. +18
    -4
      src/dds.main.cpp
  2. +61
    -22
      src/debate/argument_parser.cpp

+ 18
- 4
src/dds.main.cpp View File

#include <debate/enum.hpp> #include <debate/enum.hpp>


#include <boost/leaf/handle_exception.hpp> #include <boost/leaf/handle_exception.hpp>
#include <fansi/styled.hpp>
#include <fmt/ostream.h> #include <fmt/ostream.h>
#include <neo/event.hpp> #include <neo/event.hpp>


#include <iostream> #include <iostream>
#include <locale> #include <locale>


using namespace fansi::literals;

static void load_locale() { static void load_locale() {
auto lang = dds::getenv("LANG"); auto lang = dds::getenv("LANG");
if (!lang) { if (!lang) {
}, },
[&](debate::unrecognized_argument, [&](debate::unrecognized_argument,
debate::e_argument_parser p, debate::e_argument_parser p,
debate::e_arg_spelling arg) {
debate::e_arg_spelling arg,
debate::e_did_you_mean* dym) {
std::cerr << p.parser.usage_string(program_name) << '\n'; std::cerr << p.parser.usage_string(program_name) << '\n';
if (p.parser.subparsers()) { if (p.parser.subparsers()) {
fmt::print(std::cerr, "Unrecognized argument/subcommand: \"{}\"\n", arg.spelling);
fmt::print(std::cerr,
"Unrecognized argument/subcommand: \".bold.red[{}]\"\n"_styled,
arg.spelling);
} else { } else {
fmt::print(std::cerr, "Unrecognized argument: \"{}\"\n", arg.spelling);
fmt::print(std::cerr,
"Unrecognized argument: \".bold.red[{}]\"\n"_styled,
arg.spelling);
}
if (dym) {
fmt::print(std::cerr,
" (Did you mean '.br.yellow[{}]'?)\n"_styled,
dym->candidate);
} }
return 2; return 2;
}, },
return 2; return 2;
}, },
[&](debate::invalid_repitition, debate::e_argument_parser p, debate::e_arg_spelling sp) { [&](debate::invalid_repitition, debate::e_argument_parser p, debate::e_arg_spelling sp) {
fmt::print(std::cerr << "{}\nArgument '{}' cannot be provided more than once\n",
fmt::print(std::cerr,
"{}\nArgument '{}' cannot be provided more than once\n",
p.parser.usage_string(program_name), p.parser.usage_string(program_name),
sp.spelling); sp.spelling);
return 2; return 2;

+ 61
- 22
src/debate/argument_parser.cpp View File

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


/// XXX: Refactor this after pulling debate:: out of dds
#include <dds/dym.hpp>

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


#include <fmt/color.h> #include <fmt/color.h>
#include <fmt/format.h> #include <fmt/format.h>
#include <neo/scope.hpp>


#include <set> #include <set>


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


finalize(); finalize();
} }


std::optional<std::string> find_nearest_arg_spelling(std::string_view given) const noexcept {
std::vector<std::string> candidates;
// Only match arguments of the corrent type
auto given_long = given.starts_with("--");
auto has_dash = given.starts_with("-");
auto parser = bottom_parser;
while (parser) {
for (auto& arg : parser->arguments()) {
if (given_long) {
for (auto& l : arg.long_spellings) {
candidates.push_back("--" + l);
}
}
if (has_dash) {
for (auto& s : arg.short_spellings) {
candidates.push_back("-" + s);
}
}
if (!has_dash && parser->subparsers()) {
auto&& grp = *parser->subparsers();
for (auto& p : grp._p_subparsers) {
candidates.push_back(p.name);
}
}
}
parser = parser->parent().pointer();
}
return dds::did_you_mean(given, candidates);
}

void parse_another() { void parse_another() {
auto given = current_arg(); auto given = current_arg();
auto did_parse = try_parse_given(given); auto did_parse = try_parse_given(given);
if (!did_parse) { if (!did_parse) {
throw boost::leaf::exception(unrecognized_argument("Unrecognized argument"),
e_arg_spelling{std::string(given)});
neo_defer {
auto dym = find_nearest_arg_spelling(given);
if (dym) {
boost::leaf::current_error().load(e_did_you_mean{*dym});
}
};
BOOST_LEAF_THROW_EXCEPTION(unrecognized_argument("Unrecognized argument"),
e_arg_spelling{std::string(given)});
} }
} }




bool try_parse_long(strv tail, const strv given) { bool try_parse_long(strv tail, const strv given) {
if (tail == "help") { if (tail == "help") {
throw boost::leaf::exception(help_request());
BOOST_LEAF_THROW_EXCEPTION(help_request());
} }
auto argset = bottom_parser; auto argset = bottom_parser;
while (argset) { while (argset) {
if (arg.nargs == 0) { if (arg.nargs == 0) {
if (!tail.empty()) { if (!tail.empty()) {
// We should not have a value // We should not have a value
throw boost::leaf::exception(invalid_arguments("Argument does not expect a value"),
e_wrong_val_num{1});
BOOST_LEAF_THROW_EXCEPTION(invalid_arguments("Argument does not expect a value"),
e_wrong_val_num{1});
} }
// Just a switch. Dispatch // Just a switch. Dispatch
arg.action(given, given); arg.action(given, given);
tail.remove_prefix(1); tail.remove_prefix(1);
// The remainder is a single value // The remainder is a single value
if (arg.nargs > 1) { if (arg.nargs > 1) {
throw boost::leaf::exception(invalid_arguments("Invalid number of values"),
e_wrong_val_num{1});
BOOST_LEAF_THROW_EXCEPTION(invalid_arguments("Invalid number of values"),
e_wrong_val_num{1});
} }
arg.action(tail, given); arg.action(tail, given);
} else { } else {
// Trailing words are arguments // Trailing words are arguments
for (auto i = 0; i < arg.nargs; ++i) { for (auto i = 0; i < arg.nargs; ++i) {
if (at_end()) { if (at_end()) {
throw boost::leaf::exception(invalid_arguments(
"Invalid number of argument values"),
e_wrong_val_num{i});
BOOST_LEAF_THROW_EXCEPTION(invalid_arguments(
"Invalid number of argument values"),
e_wrong_val_num{i});
} }
arg.action(current_arg(), given); arg.action(current_arg(), given);
shift(); shift();


bool try_parse_short(strv tail, const strv given) { bool try_parse_short(strv tail, const strv given) {
if (tail == "h") { if (tail == "h") {
throw boost::leaf::exception(help_request());
BOOST_LEAF_THROW_EXCEPTION(help_request());
} }
auto argset = bottom_parser; auto argset = bottom_parser;
while (argset) { while (argset) {
// The next argument is the value // The next argument is the value
shift(); shift();
if (at_end()) { if (at_end()) {
throw boost::leaf::exception(invalid_arguments("Expected a value"));
BOOST_LEAF_THROW_EXCEPTION(invalid_arguments("Expected a value"));
} }
arg.action(current_arg(), spelling); arg.action(current_arg(), spelling);
shift(); shift();
} else { } else {
// Consume the next arguments // Consume the next arguments
if (!tail.empty()) { if (!tail.empty()) {
throw boost::leaf::exception(invalid_arguments(
"Wrong number of argument values given"),
e_wrong_val_num{1});
BOOST_LEAF_THROW_EXCEPTION(invalid_arguments(
"Wrong number of argument values given"),
e_wrong_val_num{1});
} }
shift(); shift();
for (auto i = 0; i < arg.nargs; ++i) { for (auto i = 0; i < arg.nargs; ++i) {
if (at_end()) { if (at_end()) {
throw boost::leaf::exception(invalid_arguments(
"Wrong number of argument values"),
e_wrong_val_num{i});
BOOST_LEAF_THROW_EXCEPTION(invalid_arguments("Wrong number of argument values"),
e_wrong_val_num{i});
} }
arg.action(current_arg(), spelling); arg.action(current_arg(), spelling);
shift(); shift();
argset = argset->parent().pointer(); argset = argset->parent().pointer();
} }
if (bottom_parser->subparsers() && bottom_parser->subparsers()->required) { if (bottom_parser->subparsers() && bottom_parser->subparsers()->required) {
throw boost::leaf::exception(missing_required("Expected a subcommand"));
BOOST_LEAF_THROW_EXCEPTION(missing_required("Expected a subcommand"));
} }
} }


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

Loading…
Cancel
Save