mirror of
https://github.com/CLIUtils/CLI11.git
synced 2025-05-04 14:23:51 +00:00
123 lines
4.2 KiB
C++
123 lines
4.2 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 <algorithm>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <numeric>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
// 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) {
|
|
std::size_t len1 = s1.size(), len2 = s2.size();
|
|
if(len1 == 0)
|
|
return len2;
|
|
if(len2 == 0)
|
|
return len1;
|
|
|
|
std::vector<std::size_t> 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) {
|
|
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;
|
|
|
|
// 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)
|
|
std::pair<std::string, std::size_t> findClosestMatch(const std::string &input,
|
|
const std::vector<std::string> &candidates) {
|
|
std::string closest;
|
|
std::size_t minDistance = (std::numeric_limits<std::size_t>::max)();
|
|
for(const auto &candidate : candidates) {
|
|
std::size_t distance = levenshteinDistance(input, candidate);
|
|
if(distance < minDistance) {
|
|
minDistance = distance;
|
|
closest = candidate;
|
|
}
|
|
}
|
|
|
|
return {closest, minDistance};
|
|
}
|
|
|
|
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);
|
|
// generate a list of subcommand names
|
|
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=std::move(list)]() {
|
|
auto extras = app->remaining();
|
|
if(extras.empty()) {
|
|
return;
|
|
}
|
|
|
|
for(auto &extra : extras) {
|
|
if(extra.front() != '-') {
|
|
auto closest = findClosestMatch(extra, list);
|
|
if(closest.second <= minDistance) {
|
|
std::cout << "unmatched command \"" << 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"};
|
|
// 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", "");
|
|
// 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 << sub->get_name() << "\n";
|
|
}
|
|
return 0;
|
|
}
|