1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-05-04 14:23:51 +00:00

update documentation and tests on prefix matching and clean up close match function.

This commit is contained in:
Philip Top 2025-05-02 07:46:33 -07:00
parent 279e710544
commit 433ee18d0c
4 changed files with 70 additions and 35 deletions

View File

@ -914,6 +914,8 @@ option_groups. These are:
is not allowed to have a single character short option starting with the same 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 character as a single dash long form name; for example, `-s` and `-single` are
not allowed in the same application. 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 - `.fallthrough()`: Allow extra unmatched options and positionals to "fall
through" and be matched on a parent option. Subcommands by default are allowed 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 to "fall through" as in they will first attempt to match on the current

View File

@ -105,6 +105,7 @@ at the point the subcommand is created:
- Fallthrough - Fallthrough
- Group name - Group name
- Max required subcommands - Max required subcommands
- prefix_matching
- validate positional arguments - validate positional arguments
- validate optional 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 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. 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 ### Silent subcommands
Subcommands can be modified by using the `silent` option. This will prevent the Subcommands can be modified by using the `silent` option. This will prevent the

View File

@ -252,6 +252,19 @@ set_property(TEST retired_deprecated PROPERTY PASS_REGULAR_EXPRESSION "deprecate
add_cli_exe(close_match close_match.cpp) 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_cli_exe(custom_parse custom_parse.cpp)
add_test(NAME cp_test COMMAND custom_parse --dv 1.7) add_test(NAME cp_test COMMAND custom_parse --dv 1.7)

View File

@ -12,29 +12,33 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <numeric>
// Levenshtein distance function code generated by chatgpt // Levenshtein distance function code generated by chatgpt/copilot
std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { std::size_t levenshteinDistance(const std::string& s1, const std::string& s2) {
size_t len1 = s1.size(), len2 = s2.size(); std::size_t len1 = s1.size(), len2 = s2.size();
std::vector<std::vector<std::size_t>> dp(len1 + 1, std::vector<std::size_t>(len2 + 1)); if (len1 == 0) return len2;
if (len2 == 0) return len1;
for(size_t ii = 0; ii <= len1; ++ii) std::vector<std::size_t> prev(len2 + 1), curr(len2 + 1);
dp[ii][0] = ii; std::iota(prev.begin(), prev.end(), 0); // Fill prev with {0, 1, ..., len2}
for(size_t jj = 0; jj <= len2; ++jj)
dp[0][jj] = jj;
for(size_t ii = 1; ii <= len1; ++ii) { for (std::size_t ii = 1; ii <= len1; ++ii) {
for(size_t jj = 1; jj <= len2; ++jj) { 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; 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) // Finds the closest string from a list (modified from chat gpt code)
@ -53,30 +57,33 @@ std::pair<std::string, std::size_t> findClosestMatch(const std::string &input,
return {closest, minDistance}; 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->allow_extras(true);
// generate a list of subcommand names
app->parse_complete_callback([&app, minDistance]() { auto subs = app->get_subcommands(nullptr);
std::vector<std::string> 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(); auto extras = app->remaining();
if(extras.empty()) { if(extras.empty()) {
return; return;
} }
auto subs = app->get_subcommands(nullptr);
std::vector<std::string> 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) { for(auto &extra : extras) {
if(extra.front() != '-') { if(extra.front() != '-') {
auto closest = findClosestMatch(extra, list); auto closest = findClosestMatch(extra, list);
if(closest.second <= minDistance) { 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 /** This example demonstrates the use of close match detection to detect invalid commands that are close matches to
* existing ones * existing ones
*/ */
int main(int argc, const char *argv[]) { int main(int argc, const char* argv[]) {
int value{0}; int value{0};
CLI::App app{"cose string App"}; CLI::App app{"cose string App"};
@ -98,7 +105,14 @@ int main(int argc, const char *argv[]) {
app.add_subcommand("upgrade", ""); app.add_subcommand("upgrade", "");
app.add_subcommand("remove", ""); app.add_subcommand("remove", "");
app.add_subcommand("test", ""); app.add_subcommand("test", "");
addCloseMatchDetection(&app, 5); //enable close matching for subcommands
addSubcommandCloseMatchDetection(&app, 5);
CLI11_PARSE(app, argc, argv); CLI11_PARSE(app, argc, argv);
auto subs=app.get_subcommands();
for (const auto& sub : subs)
{
std::cout<<sub->get_name()<<"\n";
}
return 0; return 0;
} }