// 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 #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) { size_t len1 = s1.size(), len2 = s2.size(); std::vector> dp(len1 + 1, std::vector(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 findClosestMatch(const std::string &input, const std::vector &candidates, MatchType match) { 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); 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 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; }