From 433ee18d0c62c7b3701c1f51a7bf288b793d060f Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 2 May 2025 07:46:33 -0700 Subject: [PATCH] update documentation and tests on prefix matching and clean up close match function. --- README.md | 2 + book/chapters/subcommands.md | 6 +++ examples/CMakeLists.txt | 13 ++++++ examples/close_match.cpp | 84 +++++++++++++++++++++--------------- 4 files changed, 70 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 586d5c0f..f2f0ff17 100644 --- a/README.md +++ b/README.md @@ -914,6 +914,8 @@ option_groups. These are: is not allowed to have a single character short option starting with the same character as a single dash long form name; for example, `-s` and `-single` are not allowed in the same application. +- `.allow_subcommand_prefix_matching()`:🚧 If this modifier is enabled, unambiguious prefix portions of a subcommand will match. + For example `upgrade_package` would match on `upgrade_`, `upg`, `u` as long as no other subcommand would also match. It also disallows subcommand names that are full prefixes of another subcommand. - `.fallthrough()`: Allow extra unmatched options and positionals to "fall through" and be matched on a parent option. Subcommands by default are allowed to "fall through" as in they will first attempt to match on the current diff --git a/book/chapters/subcommands.md b/book/chapters/subcommands.md index 6ab331a2..7499e5a0 100644 --- a/book/chapters/subcommands.md +++ b/book/chapters/subcommands.md @@ -105,6 +105,7 @@ at the point the subcommand is created: - Fallthrough - Group name - Max required subcommands +- prefix_matching - validate positional arguments - validate optional arguments @@ -156,6 +157,11 @@ ignored, even if they could match. Git is the traditional example for prefix commands; if you run git with an unknown subcommand, like "`git thing`", it then calls another command called "`git-thing`" with the remaining options intact. +### prefix matching + +A modifier is available for subcommand matching, `->allow_subcommand_prefix_matching()`. if this is enabled unambiguious prefix portions of a subcommand will match. +For Example `upgrade_package` would match on `upgrade_`, `upg`, `u` as long as no other subcommand would also match. It also disallows subcommand names that are full prefixes of another subcommand. + ### Silent subcommands Subcommands can be modified by using the `silent` option. This will prevent the diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index eef7f782..ecaa334f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -252,6 +252,19 @@ set_property(TEST retired_deprecated PROPERTY PASS_REGULAR_EXPRESSION "deprecate add_cli_exe(close_match close_match.cpp) +add_test(NAME close_match_test COMMAND close_match i) +add_test(NAME close_match_test2 COMMAND close_match upg) +add_test(NAME close_match_test3 COMMAND close_match rem) +add_test(NAME close_match_test4 COMMAND close_match upgrde) + +set_property(TEST close_match_test PROPERTY PASS_REGULAR_EXPRESSION "install") + +set_property(TEST close_match_test2 PROPERTY PASS_REGULAR_EXPRESSION "upgrade") + +set_property(TEST close_match_test3 PROPERTY PASS_REGULAR_EXPRESSION "remove") + +set_property(TEST close_match_test4 PROPERTY PASS_REGULAR_EXPRESSION "closest match is upgrade") + #-------------------------------------------- add_cli_exe(custom_parse custom_parse.cpp) add_test(NAME cp_test COMMAND custom_parse --dv 1.7) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 6310cd0e..0df99498 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -12,29 +12,33 @@ #include #include #include +#include -// Levenshtein distance function code generated by chatgpt -std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { - size_t len1 = s1.size(), len2 = s2.size(); - std::vector> dp(len1 + 1, std::vector(len2 + 1)); +// Levenshtein distance function code generated by chatgpt/copilot +std::size_t levenshteinDistance(const std::string& s1, const std::string& s2) { + std::size_t len1 = s1.size(), len2 = s2.size(); + if (len1 == 0) return len2; + if (len2 == 0) return len1; - for(size_t ii = 0; ii <= len1; ++ii) - dp[ii][0] = ii; - for(size_t jj = 0; jj <= len2; ++jj) - dp[0][jj] = jj; + std::vector prev(len2 + 1), curr(len2 + 1); + std::iota(prev.begin(), prev.end(), 0); // Fill prev with {0, 1, ..., len2} - for(size_t ii = 1; ii <= len1; ++ii) { - for(size_t jj = 1; jj <= len2; ++jj) { + for (std::size_t ii = 1; ii <= len1; ++ii) { + curr[0] = ii; + for (std::size_t jj = 1; jj <= len2; ++jj) { + // If characters match, no substitution cost; otherwise, cost is 1. std::size_t cost = (s1[ii - 1] == s2[jj - 1]) ? 0 : 1; - dp[ii][jj] = (std::min)({ - dp[ii - 1][jj] + 1, // deletion - dp[ii][jj - 1] + 1, // insertion - dp[ii - 1][jj - 1] + cost // substitution - }); - } - } - return dp[len1][len2]; + // Compute the minimum cost between: + // - Deleting a character from `s1` (prev[jj] + 1) + // - Inserting a character into `s1` (curr[jj - 1] + 1) + // - Substituting a character (prev[jj - 1] + cost) + + curr[jj] = std::min({ prev[jj] + 1, curr[jj - 1] + 1, prev[jj - 1] + cost }); + } + prev = std::exchange(curr, prev); // Swap vectors efficiently + } + return prev[len2]; } // Finds the closest string from a list (modified from chat gpt code) @@ -53,30 +57,33 @@ std::pair findClosestMatch(const std::string &input, return {closest, minDistance}; } -void addCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3) { +void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3) { + //if extras are not allowed then there will be no remaining app->allow_extras(true); - - app->parse_complete_callback([&app, minDistance]() { + // generate a list of subcommand names + auto subs = app->get_subcommands(nullptr); + std::vector list; + for(const auto *sub : subs) { + if(!sub->get_name().empty()) { + list.push_back(sub->get_name()); + } + auto aliases = sub->get_aliases(); + if(!aliases.empty()) { + list.insert(list.end(), aliases.begin(), aliases.end()); + } + } + // add a callback that runs before a final callback and loops over the remaining arguments for subcommands + app->parse_complete_callback([&app, minDistance,list]() { auto extras = app->remaining(); if(extras.empty()) { return; } - auto subs = app->get_subcommands(nullptr); - std::vector list; - for(const auto *sub : subs) { - if(!sub->get_name().empty()) { - list.push_back(sub->get_name()); - } - auto aliases = sub->get_aliases(); - if(!aliases.empty()) { - list.insert(list.end(), aliases.begin(), aliases.end()); - } - } + for(auto &extra : extras) { if(extra.front() != '-') { auto closest = findClosestMatch(extra, list); if(closest.second <= minDistance) { - std::cout << "unmatched commands " << extra << ", closest match is " << closest.first << "\n"; + std::cout << "unmatched command \"" << extra << "\", closest match is " << closest.first << "\n"; } } } @@ -86,7 +93,7 @@ void addCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3) { /** This example demonstrates the use of close match detection to detect invalid commands that are close matches to * existing ones */ -int main(int argc, const char *argv[]) { +int main(int argc, const char* argv[]) { int value{0}; CLI::App app{"cose string App"}; @@ -98,7 +105,14 @@ int main(int argc, const char *argv[]) { app.add_subcommand("upgrade", ""); app.add_subcommand("remove", ""); app.add_subcommand("test", ""); - addCloseMatchDetection(&app, 5); + //enable close matching for subcommands + addSubcommandCloseMatchDetection(&app, 5); CLI11_PARSE(app, argc, argv); + + auto subs=app.get_subcommands(); + for (const auto& sub : subs) + { + std::cout<get_name()<<"\n"; + } return 0; }