diff --git a/include/argparse/argparse.hpp b/include/argparse/argparse.hpp index 00d4ad7..37c8cf5 100644 --- a/include/argparse/argparse.hpp +++ b/include/argparse/argparse.hpp @@ -452,6 +452,46 @@ template struct IsChoiceTypeSupported { std::is_same::value; }; +template +int get_levenshtein_distance(const StringType &s1, const StringType &s2) { + std::vector> dp(s1.size() + 1, + std::vector(s2.size() + 1, 0)); + + for (int i = 0; i <= s1.size(); ++i) { + for (int j = 0; j <= s2.size(); ++j) { + if (i == 0) { + dp[i][j] = j; + } else if (j == 0) { + dp[i][j] = i; + } else if (s1[i - 1] == s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + std::min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}); + } + } + } + + return dp[s1.size()][s2.size()]; +} + +template +std::string_view +get_most_similar_string(const std::map &map, + const std::string_view input) { + std::string_view most_similar{}; + int min_distance = std::numeric_limits::max(); + + for (const auto &entry : map) { + int distance = get_levenshtein_distance(entry.first, input); + if (distance < min_distance) { + min_distance = distance; + most_similar = entry.first; + } + } + + return most_similar; +} + } // namespace details enum class nargs_pattern { optional, any, at_least_one }; @@ -1804,6 +1844,15 @@ private: if (m_positional_arguments.empty()) { + /// Check sub-parsers first + if (!m_subparser_map.empty()) { + throw std::runtime_error( + "Failed to parse '" + current_argument + "', did you mean '" + + std::string{details::get_most_similar_string( + m_subparser_map, current_argument)} + + "'"); + } + if (!m_optional_arguments.empty()) { bool not_help_or_version{true}; for (const auto &opt : m_optional_arguments) { diff --git a/test/test_error_reporting.cpp b/test/test_error_reporting.cpp index 302c47e..9365756 100644 --- a/test/test_error_reporting.cpp +++ b/test/test_error_reporting.cpp @@ -66,4 +66,32 @@ TEST_CASE("Missing optional argument name with other positional arguments" * "Maximum number of positional arguments exceeded, failed to parse '3'", std::runtime_error); } +} + +TEST_CASE("Detect unknown subcommand" * test_suite("error_reporting")) { + argparse::ArgumentParser program("git"); + argparse::ArgumentParser log_command("log"); + argparse::ArgumentParser notes_command("notes"); + argparse::ArgumentParser add_command("add"); + program.add_subparser(log_command); + program.add_subparser(notes_command); + program.add_subparser(add_command); + + SUBCASE("Typo for 'notes'") { + REQUIRE_THROWS_WITH_AS(program.parse_args({"git", "tote"}), + "Failed to parse 'tote', did you mean 'notes'", + std::runtime_error); + } + + SUBCASE("Typo for 'add'") { + REQUIRE_THROWS_WITH_AS(program.parse_args({"git", "bad"}), + "Failed to parse 'bad', did you mean 'add'", + std::runtime_error); + } + + SUBCASE("Typo for 'log'") { + REQUIRE_THROWS_WITH_AS(program.parse_args({"git", "logic"}), + "Failed to parse 'logic', did you mean 'log'", + std::runtime_error); + } } \ No newline at end of file