1
0
mirror of https://github.com/CLIUtils/CLI11.git synced 2025-04-29 12:13:52 +00:00
CLI11/examples/close_match.cpp
2025-04-22 18:27:05 -07:00

135 lines
4.0 KiB
C++

// 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 <CLI/CLI.hpp>
#include <iostream>
#include <limits>
#include <string>
#include <utility>
#include <vector>
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 {
return std::string::npos;
}
}
}
// 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<std::vector<std::size_t>> dp(len1 + 1, std::vector<std::size_t>(len2 + 1));
for(size_t ii = 0; ii <= len1; ++ii)
dp[ii][0] = ii;
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
});
}
}
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<std::string, std::size_t> findClosestMatch(const std::string &input, const std::vector<std::string> &candidates,MatchType match) {
std::string closest;
int minDistance = (std::numeric_limits<std::size_t>::max)();
std::size_t distance=minDistance;
for(const auto &candidate : candidates) {
if (match == MatchType::proximity)
{
distance = levenshteinDistance(input, candidate);
}
else
{
distance = prefixMatch(input, candidate);
}
if(distance < minDistance) {
minDistance = distance;
closest = candidate;
}
}
return {closest, minDistance};
}
void addCloseMatchDetection(CLI::App* app, MatchType match)
{
app->allow_extras(true);
app->parse_complete_callback([&app,match]() {
auto extras = app->remaining();
if(extras.empty()) {
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) {
if(extra.front() != '-') {
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;
}