// 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 #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) { std::size_t len1 = s1.size(), len2 = s2.size(); 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) { 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 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 = 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 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; }