From 0549f7c45034102d706d3644c3f5b8cdd4e89d34 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 21 Apr 2025 09:06:14 -0700 Subject: [PATCH 01/30] add an example of finding close matches --- examples/CMakeLists.txt | 2 + examples/close_match.cpp | 107 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 examples/close_match.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 07c8f64d7..eef7f7820 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -250,6 +250,8 @@ set_property(TEST retired_retired_test3 PROPERTY PASS_REGULAR_EXPRESSION "WARNIN set_property(TEST retired_deprecated PROPERTY PASS_REGULAR_EXPRESSION "deprecated.*not_deprecated") +add_cli_exe(close_match close_match.cpp) + #-------------------------------------------- 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 new file mode 100644 index 000000000..2eae5007b --- /dev/null +++ b/examples/close_match.cpp @@ -0,0 +1,107 @@ +// Copyright (c) 2017-2025, University of Cincinnati, developed by Henry Schreiner +// under NSF AWARD 1414736 and by the respective contributors. +// All rights reserved. +// +// SPDX-License-Identifier: BSD-3-Clause + +// Code inspired by discussion from https://github.com/CLIUtils/CLI11/issues/1149 + +#include +#include +#include +#include +#include + +// Levenshtein distance function code generated by chatgpt +int 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)); + + for (size_t i = 0; i <= len1; ++i) dp[i][0] = i; + for (size_t j = 0; j <= len2; ++j) dp[0][j] = j; + + for (size_t i = 1; i <= len1; ++i) { + for (size_t j = 1; j <= len2; ++j) { + int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; + dp[i][j] = std::min({ + dp[i - 1][j] + 1, // deletion + dp[i][j - 1] + 1, // insertion + dp[i - 1][j - 1] + cost // substitution + }); + } + } + + return dp[len1][len2]; +} + +// Finds the closest string from a list +std::pair findClosestMatch(const std::string& input, const std::vector& candidates) { + std::string closest; + int minDistance = std::numeric_limits::max(); + + for (const auto& candidate : candidates) { + int distance = levenshteinDistance(input, candidate); + if (distance < minDistance) { + minDistance = distance; + closest = candidate; + } + } + + return { closest,minDistance }; +} + +/** This example demonstrates the use of `prefix_command` on a subcommand +to capture all subsequent arguments along with an alias to make it appear as a regular options. + +All the values after the "sub" or "--sub" are available in the remaining() method. +*/ +int main(int argc, const char *argv[]) { + + int value{0}; + CLI::App app{"cose string App"}; + app.add_option("-v", value, "value"); + + app.add_subcommand("install", ""); + app.add_subcommand("upgrade",""); + app.add_subcommand("remove",""); + app.add_subcommand("test",""); + app.allow_extras(true); + + app.parse_complete_callback([&app]() { + 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 <= 3) + { + std::cout<<"unmatched commands "< Date: Mon, 21 Apr 2025 16:07:10 +0000 Subject: [PATCH 02/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 68 ++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 2eae5007b..403f808a1 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -8,26 +8,28 @@ #include #include +#include #include #include -#include // Levenshtein distance function code generated by chatgpt -int levenshteinDistance(const std::string& s1, const std::string& s2) { +int 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)); - for (size_t i = 0; i <= len1; ++i) dp[i][0] = i; - for (size_t j = 0; j <= len2; ++j) dp[0][j] = j; + for(size_t i = 0; i <= len1; ++i) + dp[i][0] = i; + for(size_t j = 0; j <= len2; ++j) + dp[0][j] = j; - for (size_t i = 1; i <= len1; ++i) { - for (size_t j = 1; j <= len2; ++j) { + for(size_t i = 1; i <= len1; ++i) { + for(size_t j = 1; j <= len2; ++j) { int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; - dp[i][j] = std::min({ - dp[i - 1][j] + 1, // deletion - dp[i][j - 1] + 1, // insertion - dp[i - 1][j - 1] + cost // substitution - }); + dp[i][j] = std::min({ + dp[i - 1][j] + 1, // deletion + dp[i][j - 1] + 1, // insertion + dp[i - 1][j - 1] + cost // substitution + }); } } @@ -35,19 +37,19 @@ int levenshteinDistance(const std::string& s1, const std::string& s2) { } // Finds the closest string from a list -std::pair findClosestMatch(const std::string& input, const std::vector& candidates) { +std::pair findClosestMatch(const std::string &input, const std::vector &candidates) { std::string closest; int minDistance = std::numeric_limits::max(); - for (const auto& candidate : candidates) { + for(const auto &candidate : candidates) { int distance = levenshteinDistance(input, candidate); - if (distance < minDistance) { + if(distance < minDistance) { minDistance = distance; closest = candidate; } } - return { closest,minDistance }; + return {closest, minDistance}; } /** This example demonstrates the use of `prefix_command` on a subcommand @@ -62,46 +64,36 @@ int main(int argc, const char *argv[]) { app.add_option("-v", value, "value"); app.add_subcommand("install", ""); - app.add_subcommand("upgrade",""); - app.add_subcommand("remove",""); - app.add_subcommand("test",""); + app.add_subcommand("upgrade", ""); + app.add_subcommand("remove", ""); + app.add_subcommand("test", ""); app.allow_extras(true); app.parse_complete_callback([&app]() { auto extras = app.remaining(); - if (extras.empty()) - { + if(extras.empty()) { return; } auto subs = app.get_subcommands(nullptr); std::vector list; - for (const auto* sub : subs) { - if (!sub->get_name().empty()) - { + for(const auto *sub : subs) { + if(!sub->get_name().empty()) { list.push_back(sub->get_name()); } auto aliases = sub->get_aliases(); - if (!aliases.empty()) - { + 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 <= 3) - { - std::cout<<"unmatched commands "< Date: Mon, 21 Apr 2025 09:27:50 -0700 Subject: [PATCH 03/30] fix pre-commit items --- examples/close_match.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 403f808a1..c39522710 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -25,21 +25,21 @@ int levenshteinDistance(const std::string &s1, const std::string &s2) { for(size_t i = 1; i <= len1; ++i) { for(size_t j = 1; j <= len2; ++j) { int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; - dp[i][j] = std::min({ - dp[i - 1][j] + 1, // deletion - dp[i][j - 1] + 1, // insertion - dp[i - 1][j - 1] + cost // substitution - }); + dp[i][j] = (std::min)({ + dp[i - 1][j] + 1, // deletion + dp[i][j - 1] + 1, // insertion + dp[i - 1][j - 1] + cost // substitution + }); } } return dp[len1][len2]; } -// Finds the closest string from a list +// Finds the closest string from a list (modified from chat gpt code) std::pair findClosestMatch(const std::string &input, const std::vector &candidates) { std::string closest; - int minDistance = std::numeric_limits::max(); + int minDistance = (std::numeric_limits::max)(); for(const auto &candidate : candidates) { int distance = levenshteinDistance(input, candidate); From 91ded769921ac782d4c105af7a5229009c0dada0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:29:14 +0000 Subject: [PATCH 04/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index c39522710..7b42a1d7a 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -25,11 +25,11 @@ int levenshteinDistance(const std::string &s1, const std::string &s2) { for(size_t i = 1; i <= len1; ++i) { for(size_t j = 1; j <= len2; ++j) { int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; - dp[i][j] = (std::min)({ - dp[i - 1][j] + 1, // deletion - dp[i][j - 1] + 1, // insertion - dp[i - 1][j - 1] + cost // substitution - }); + dp[i][j] = (std::min)({ + dp[i - 1][j] + 1, // deletion + dp[i][j - 1] + 1, // insertion + dp[i - 1][j - 1] + cost // substitution + }); } } From b857f3516e0cf67b4bf61b053fe89abd15af88c1 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 21 Apr 2025 14:03:19 -0700 Subject: [PATCH 05/30] clang-tidy fixes --- examples/close_match.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 7b42a1d7a..279b0a329 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -11,6 +11,7 @@ #include #include #include +#include // Levenshtein distance function code generated by chatgpt int levenshteinDistance(const std::string &s1, const std::string &s2) { @@ -18,9 +19,9 @@ int levenshteinDistance(const std::string &s1, const std::string &s2) { std::vector> dp(len1 + 1, std::vector(len2 + 1)); for(size_t i = 0; i <= len1; ++i) - dp[i][0] = i; + dp[i][0] = static_cast(i); for(size_t j = 0; j <= len2; ++j) - dp[0][j] = j; + dp[0][j] = static_cast(j); for(size_t i = 1; i <= len1; ++i) { for(size_t j = 1; j <= len2; ++j) { From 77b33a879d6b37d9d14fc9134d5381fe524c54e9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:04:08 +0000 Subject: [PATCH 06/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 279b0a329..682cac897 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -10,8 +10,8 @@ #include #include #include -#include #include +#include // Levenshtein distance function code generated by chatgpt int levenshteinDistance(const std::string &s1, const std::string &s2) { From 2f59ab8cfa3d7450a79dc6deb12844d5783f20d8 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Tue, 22 Apr 2025 18:18:15 -0700 Subject: [PATCH 07/30] prototype separate function call for match and prefix matching --- examples/close_match.cpp | 90 +++++++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 682cac897..5045a0908 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -13,15 +13,38 @@ #include #include +int prefixMatch(const std::string& s1, const std::string& s2) +{ + if (s1.size() < s2.size()) + { + if (s2.compare(0, s1.size(), s1.c_str()) == 0) + { + return s2.size()-s1.size(); + } + else { + return std::string::npos; + } + } + else { + if (s1.compare(0, s2.size(), s2.c_str()) == 0) + { + return s1.size()-s2.size(); + } + else { + return std::string::npos; + } + } +} + // Levenshtein distance function code generated by chatgpt int 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)); - for(size_t i = 0; i <= len1; ++i) - dp[i][0] = static_cast(i); - for(size_t j = 0; j <= len2; ++j) - dp[0][j] = static_cast(j); + for(size_t ii = 0; ii <= len1; ++ii) + dp[ii][0] = static_cast(ii); + for(size_t jj = 0; jj <= len2; ++jj) + dp[0][jj] = static_cast(jj); for(size_t i = 1; i <= len1; ++i) { for(size_t j = 1; j <= len2; ++j) { @@ -37,13 +60,22 @@ int levenshteinDistance(const std::string &s1, const std::string &s2) { return dp[len1][len2]; } +enum class MatchType:std::uint8_t {proximity,prefix}; + // Finds the closest string from a list (modified from chat gpt code) -std::pair findClosestMatch(const std::string &input, const std::vector &candidates) { +std::pair findClosestMatch(const std::string &input, const std::vector &candidates,MatchType match) { std::string closest; int minDistance = (std::numeric_limits::max)(); - + int distance=minDistance; for(const auto &candidate : candidates) { - int distance = levenshteinDistance(input, candidate); + if (match == MatchType::proximity) + { + distance = levenshteinDistance(input, candidate); + } + else + { + distance = prefixMatch(input, candidate); + } if(distance < minDistance) { minDistance = distance; closest = candidate; @@ -53,29 +85,16 @@ std::pair findClosestMatch(const std::string &input, const std return {closest, minDistance}; } -/** This example demonstrates the use of `prefix_command` on a subcommand -to capture all subsequent arguments along with an alias to make it appear as a regular options. - -All the values after the "sub" or "--sub" are available in the remaining() method. -*/ -int main(int argc, const char *argv[]) { - - int value{0}; - CLI::App app{"cose string App"}; - app.add_option("-v", value, "value"); +void addCloseMatchDetection(CLI::App* app, MatchType match) +{ + app->allow_extras(true); - app.add_subcommand("install", ""); - app.add_subcommand("upgrade", ""); - app.add_subcommand("remove", ""); - app.add_subcommand("test", ""); - app.allow_extras(true); - - app.parse_complete_callback([&app]() { - auto extras = app.remaining(); + app->parse_complete_callback([&app,match]() { + auto extras = app->remaining(); if(extras.empty()) { return; } - auto subs = app.get_subcommands(nullptr); + auto subs = app->get_subcommands(nullptr); std::vector list; for(const auto *sub : subs) { if(!sub->get_name().empty()) { @@ -88,13 +107,28 @@ int main(int argc, const char *argv[]) { } for(auto &extra : extras) { if(extra.front() != '-') { - auto closest = findClosestMatch(extra, list); + auto closest = findClosestMatch(extra, list,match); if(closest.second <= 3) { std::cout << "unmatched commands " << extra << ", closest match is " << closest.first << "\n"; } } } - }); + }); +} + +/** 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 value{0}; + CLI::App app{"cose string App"}; + app.add_option("-v", value, "value"); + + app.add_subcommand("install", ""); + app.add_subcommand("upgrade", ""); + app.add_subcommand("remove", ""); + app.add_subcommand("test", ""); + addCloseMatchDetection(&app,MatchType::prefix); CLI11_PARSE(app, argc, argv); return 0; } From ae0735c88cfd6799f5b4fcec41455d8b05706c9c Mon Sep 17 00:00:00 2001 From: Philip Top Date: Tue, 22 Apr 2025 18:26:53 -0700 Subject: [PATCH 08/30] change to size_t for levenshteinDistance --- examples/close_match.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 5045a0908..f7edd4a89 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -13,7 +13,7 @@ #include #include -int prefixMatch(const std::string& s1, const std::string& s2) +std::size_t prefixMatch(const std::string& s1, const std::string& s2) { if (s1.size() < s2.size()) { @@ -37,14 +37,14 @@ int prefixMatch(const std::string& s1, const std::string& s2) } // Levenshtein distance function code generated by chatgpt -int 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::vector> dp(len1 + 1, std::vector(len2 + 1)); + std::vector> dp(len1 + 1, std::vector(len2 + 1)); for(size_t ii = 0; ii <= len1; ++ii) - dp[ii][0] = static_cast(ii); + dp[ii][0] = ii; for(size_t jj = 0; jj <= len2; ++jj) - dp[0][jj] = static_cast(jj); + dp[0][jj] = jj; for(size_t i = 1; i <= len1; ++i) { for(size_t j = 1; j <= len2; ++j) { @@ -63,10 +63,10 @@ int levenshteinDistance(const std::string &s1, const std::string &s2) { enum class MatchType:std::uint8_t {proximity,prefix}; // Finds the closest string from a list (modified from chat gpt code) -std::pair findClosestMatch(const std::string &input, const std::vector &candidates,MatchType match) { +std::pair findClosestMatch(const std::string &input, const std::vector &candidates,MatchType match) { std::string closest; - int minDistance = (std::numeric_limits::max)(); - int distance=minDistance; + int minDistance = (std::numeric_limits::max)(); + std::size_t distance=minDistance; for(const auto &candidate : candidates) { if (match == MatchType::proximity) { From 31a62ac3f2c47873a730ed95c87eb7400c25fb54 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 01:28:38 +0000 Subject: [PATCH 09/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 55 +++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index f7edd4a89..9376bf435 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -13,24 +13,17 @@ #include #include -std::size_t prefixMatch(const std::string& s1, const std::string& s2) -{ - if (s1.size() < s2.size()) - { - if (s2.compare(0, s1.size(), s1.c_str()) == 0) - { - return s2.size()-s1.size(); - } - else { +std::size_t prefixMatch(const std::string &s1, const std::string &s2) { + if(s1.size() < s2.size()) { + if(s2.compare(0, s1.size(), s1.c_str()) == 0) { + return s2.size() - s1.size(); + } else { return std::string::npos; } - } - else { - if (s1.compare(0, s2.size(), s2.c_str()) == 0) - { - return s1.size()-s2.size(); - } - else { + } else { + if(s1.compare(0, s2.size(), s2.c_str()) == 0) { + return s1.size() - s2.size(); + } else { return std::string::npos; } } @@ -60,20 +53,18 @@ std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { return dp[len1][len2]; } -enum class MatchType:std::uint8_t {proximity,prefix}; +enum class MatchType : std::uint8_t { proximity, prefix }; // Finds the closest string from a list (modified from chat gpt code) -std::pair findClosestMatch(const std::string &input, const std::vector &candidates,MatchType match) { +std::pair +findClosestMatch(const std::string &input, const std::vector &candidates, MatchType match) { std::string closest; int minDistance = (std::numeric_limits::max)(); - std::size_t distance=minDistance; + std::size_t distance = minDistance; for(const auto &candidate : candidates) { - if (match == MatchType::proximity) - { + if(match == MatchType::proximity) { distance = levenshteinDistance(input, candidate); - } - else - { + } else { distance = prefixMatch(input, candidate); } if(distance < minDistance) { @@ -85,11 +76,10 @@ std::pair findClosestMatch(const std::string &input, c return {closest, minDistance}; } -void addCloseMatchDetection(CLI::App* app, MatchType match) -{ +void addCloseMatchDetection(CLI::App *app, MatchType match) { app->allow_extras(true); - app->parse_complete_callback([&app,match]() { + app->parse_complete_callback([&app, match]() { auto extras = app->remaining(); if(extras.empty()) { return; @@ -107,17 +97,18 @@ void addCloseMatchDetection(CLI::App* app, MatchType match) } for(auto &extra : extras) { if(extra.front() != '-') { - auto closest = findClosestMatch(extra, list,match); + auto closest = findClosestMatch(extra, list, match); if(closest.second <= 3) { std::cout << "unmatched commands " << extra << ", closest match is " << closest.first << "\n"; } } } - }); + }); } -/** This example demonstrates the use of close match detection to detect invalid commands that are close matches to existing ones -*/ +/** 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 value{0}; @@ -128,7 +119,7 @@ int main(int argc, const char *argv[]) { app.add_subcommand("upgrade", ""); app.add_subcommand("remove", ""); app.add_subcommand("test", ""); - addCloseMatchDetection(&app,MatchType::prefix); + addCloseMatchDetection(&app, MatchType::prefix); CLI11_PARSE(app, argc, argv); return 0; } From a5768acf083786b90dfcdcf9f273ef27a3d56c60 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 28 Apr 2025 06:29:31 -0700 Subject: [PATCH 10/30] clang-tidy fixes --- examples/close_match.cpp | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 9376bf435..0cea7f7d2 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -15,17 +15,15 @@ std::size_t prefixMatch(const std::string &s1, const std::string &s2) { if(s1.size() < s2.size()) { - if(s2.compare(0, s1.size(), s1.c_str()) == 0) { + if (s2.compare(0, s1.size(), s1) == 0) { return s2.size() - s1.size(); - } else { - return std::string::npos; } + return std::string::npos; } else { - if(s1.compare(0, s2.size(), s2.c_str()) == 0) { + if(s1.compare(0, s2.size(), s2) == 0) { return s1.size() - s2.size(); - } else { - return std::string::npos; } + return std::string::npos; } } @@ -59,14 +57,9 @@ enum class MatchType : std::uint8_t { proximity, prefix }; std::pair findClosestMatch(const std::string &input, const std::vector &candidates, MatchType match) { std::string closest; - int minDistance = (std::numeric_limits::max)(); - std::size_t distance = minDistance; + std::size_t minDistance = (std::numeric_limits::max)(); for(const auto &candidate : candidates) { - if(match == MatchType::proximity) { - distance = levenshteinDistance(input, candidate); - } else { - distance = prefixMatch(input, candidate); - } + std::size_t distance=(match == MatchType::proximity)?levenshteinDistance(input, candidate):prefixMatch(input, candidate); if(distance < minDistance) { minDistance = distance; closest = candidate; From bf467089b2e9f040be8d175d7d260cdaa9233013 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 13:31:17 +0000 Subject: [PATCH 11/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 0cea7f7d2..2539720d3 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -15,7 +15,7 @@ std::size_t prefixMatch(const std::string &s1, const std::string &s2) { if(s1.size() < s2.size()) { - if (s2.compare(0, s1.size(), s1) == 0) { + if(s2.compare(0, s1.size(), s1) == 0) { return s2.size() - s1.size(); } return std::string::npos; @@ -59,7 +59,8 @@ findClosestMatch(const std::string &input, const std::vector &candi std::string closest; std::size_t minDistance = (std::numeric_limits::max)(); for(const auto &candidate : candidates) { - std::size_t distance=(match == MatchType::proximity)?levenshteinDistance(input, candidate):prefixMatch(input, candidate); + std::size_t distance = + (match == MatchType::proximity) ? levenshteinDistance(input, candidate) : prefixMatch(input, candidate); if(distance < minDistance) { minDistance = distance; closest = candidate; From 2175dc6c57d5dab7f6dc8225d5ee24299a757017 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 30 Apr 2025 05:34:06 -0700 Subject: [PATCH 12/30] add prefix matching to be enabled by a flag in the app. --- examples/close_match.cpp | 45 ++++++---------- include/CLI/App.hpp | 18 +++++-- include/CLI/impl/App_inl.hpp | 55 ++++++++++++++++---- tests/SubcommandTest.cpp | 99 ++++++++++++++++++++++++++++++++++++ 4 files changed, 176 insertions(+), 41 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 2539720d3..46ffe9d80 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -13,19 +13,6 @@ #include #include -std::size_t prefixMatch(const std::string &s1, const std::string &s2) { - if(s1.size() < s2.size()) { - if(s2.compare(0, s1.size(), s1) == 0) { - return s2.size() - s1.size(); - } - return std::string::npos; - } else { - if(s1.compare(0, s2.size(), s2) == 0) { - return s1.size() - s2.size(); - } - return std::string::npos; - } -} // Levenshtein distance function code generated by chatgpt std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { @@ -37,13 +24,13 @@ std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { for(size_t jj = 0; jj <= len2; ++jj) dp[0][jj] = jj; - for(size_t i = 1; i <= len1; ++i) { - for(size_t j = 1; j <= len2; ++j) { - int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; - dp[i][j] = (std::min)({ - dp[i - 1][j] + 1, // deletion - dp[i][j - 1] + 1, // insertion - dp[i - 1][j - 1] + cost // substitution + for(size_t ii = 1; ii <= len1; ++ii) { + for(size_t jj = 1; jj <= len2; ++jj) { + 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 }); } } @@ -51,16 +38,14 @@ std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { return dp[len1][len2]; } -enum class MatchType : std::uint8_t { proximity, prefix }; // Finds the closest string from a list (modified from chat gpt code) std::pair -findClosestMatch(const std::string &input, const std::vector &candidates, MatchType match) { +findClosestMatch(const std::string &input, const std::vector &candidates) { std::string closest; std::size_t minDistance = (std::numeric_limits::max)(); for(const auto &candidate : candidates) { - std::size_t distance = - (match == MatchType::proximity) ? levenshteinDistance(input, candidate) : prefixMatch(input, candidate); + std::size_t distance = levenshteinDistance(input, candidate); if(distance < minDistance) { minDistance = distance; closest = candidate; @@ -70,10 +55,10 @@ findClosestMatch(const std::string &input, const std::vector &candi return {closest, minDistance}; } -void addCloseMatchDetection(CLI::App *app, MatchType match) { +void addCloseMatchDetection(CLI::App *app, std::size_t minDistance=3) { app->allow_extras(true); - app->parse_complete_callback([&app, match]() { + app->parse_complete_callback([&app, minDistance]() { auto extras = app->remaining(); if(extras.empty()) { return; @@ -91,8 +76,8 @@ void addCloseMatchDetection(CLI::App *app, MatchType match) { } for(auto &extra : extras) { if(extra.front() != '-') { - auto closest = findClosestMatch(extra, list, match); - if(closest.second <= 3) { + auto closest = findClosestMatch(extra, list); + if(closest.second <= minDistance) { std::cout << "unmatched commands " << extra << ", closest match is " << closest.first << "\n"; } } @@ -107,13 +92,15 @@ int main(int argc, const char *argv[]) { int value{0}; CLI::App app{"cose string App"}; + //turn on prefix matching + app.allow_subcommand_prefix_matching(); app.add_option("-v", value, "value"); app.add_subcommand("install", ""); app.add_subcommand("upgrade", ""); app.add_subcommand("remove", ""); app.add_subcommand("test", ""); - addCloseMatchDetection(&app, MatchType::prefix); + addCloseMatchDetection(&app,5); CLI11_PARSE(app, argc, argv); return 0; } diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index ffbe986fc..372c342b8 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -271,6 +271,9 @@ class App { /// indicator that the subcommand should allow non-standard option arguments, such as -single_dash_flag bool allow_non_standard_options_{false}; + /// indicator to allow subcommands to match with prefix matching + bool allow_prefix_matching_{false}; + /// Counts the number of times this command/subcommand was parsed std::uint32_t parsed_{0U}; @@ -317,7 +320,7 @@ class App { /// Special private constructor for subcommand App(std::string app_description, std::string app_name, App *parent); - + public: /// @name Basic ///@{ @@ -409,6 +412,11 @@ class App { return this; } + /// allow prefix matching for subcommands + App *allow_subcommand_prefix_matching(bool allowed = true) { + allow_prefix_matching_ = allowed; + return this; + } /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled App *disabled_by_default(bool disable = true) { if(disable) { @@ -1163,9 +1171,12 @@ class App { /// Get the status of silence CLI11_NODISCARD bool get_silent() const { return silent_; } - /// Get the status of silence + /// Get the status of allowing non standard option names CLI11_NODISCARD bool get_allow_non_standard_option_names() const { return allow_non_standard_options_; } + /// Get the status of allowing prefix matching for subcommands + CLI11_NODISCARD bool get_allow_subcommand_prefix_matching() const { return allow_prefix_matching_; } + /// Get the status of disabled CLI11_NODISCARD bool get_immediate_callback() const { return immediate_callback_; } @@ -1225,7 +1236,8 @@ class App { CLI11_NODISCARD std::string get_display_name(bool with_aliases = false) const; /// Check the name, case-insensitive and underscore insensitive if set - CLI11_NODISCARD bool check_name(std::string name_to_check) const; + /// @return 0 if no match, 1 or higher if there is a match (2 or more is the character difference with prefix matching enabled) + CLI11_NODISCARD int check_name(std::string name_to_check) const; /// Get the groups available directly from this option (in order) CLI11_NODISCARD std::vector get_groups() const; diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index fdeab4e05..08d85436f 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -57,6 +57,7 @@ CLI11_INLINE App::App(std::string app_description, std::string app_name, App *pa formatter_ = parent_->formatter_; config_formatter_ = parent_->config_formatter_; require_subcommand_max_ = parent_->require_subcommand_max_; + allow_prefix_matching_=parent_->allow_prefix_matching_; } } @@ -893,7 +894,7 @@ CLI11_NODISCARD CLI11_INLINE std::string App::get_display_name(bool with_aliases return dispname; } -CLI11_NODISCARD CLI11_INLINE bool App::check_name(std::string name_to_check) const { +CLI11_NODISCARD CLI11_INLINE int App::check_name(std::string name_to_check) const { std::string local_name = name_; if(ignore_underscore_) { local_name = detail::remove_underscore(name_); @@ -905,7 +906,14 @@ CLI11_NODISCARD CLI11_INLINE bool App::check_name(std::string name_to_check) con } if(local_name == name_to_check) { - return true; + return 1; + } + if (allow_prefix_matching_ && name_to_check.size()(local_name.size()-name_to_check.size()+1); + } } for(std::string les : aliases_) { // NOLINT(performance-for-range-copy) if(ignore_underscore_) { @@ -915,10 +923,17 @@ CLI11_NODISCARD CLI11_INLINE bool App::check_name(std::string name_to_check) con les = detail::to_lower(les); } if(les == name_to_check) { - return true; + return 1; + } + if (allow_prefix_matching_ && name_to_check.size()(les.size()-name_to_check.size()+1); + } } } - return false; + return 0; } CLI11_NODISCARD CLI11_INLINE std::vector App::get_groups() const { @@ -1821,21 +1836,43 @@ CLI11_INLINE bool App::_parse_positional(std::vector &args, bool ha CLI11_NODISCARD CLI11_INLINE App * App::_find_subcommand(const std::string &subc_name, bool ignore_disabled, bool ignore_used) const noexcept { + App *bcom{nullptr}; for(const App_p &com : subcommands_) { if(com->disabled_ && ignore_disabled) continue; if(com->get_name().empty()) { auto *subc = com->_find_subcommand(subc_name, ignore_disabled, ignore_used); if(subc != nullptr) { - return subc; + if (bcom != nullptr) + { + return nullptr; + } + bcom=subc; + if (!allow_prefix_matching_) { + return bcom; + } } } - if(com->check_name(subc_name)) { - if((!*com) || !ignore_used) - return com.get(); + auto res=com->check_name(subc_name); + if(res!=0) { + if ((!*com) || !ignore_used) { + if (res == 1) + { + return com.get(); + } + if (bcom != nullptr) + { + return nullptr; + } + bcom=com.get(); + if (!allow_prefix_matching_) { + return bcom; + } + + } } } - return nullptr; + return bcom; } CLI11_INLINE bool App::_parse_subcommand(std::vector &args) { diff --git a/tests/SubcommandTest.cpp b/tests/SubcommandTest.cpp index b794e6b8d..279157a20 100644 --- a/tests/SubcommandTest.cpp +++ b/tests/SubcommandTest.cpp @@ -110,6 +110,105 @@ TEST_CASE_METHOD(TApp, "CrazyNameSubcommand", "[subcom]") { CHECK(1u == sub1->count()); } +TEST_CASE_METHOD(TApp, "subcommandPrefix", "[subcom]") { + app.allow_subcommand_prefix_matching(); + auto *sub1 = app.add_subcommand("sub1"); + CHECK(app.get_allow_subcommand_prefix_matching()); + // name can be set to whatever + CHECK_NOTHROW(sub1->name("crazy name with spaces")); + args = {"crazy name with spaces"}; + + run(); + + CHECK(app.got_subcommand("crazy name with spaces")); + CHECK(1u == sub1->count()); + + args = {"crazy name"}; + run(); + + CHECK(app.got_subcommand("crazy name with spaces")); + CHECK(1u == sub1->count()); + + args = {"crazy"}; + run(); + + CHECK(app.got_subcommand("crazy name")); + CHECK(1u == sub1->count()); + + args = {"cr"}; + run(); + + CHECK(app.got_subcommand("crazy")); + CHECK(1u == sub1->count()); + + args = {"c"}; + run(); + + CHECK(app.got_subcommand("crazy")); + CHECK(1u == sub1->count()); +} + +TEST_CASE_METHOD(TApp, "subcommandPrefixAlias", "[subcom]") { + app.allow_subcommand_prefix_matching(); + auto *sub1 = app.add_subcommand("sub1"); + CHECK(app.get_allow_subcommand_prefix_matching()); + // name can be set to whatever + sub1->alias("crazy name with spaces"); + args = {"crazy name with spaces"}; + + run(); + + CHECK(app.got_subcommand("crazy name with spaces")); + CHECK(1u == sub1->count()); + + args = {"crazy name"}; + run(); + + CHECK(app.got_subcommand("crazy name with spaces")); + CHECK(1u == sub1->count()); + + args = {"crazy"}; + run(); + + CHECK(app.got_subcommand("crazy name")); + CHECK(1u == sub1->count()); + + args = {"cr"}; + run(); + + CHECK(app.got_subcommand("crazy")); + CHECK(1u == sub1->count()); + + args = {"c"}; + run(); + + CHECK(app.got_subcommand("crazy")); + CHECK(1u == sub1->count()); +} + +TEST_CASE_METHOD(TApp, "subcommandPrefixMultiple", "[subcom]") { + app.allow_subcommand_prefix_matching(); + auto *sub1 = app.add_subcommand("sub_long_prefix"); + auto *sub2 = app.add_subcommand("sub_elong_prefix"); + // name can be set to whatever + args = {"sub_long"}; + + run(); + + CHECK(app.got_subcommand("sub_long_prefix")); + CHECK(1u == sub1->count()); + + args = {"sub_e"}; + run(); + + CHECK(app.got_subcommand("sub_elong_prefix")); + CHECK(1u == sub2->count()); + + args = {"sub_"}; + CHECK_THROWS_AS(run(), CLI::ExtrasError); + +} + TEST_CASE_METHOD(TApp, "RequiredAndSubcommands", "[subcom]") { std::string baz; From 7c4a4d555f41cb24ad4c4f5c040e422f648b0fbb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:34:37 +0000 Subject: [PATCH 13/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 12 +++++------ include/CLI/App.hpp | 5 +++-- include/CLI/impl/App_inl.hpp | 42 +++++++++++++++--------------------- tests/SubcommandTest.cpp | 1 - 4 files changed, 25 insertions(+), 35 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 46ffe9d80..6310cd0e5 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -13,7 +13,6 @@ #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(); @@ -38,10 +37,9 @@ std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { return dp[len1][len2]; } - // Finds the closest string from a list (modified from chat gpt code) -std::pair -findClosestMatch(const std::string &input, const std::vector &candidates) { +std::pair findClosestMatch(const std::string &input, + const std::vector &candidates) { std::string closest; std::size_t minDistance = (std::numeric_limits::max)(); for(const auto &candidate : candidates) { @@ -55,7 +53,7 @@ findClosestMatch(const std::string &input, const std::vector &candi return {closest, minDistance}; } -void addCloseMatchDetection(CLI::App *app, std::size_t minDistance=3) { +void addCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3) { app->allow_extras(true); app->parse_complete_callback([&app, minDistance]() { @@ -92,7 +90,7 @@ int main(int argc, const char *argv[]) { int value{0}; CLI::App app{"cose string App"}; - //turn on prefix matching + // turn on prefix matching app.allow_subcommand_prefix_matching(); app.add_option("-v", value, "value"); @@ -100,7 +98,7 @@ int main(int argc, const char *argv[]) { app.add_subcommand("upgrade", ""); app.add_subcommand("remove", ""); app.add_subcommand("test", ""); - addCloseMatchDetection(&app,5); + addCloseMatchDetection(&app, 5); CLI11_PARSE(app, argc, argv); return 0; } diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 372c342b8..3ce84f025 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -320,7 +320,7 @@ class App { /// Special private constructor for subcommand App(std::string app_description, std::string app_name, App *parent); - + public: /// @name Basic ///@{ @@ -1236,7 +1236,8 @@ class App { CLI11_NODISCARD std::string get_display_name(bool with_aliases = false) const; /// Check the name, case-insensitive and underscore insensitive if set - /// @return 0 if no match, 1 or higher if there is a match (2 or more is the character difference with prefix matching enabled) + /// @return 0 if no match, 1 or higher if there is a match (2 or more is the character difference with prefix + /// matching enabled) CLI11_NODISCARD int check_name(std::string name_to_check) const; /// Get the groups available directly from this option (in order) diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index 08d85436f..3b8b539f5 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -57,7 +57,7 @@ CLI11_INLINE App::App(std::string app_description, std::string app_name, App *pa formatter_ = parent_->formatter_; config_formatter_ = parent_->config_formatter_; require_subcommand_max_ = parent_->require_subcommand_max_; - allow_prefix_matching_=parent_->allow_prefix_matching_; + allow_prefix_matching_ = parent_->allow_prefix_matching_; } } @@ -908,11 +908,9 @@ CLI11_NODISCARD CLI11_INLINE int App::check_name(std::string name_to_check) cons if(local_name == name_to_check) { return 1; } - if (allow_prefix_matching_ && name_to_check.size()(local_name.size()-name_to_check.size()+1); + if(allow_prefix_matching_ && name_to_check.size() < local_name.size()) { + if(local_name.compare(0, name_to_check.size(), name_to_check) == 0) { + return static_cast(local_name.size() - name_to_check.size() + 1); } } for(std::string les : aliases_) { // NOLINT(performance-for-range-copy) @@ -925,11 +923,9 @@ CLI11_NODISCARD CLI11_INLINE int App::check_name(std::string name_to_check) cons if(les == name_to_check) { return 1; } - if (allow_prefix_matching_ && name_to_check.size()(les.size()-name_to_check.size()+1); + if(allow_prefix_matching_ && name_to_check.size() < les.size()) { + if(les.compare(0, name_to_check.size(), name_to_check) == 0) { + return static_cast(les.size() - name_to_check.size() + 1); } } } @@ -1843,32 +1839,28 @@ App::_find_subcommand(const std::string &subc_name, bool ignore_disabled, bool i if(com->get_name().empty()) { auto *subc = com->_find_subcommand(subc_name, ignore_disabled, ignore_used); if(subc != nullptr) { - if (bcom != nullptr) - { + if(bcom != nullptr) { return nullptr; } - bcom=subc; - if (!allow_prefix_matching_) { + bcom = subc; + if(!allow_prefix_matching_) { return bcom; } } } - auto res=com->check_name(subc_name); - if(res!=0) { - if ((!*com) || !ignore_used) { - if (res == 1) - { + auto res = com->check_name(subc_name); + if(res != 0) { + if((!*com) || !ignore_used) { + if(res == 1) { return com.get(); } - if (bcom != nullptr) - { + if(bcom != nullptr) { return nullptr; } - bcom=com.get(); - if (!allow_prefix_matching_) { + bcom = com.get(); + if(!allow_prefix_matching_) { return bcom; } - } } } diff --git a/tests/SubcommandTest.cpp b/tests/SubcommandTest.cpp index 279157a20..3cc1b7b16 100644 --- a/tests/SubcommandTest.cpp +++ b/tests/SubcommandTest.cpp @@ -206,7 +206,6 @@ TEST_CASE_METHOD(TApp, "subcommandPrefixMultiple", "[subcom]") { args = {"sub_"}; CHECK_THROWS_AS(run(), CLI::ExtrasError); - } TEST_CASE_METHOD(TApp, "RequiredAndSubcommands", "[subcom]") { From eec2211b0f5dc660a48a91f4484dc1dfeb41eefb Mon Sep 17 00:00:00 2001 From: Philip Top Date: Wed, 30 Apr 2025 08:00:27 -0700 Subject: [PATCH 14/30] refactor the close_match method into 2 different methods --- include/CLI/App.hpp | 10 +++++++--- include/CLI/impl/App_inl.hpp | 23 ++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 3ce84f025..6519af90e 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -1235,10 +1235,14 @@ class App { /// Get a display name for an app CLI11_NODISCARD std::string get_display_name(bool with_aliases = false) const; + /// Check the name, case-insensitive and underscore insensitive, and prefix matching if set + ///@return true if matched + CLI11_NODISCARD bool check_name(std::string name_to_check) const; + + enum class NameMatch:std::uint8_t{none=0,match=1,prefix=2}; /// Check the name, case-insensitive and underscore insensitive if set - /// @return 0 if no match, 1 or higher if there is a match (2 or more is the character difference with prefix - /// matching enabled) - CLI11_NODISCARD int check_name(std::string name_to_check) const; + /// @return NameMatch::none if no match, NameMatch::match if exact NameMatch::prefix if prefix is enabled and a prefix matches + CLI11_NODISCARD NameMatch check_name_detail(std::string name_to_check) const; /// Get the groups available directly from this option (in order) CLI11_NODISCARD std::vector get_groups() const; diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index 3b8b539f5..738b08876 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -894,7 +894,12 @@ CLI11_NODISCARD CLI11_INLINE std::string App::get_display_name(bool with_aliases return dispname; } -CLI11_NODISCARD CLI11_INLINE int App::check_name(std::string name_to_check) const { +CLI11_NODISCARD CLI11_INLINE bool App::check_name(std::string name_to_check) const { + auto result=check_name_detail(std::move(name_to_check)); + return (result!=NameMatch::none); +} + +CLI11_NODISCARD CLI11_INLINE App::NameMatch App::check_name_detail(std::string name_to_check) const { std::string local_name = name_; if(ignore_underscore_) { local_name = detail::remove_underscore(name_); @@ -906,11 +911,11 @@ CLI11_NODISCARD CLI11_INLINE int App::check_name(std::string name_to_check) cons } if(local_name == name_to_check) { - return 1; + return App::NameMatch::match; } if(allow_prefix_matching_ && name_to_check.size() < local_name.size()) { if(local_name.compare(0, name_to_check.size(), name_to_check) == 0) { - return static_cast(local_name.size() - name_to_check.size() + 1); + return App::NameMatch::prefix; } } for(std::string les : aliases_) { // NOLINT(performance-for-range-copy) @@ -921,15 +926,15 @@ CLI11_NODISCARD CLI11_INLINE int App::check_name(std::string name_to_check) cons les = detail::to_lower(les); } if(les == name_to_check) { - return 1; + return App::NameMatch::match; } if(allow_prefix_matching_ && name_to_check.size() < les.size()) { if(les.compare(0, name_to_check.size(), name_to_check) == 0) { - return static_cast(les.size() - name_to_check.size() + 1); + return App::NameMatch::prefix; } } } - return 0; + return App::NameMatch::none; } CLI11_NODISCARD CLI11_INLINE std::vector App::get_groups() const { @@ -1848,10 +1853,10 @@ App::_find_subcommand(const std::string &subc_name, bool ignore_disabled, bool i } } } - auto res = com->check_name(subc_name); - if(res != 0) { + auto res = com->check_name_detail(subc_name); + if(res != NameMatch::none) { if((!*com) || !ignore_used) { - if(res == 1) { + if(res == NameMatch::match) { return com.get(); } if(bcom != nullptr) { From f29420fb828982a7e37327f3c25ea0e9d97e27f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:04:03 +0000 Subject: [PATCH 15/30] style: pre-commit.ci fixes --- include/CLI/App.hpp | 5 +++-- include/CLI/impl/App_inl.hpp | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index 6519af90e..a71f2d6de 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -1239,9 +1239,10 @@ class App { ///@return true if matched CLI11_NODISCARD bool check_name(std::string name_to_check) const; - enum class NameMatch:std::uint8_t{none=0,match=1,prefix=2}; + enum class NameMatch : std::uint8_t { none = 0, match = 1, prefix = 2 }; /// Check the name, case-insensitive and underscore insensitive if set - /// @return NameMatch::none if no match, NameMatch::match if exact NameMatch::prefix if prefix is enabled and a prefix matches + /// @return NameMatch::none if no match, NameMatch::match if exact NameMatch::prefix if prefix is enabled and a + /// prefix matches CLI11_NODISCARD NameMatch check_name_detail(std::string name_to_check) const; /// Get the groups available directly from this option (in order) diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index 738b08876..6b03a453c 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -895,8 +895,8 @@ CLI11_NODISCARD CLI11_INLINE std::string App::get_display_name(bool with_aliases } CLI11_NODISCARD CLI11_INLINE bool App::check_name(std::string name_to_check) const { - auto result=check_name_detail(std::move(name_to_check)); - return (result!=NameMatch::none); + auto result = check_name_detail(std::move(name_to_check)); + return (result != NameMatch::none); } CLI11_NODISCARD CLI11_INLINE App::NameMatch App::check_name_detail(std::string name_to_check) const { From 72c78a248407871b6c38625e49a2408a69637981 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 1 May 2025 05:21:47 -0700 Subject: [PATCH 16/30] add some more tests and code cleanup --- include/CLI/App.hpp | 9 +++++---- include/CLI/impl/App_inl.hpp | 6 +++--- tests/SubcommandTest.cpp | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index a71f2d6de..ad909c7a9 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -1236,13 +1236,14 @@ class App { CLI11_NODISCARD std::string get_display_name(bool with_aliases = false) const; /// Check the name, case-insensitive and underscore insensitive, and prefix matching if set - ///@return true if matched + /// @return true if matched CLI11_NODISCARD bool check_name(std::string name_to_check) const; - enum class NameMatch : std::uint8_t { none = 0, match = 1, prefix = 2 }; + /// @brief enumeration of matching possibilities + enum class NameMatch:std::uint8_t{none=0,exact=1,prefix=2}; + /// Check the name, case-insensitive and underscore insensitive if set - /// @return NameMatch::none if no match, NameMatch::match if exact NameMatch::prefix if prefix is enabled and a - /// prefix matches + /// @return NameMatch::none if no match, NameMatch::exact if the match is exact NameMatch::prefix if prefix is enabled and a prefix matches CLI11_NODISCARD NameMatch check_name_detail(std::string name_to_check) const; /// Get the groups available directly from this option (in order) diff --git a/include/CLI/impl/App_inl.hpp b/include/CLI/impl/App_inl.hpp index 6b03a453c..b6e28efa6 100644 --- a/include/CLI/impl/App_inl.hpp +++ b/include/CLI/impl/App_inl.hpp @@ -911,7 +911,7 @@ CLI11_NODISCARD CLI11_INLINE App::NameMatch App::check_name_detail(std::string n } if(local_name == name_to_check) { - return App::NameMatch::match; + return App::NameMatch::exact; } if(allow_prefix_matching_ && name_to_check.size() < local_name.size()) { if(local_name.compare(0, name_to_check.size(), name_to_check) == 0) { @@ -926,7 +926,7 @@ CLI11_NODISCARD CLI11_INLINE App::NameMatch App::check_name_detail(std::string n les = detail::to_lower(les); } if(les == name_to_check) { - return App::NameMatch::match; + return App::NameMatch::exact; } if(allow_prefix_matching_ && name_to_check.size() < les.size()) { if(les.compare(0, name_to_check.size(), name_to_check) == 0) { @@ -1856,7 +1856,7 @@ App::_find_subcommand(const std::string &subc_name, bool ignore_disabled, bool i auto res = com->check_name_detail(subc_name); if(res != NameMatch::none) { if((!*com) || !ignore_used) { - if(res == NameMatch::match) { + if(res == NameMatch::exact) { return com.get(); } if(bcom != nullptr) { diff --git a/tests/SubcommandTest.cpp b/tests/SubcommandTest.cpp index 3cc1b7b16..ee6fb1928 100644 --- a/tests/SubcommandTest.cpp +++ b/tests/SubcommandTest.cpp @@ -335,6 +335,7 @@ TEST_CASE_METHOD(TApp, "DuplicateSubcommandCallbacksValues", "[subcom]") { CHECK(36 == vals[2]); } + TEST_CASE_METHOD(TApp, "Callbacks", "[subcom]") { auto *sub1 = app.add_subcommand("sub1"); sub1->callback([]() { throw CLI::Success(); }); @@ -1871,6 +1872,25 @@ TEST_CASE_METHOD(TApp, "AliasErrors", "[subcom]") { sub2->ignore_underscore(); CHECK_THROWS_AS(sub2->alias("les3"), CLI::OptionAlreadyAdded); } + +TEST_CASE_METHOD(TApp, "DuplicateErrorsPrefix", "[subcom]") { + app.allow_subcommand_prefix_matching(true); + auto *sub1 = app.add_subcommand("sub_test"); + auto *sub2 = app.add_subcommand("sub_deploy"); + + + CHECK_THROWS_AS(app.add_subcommand("sub"), CLI::OptionAlreadyAdded); + // cannot alias to an existing subcommand + CHECK_THROWS_AS(sub2->alias("sub"), CLI::OptionAlreadyAdded); + app.ignore_case(); + //this needs to be opposite of the subcommand the alias is being tested on to check for ambiguity + sub2->ignore_case(); + CHECK_THROWS_AS(sub1->alias("SUB_"), CLI::OptionAlreadyAdded); + app.ignore_underscore(); + sub1->ignore_underscore(); + CHECK_THROWS_AS(sub2->alias("su_bt"), CLI::OptionAlreadyAdded); +} + // test adding a subcommand via the pointer TEST_CASE_METHOD(TApp, "ExistingSubcommandMatch", "[subcom]") { auto sshared = std::make_shared("documenting the subcommand", "sub1"); @@ -1920,6 +1940,8 @@ TEST_CASE_METHOD(TApp, "AliasErrorsInOptionGroup", "[subcom]") { CHECK_THROWS_AS(sub2->name("sub1"), CLI::OptionAlreadyAdded); } + + TEST_CASE("SharedSubTests: SharedSubcommand", "[subcom]") { double val{0.0}, val2{0.0}, val3{0.0}, val4{0.0}; CLI::App app1{"test program1"}; From 279e710544fedd30cb70c4df0696282e762d5b03 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 12:23:27 +0000 Subject: [PATCH 17/30] style: pre-commit.ci fixes --- include/CLI/App.hpp | 5 +++-- tests/SubcommandTest.cpp | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/include/CLI/App.hpp b/include/CLI/App.hpp index ad909c7a9..2dbd77040 100644 --- a/include/CLI/App.hpp +++ b/include/CLI/App.hpp @@ -1240,10 +1240,11 @@ class App { CLI11_NODISCARD bool check_name(std::string name_to_check) const; /// @brief enumeration of matching possibilities - enum class NameMatch:std::uint8_t{none=0,exact=1,prefix=2}; + enum class NameMatch : std::uint8_t { none = 0, exact = 1, prefix = 2 }; /// Check the name, case-insensitive and underscore insensitive if set - /// @return NameMatch::none if no match, NameMatch::exact if the match is exact NameMatch::prefix if prefix is enabled and a prefix matches + /// @return NameMatch::none if no match, NameMatch::exact if the match is exact NameMatch::prefix if prefix is + /// enabled and a prefix matches CLI11_NODISCARD NameMatch check_name_detail(std::string name_to_check) const; /// Get the groups available directly from this option (in order) diff --git a/tests/SubcommandTest.cpp b/tests/SubcommandTest.cpp index ee6fb1928..2feb6e188 100644 --- a/tests/SubcommandTest.cpp +++ b/tests/SubcommandTest.cpp @@ -335,7 +335,6 @@ TEST_CASE_METHOD(TApp, "DuplicateSubcommandCallbacksValues", "[subcom]") { CHECK(36 == vals[2]); } - TEST_CASE_METHOD(TApp, "Callbacks", "[subcom]") { auto *sub1 = app.add_subcommand("sub1"); sub1->callback([]() { throw CLI::Success(); }); @@ -1878,12 +1877,11 @@ TEST_CASE_METHOD(TApp, "DuplicateErrorsPrefix", "[subcom]") { auto *sub1 = app.add_subcommand("sub_test"); auto *sub2 = app.add_subcommand("sub_deploy"); - CHECK_THROWS_AS(app.add_subcommand("sub"), CLI::OptionAlreadyAdded); // cannot alias to an existing subcommand CHECK_THROWS_AS(sub2->alias("sub"), CLI::OptionAlreadyAdded); app.ignore_case(); - //this needs to be opposite of the subcommand the alias is being tested on to check for ambiguity + // this needs to be opposite of the subcommand the alias is being tested on to check for ambiguity sub2->ignore_case(); CHECK_THROWS_AS(sub1->alias("SUB_"), CLI::OptionAlreadyAdded); app.ignore_underscore(); @@ -1940,8 +1938,6 @@ TEST_CASE_METHOD(TApp, "AliasErrorsInOptionGroup", "[subcom]") { CHECK_THROWS_AS(sub2->name("sub1"), CLI::OptionAlreadyAdded); } - - TEST_CASE("SharedSubTests: SharedSubcommand", "[subcom]") { double val{0.0}, val2{0.0}, val3{0.0}, val4{0.0}; CLI::App app1{"test program1"}; From 433ee18d0c62c7b3701c1f51a7bf288b793d060f Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 2 May 2025 07:46:33 -0700 Subject: [PATCH 18/30] 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 | 82 +++++++++++++++++++++--------------- 4 files changed, 69 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 586d5c0fe..f2f0ff177 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 6ab331a26..7499e5a0f 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 eef7f7820..ecaa334f9 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 6310cd0e5..0df99498c 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 - }); + + // 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 dp[len1][len2]; + 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; } From 94f08d9c20e54731d16fcd39a51759a18053f3fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 14:47:49 +0000 Subject: [PATCH 19/30] style: pre-commit.ci fixes --- README.md | 7 +++++-- book/chapters/subcommands.md | 7 +++++-- examples/close_match.cpp | 33 +++++++++++++++++---------------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f2f0ff177..3e5008afd 100644 --- a/README.md +++ b/README.md @@ -914,8 +914,11 @@ 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. +- `.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 7499e5a0f..b625ec369 100644 --- a/book/chapters/subcommands.md +++ b/book/chapters/subcommands.md @@ -159,8 +159,11 @@ 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. +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 diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 0df99498c..cc36ee584 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -9,23 +9,25 @@ #include #include #include +#include #include #include #include -#include // 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) { std::size_t len1 = s1.size(), len2 = s2.size(); - if (len1 == 0) return len2; - if (len2 == 0) return len1; + if(len1 == 0) + return len2; + if(len2 == 0) + return len1; std::vector prev(len2 + 1), curr(len2 + 1); std::iota(prev.begin(), prev.end(), 0); // Fill prev with {0, 1, ..., len2} - for (std::size_t ii = 1; ii <= len1; ++ii) { + for(std::size_t ii = 1; ii <= len1; ++ii) { curr[0] = ii; - for (std::size_t jj = 1; jj <= len2; ++jj) { + 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; @@ -34,7 +36,7 @@ std::size_t levenshteinDistance(const std::string& s1, const std::string& s2) { // - 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 }); + curr[jj] = std::min({prev[jj] + 1, curr[jj - 1] + 1, prev[jj - 1] + cost}); } prev = std::exchange(curr, prev); // Swap vectors efficiently } @@ -58,7 +60,7 @@ std::pair findClosestMatch(const std::string &input, } void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3) { - //if extras are not allowed then there will be no remaining + // if extras are not allowed then there will be no remaining app->allow_extras(true); // generate a list of subcommand names auto subs = app->get_subcommands(nullptr); @@ -73,12 +75,12 @@ void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3 } } // add a callback that runs before a final callback and loops over the remaining arguments for subcommands - app->parse_complete_callback([&app, minDistance,list]() { + app->parse_complete_callback([&app, minDistance, list]() { auto extras = app->remaining(); if(extras.empty()) { return; } - + for(auto &extra : extras) { if(extra.front() != '-') { auto closest = findClosestMatch(extra, list); @@ -93,7 +95,7 @@ void addSubcommandCloseMatchDetection(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"}; @@ -105,14 +107,13 @@ int main(int argc, const char* argv[]) { app.add_subcommand("upgrade", ""); app.add_subcommand("remove", ""); app.add_subcommand("test", ""); - //enable close matching for subcommands + // 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"; + auto subs = app.get_subcommands(); + for(const auto &sub : subs) { + std::cout << sub->get_name() << "\n"; } return 0; } From 7bc28a9a69b5f535acfd78eede8d8919ce84369e Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 2 May 2025 08:50:43 -0700 Subject: [PATCH 20/30] update some code and change the levenshteinDistance to only work with C++14 or higher --- examples/CMakeLists.txt | 3 ++- examples/close_match.cpp | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ecaa334f9..b01e8f958 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -250,6 +250,7 @@ set_property(TEST retired_retired_test3 PROPERTY PASS_REGULAR_EXPRESSION "WARNIN set_property(TEST retired_deprecated PROPERTY PASS_REGULAR_EXPRESSION "deprecated.*not_deprecated") +if(CMAKE_CXX_STANDARD GREATER 13) add_cli_exe(close_match close_match.cpp) add_test(NAME close_match_test COMMAND close_match i) @@ -264,7 +265,7 @@ 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") - +endif() #-------------------------------------------- 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 cc36ee584..97e1161a2 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -13,6 +13,9 @@ #include #include #include +#include + +//only works with C++14 or higher // Levenshtein distance function code generated by chatgpt/copilot std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { @@ -75,7 +78,7 @@ void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3 } } // add a callback that runs before a final callback and loops over the remaining arguments for subcommands - app->parse_complete_callback([&app, minDistance, list]() { + app->parse_complete_callback([&app, minDistance,list=std::move(list)]() { auto extras = app->remaining(); if(extras.empty()) { return; From 414bff9d92dca69c6dc827bbf8af86a635dacbb1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 15:51:39 +0000 Subject: [PATCH 21/30] style: pre-commit.ci fixes --- examples/CMakeLists.txt | 18 +++++++++--------- examples/close_match.cpp | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b01e8f958..3d8373d38 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -251,20 +251,20 @@ set_property(TEST retired_retired_test3 PROPERTY PASS_REGULAR_EXPRESSION "WARNIN set_property(TEST retired_deprecated PROPERTY PASS_REGULAR_EXPRESSION "deprecated.*not_deprecated") if(CMAKE_CXX_STANDARD GREATER 13) -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) + 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_test PROPERTY PASS_REGULAR_EXPRESSION "install") -set_property(TEST close_match_test2 PROPERTY PASS_REGULAR_EXPRESSION "upgrade") + 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_test3 PROPERTY PASS_REGULAR_EXPRESSION "remove") -set_property(TEST close_match_test4 PROPERTY PASS_REGULAR_EXPRESSION "closest match is upgrade") + set_property(TEST close_match_test4 PROPERTY PASS_REGULAR_EXPRESSION "closest match is upgrade") endif() #-------------------------------------------- add_cli_exe(custom_parse custom_parse.cpp) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 97e1161a2..1f27270b4 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -7,15 +7,15 @@ // Code inspired by discussion from https://github.com/CLIUtils/CLI11/issues/1149 #include +#include #include #include #include #include #include #include -#include -//only works with C++14 or higher +// only works with C++14 or higher // Levenshtein distance function code generated by chatgpt/copilot std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { @@ -78,7 +78,7 @@ void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3 } } // add a callback that runs before a final callback and loops over the remaining arguments for subcommands - app->parse_complete_callback([&app, minDistance,list=std::move(list)]() { + app->parse_complete_callback([&app, minDistance, list = std::move(list)]() { auto extras = app->remaining(); if(extras.empty()) { return; From d376292ff58eb0d25e04ba9e83c30660b49f2b44 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Fri, 2 May 2025 09:01:25 -0700 Subject: [PATCH 22/30] fix warnings --- examples/close_match.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 1f27270b4..ba5a606cc 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -39,7 +39,7 @@ std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { // - 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}); + curr[jj] = (std::min)({prev[jj] + 1, curr[jj - 1] + 1, prev[jj - 1] + cost}); } prev = std::exchange(curr, prev); // Swap vectors efficiently } @@ -78,7 +78,7 @@ void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3 } } // add a callback that runs before a final callback and loops over the remaining arguments for subcommands - app->parse_complete_callback([&app, minDistance, list = std::move(list)]() { + app->parse_complete_callback([app, minDistance,list=std::move(list)]() { auto extras = app->remaining(); if(extras.empty()) { return; From c59489e64186db35d6cafebaca04b4e63a30d0d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 16:02:21 +0000 Subject: [PATCH 23/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index ba5a606cc..a81009398 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -78,7 +78,7 @@ void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3 } } // add a callback that runs before a final callback and loops over the remaining arguments for subcommands - app->parse_complete_callback([app, minDistance,list=std::move(list)]() { + app->parse_complete_callback([app, minDistance, list = std::move(list)]() { auto extras = app->remaining(); if(extras.empty()) { return; From 9b071f4804c35027dd718a12b277a439e4a1cb32 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 3 May 2025 05:22:42 -0700 Subject: [PATCH 24/30] clean up code and comments --- examples/close_match.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index a81009398..3f3779bf6 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -67,25 +67,20 @@ void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3 app->allow_extras(true); // generate a list of subcommand names auto subs = app->get_subcommands(nullptr); - std::vector list; + CLI::results_t list; for(const auto *sub : subs) { if(!sub->get_name().empty()) { - list.push_back(sub->get_name()); + list.emplace_back(sub->get_name()); } - auto aliases = sub->get_aliases(); + const 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 = std::move(list)]() { - auto extras = app->remaining(); - if(extras.empty()) { - return; - } - - for(auto &extra : extras) { - if(extra.front() != '-') { + for(auto &extra : app->remaining()) { + if(!extra.empty() && extra.front() != '-') { auto closest = findClosestMatch(extra, list); if(closest.second <= minDistance) { std::cout << "unmatched command \"" << extra << "\", closest match is " << closest.first << "\n"; @@ -101,7 +96,7 @@ void addSubcommandCloseMatchDetection(CLI::App *app, std::size_t minDistance = 3 int main(int argc, const char *argv[]) { int value{0}; - CLI::App app{"cose string App"}; + CLI::App app{"App for testing prefix matching and close string matching"}; // turn on prefix matching app.allow_subcommand_prefix_matching(); app.add_option("-v", value, "value"); From 67c1728649b7e70e2df6754276d157ef5ea6d3f7 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 3 May 2025 06:07:11 -0700 Subject: [PATCH 25/30] more code clean up --- examples/close_match.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 3f3779bf6..2fc1fac4a 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -6,15 +6,16 @@ // Code inspired by discussion from https://github.com/CLIUtils/CLI11/issues/1149 -#include + #include #include -#include #include #include #include #include +#include + // only works with C++14 or higher // Levenshtein distance function code generated by chatgpt/copilot @@ -50,7 +51,7 @@ std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { std::pair findClosestMatch(const std::string &input, const std::vector &candidates) { std::string closest; - std::size_t minDistance = (std::numeric_limits::max)(); + std::size_t minDistance{ std::string::npos }; for(const auto &candidate : candidates) { std::size_t distance = levenshteinDistance(input, candidate); if(distance < minDistance) { From a3c7f25bfe65dcd9faf77d1e5a112c557fcfc4c3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 3 May 2025 13:07:35 +0000 Subject: [PATCH 26/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 2fc1fac4a..e8763837f 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -6,7 +6,6 @@ // Code inspired by discussion from https://github.com/CLIUtils/CLI11/issues/1149 - #include #include #include @@ -51,7 +50,7 @@ std::size_t levenshteinDistance(const std::string &s1, const std::string &s2) { std::pair findClosestMatch(const std::string &input, const std::vector &candidates) { std::string closest; - std::size_t minDistance{ std::string::npos }; + std::size_t minDistance{std::string::npos}; for(const auto &candidate : candidates) { std::size_t distance = levenshteinDistance(input, candidate); if(distance < minDistance) { From 554ff27179a3f78a242938087cc78485dcf1d7c1 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 3 May 2025 09:41:52 -0700 Subject: [PATCH 27/30] try updating the codacy file --- .codacy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.codacy.yml b/.codacy.yml index 03a1e522b..2ad586dba 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -8,6 +8,8 @@ engines: enabled: true coverage: enabled: false + cppcheck: + language: c++ languages: exclude_paths: From 2f9c3b7ab98cb8a8fe360f694fcb6f51bcb9382e Mon Sep 17 00:00:00 2001 From: Philip Top Date: Sat, 3 May 2025 10:02:57 -0700 Subject: [PATCH 28/30] try ignoring style issues in codacy --- .codacy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.codacy.yml b/.codacy.yml index 2ad586dba..26506bc89 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -12,6 +12,9 @@ engines: language: c++ languages: +ignore: + - "style" + exclude_paths: - "fuzz/**/*" - "fuzz/*" From dc79e7060ba343a9dad553a9ea16bc097123bd60 Mon Sep 17 00:00:00 2001 From: Philip Top Date: Mon, 5 May 2025 07:31:37 -0700 Subject: [PATCH 29/30] reduce loc slightly for levenshteinDistance --- examples/close_match.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index e8763837f..2bc64fcf1 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -20,11 +20,9 @@ // 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; - + if (len1 == 0 || len2 == 0) { + return (std::max)(len1, len2); + } std::vector prev(len2 + 1), curr(len2 + 1); std::iota(prev.begin(), prev.end(), 0); // Fill prev with {0, 1, ..., len2} From 109eb34e29fa34d2c330c16448945b3d4e8b548f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 14:32:26 +0000 Subject: [PATCH 30/30] style: pre-commit.ci fixes --- examples/close_match.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/close_match.cpp b/examples/close_match.cpp index 2bc64fcf1..0326e6562 100644 --- a/examples/close_match.cpp +++ b/examples/close_match.cpp @@ -20,7 +20,7 @@ // 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 || len2 == 0) { + if(len1 == 0 || len2 == 0) { return (std::max)(len1, len2); } std::vector prev(len2 + 1), curr(len2 + 1);