From 8ef92cbf478a93075099b6808d935da9d4932496 Mon Sep 17 00:00:00 2001 From: Martchus Date: Mon, 7 May 2018 21:59:23 +0200 Subject: [PATCH] Use Levenshtein algo to provide suggestions if arg not found --- application/argumentparser.cpp | 158 +++++++++++++++++++++++++++------ application/argumentparser.h | 6 +- tests/argumentparsertests.cpp | 6 +- 3 files changed, 139 insertions(+), 31 deletions(-) diff --git a/application/argumentparser.cpp b/application/argumentparser.cpp index 58cd69f..4d8cd06 100644 --- a/application/argumentparser.cpp +++ b/application/argumentparser.cpp @@ -7,11 +7,13 @@ #include "../conversion/stringconversion.h" #include "../io/ansiescapecodes.h" #include "../io/path.h" +#include "../misc/levenshtein.h" #include #include #include #include +#include #include #include @@ -36,6 +38,72 @@ enum ArgumentDenotationType : unsigned char { FullName = 2 /**< full argument name */ }; +/*! + * \brief The ArgumentCompletionInfo struct holds information internally used for shell completion and suggestions. + */ +struct ArgumentCompletionInfo { + ArgumentCompletionInfo(const ArgumentReader &reader); + + const Argument *const lastDetectedArg; + size_t lastDetectedArgIndex = 0; + vector lastDetectedArgPath; + list relevantArgs; + list relevantPreDefinedValues; + const char *const *lastSpecifiedArg = nullptr; + unsigned int lastSpecifiedArgIndex = 0; + bool nextArgumentOrValue = false; + bool completeFiles = false, completeDirs = false; +}; + +/*! + * \brief Constructs a new completion info for the specified \a reader. + * \remarks Only assigns some defaults. Use ArgumentParser::determineCompletionInfo() to populate the struct with actual data. + */ +ArgumentCompletionInfo::ArgumentCompletionInfo(const ArgumentReader &reader) + : lastDetectedArg(reader.lastArg) +{ +} + +struct ArgumentSuggestion { + ArgumentSuggestion(const char *unknownArg, size_t unknownArgSize, const char *suggestion); + ArgumentSuggestion(const char *unknownArg, size_t unknownArgSize, const char *suggestion, size_t suggestionSize); + bool operator<(const ArgumentSuggestion &other) const; + bool operator==(const ArgumentSuggestion &other) const; + void addTo(multiset &suggestions, size_t limit) const; + + const char *const suggestion; + const size_t suggestionSize; + const size_t editingDistance; +}; + +ArgumentSuggestion::ArgumentSuggestion(const char *unknownArg, size_t unknownArgSize, const char *suggestion, size_t suggestionSize) + : suggestion(suggestion) + , suggestionSize(suggestionSize) + , editingDistance(MiscUtilities::computeDamerauLevenshteinDistance(unknownArg, unknownArgSize, suggestion, suggestionSize)) +{ +} + +ArgumentSuggestion::ArgumentSuggestion(const char *unknownArg, size_t unknownArgSize, const char *suggestion) + : ArgumentSuggestion(unknownArg, unknownArgSize, suggestion, strlen(suggestion)) +{ +} + +bool ArgumentSuggestion::operator<(const ArgumentSuggestion &other) const +{ + return editingDistance < other.editingDistance; +} + +void ArgumentSuggestion::addTo(multiset &suggestions, size_t limit) const +{ + if (suggestions.size() >= limit && !(*this < *--suggestions.end())) { + return; + } + suggestions.emplace(*this); + while (suggestions.size() > limit) { + suggestions.erase(--suggestions.end()); + } +} + /*! * \class ArgumentReader * \brief The ArgumentReader class internally encapsulates the process of reading command line arguments. @@ -886,8 +954,8 @@ void ArgumentParser::readArgs(int argc, const char *const *argv) const bool allArgsProcessed(reader.read()); NoColorArgument::apply(); if (!completionMode && !allArgsProcessed) { - - throw Failure(argsToString("The specified argument \"", *reader.argv, "\" is unknown.")); + const auto suggestions(findSuggestions(argc, argv, currentWordIndex, reader)); + throw Failure(argsToString("The specified argument \"", *reader.argv, "\" is unknown.", suggestions)); } if (completionMode) { @@ -1015,30 +1083,11 @@ void insertSiblings(const ArgumentVector &siblings, list &targ } } -struct ArgumentCompletionInfo { - ArgumentCompletionInfo(const ArgumentReader &reader); - - const Argument *const lastDetectedArg; - size_t lastDetectedArgIndex = 0; - vector lastDetectedArgPath; - list relevantArgs; - list relevantPreDefinedValues; - const char *const *lastSpecifiedArg = nullptr; - unsigned int lastSpecifiedArgIndex = 0; - bool nextArgumentOrValue = false; - bool completeFiles = false, completeDirs = false; -}; - -ArgumentCompletionInfo::ArgumentCompletionInfo(const ArgumentReader &reader) - : lastDetectedArg(reader.lastArg) -{ -} - /*! * \brief Determines arguments relevant for Bash completion or suggestions in case of typo. */ ArgumentCompletionInfo ArgumentParser::determineCompletionInfo( - int argc, const char *const *argv, unsigned int currentWordIndex, const ArgumentReader &reader) + int argc, const char *const *argv, unsigned int currentWordIndex, const ArgumentReader &reader) const { ArgumentCompletionInfo completion(reader); @@ -1132,19 +1181,76 @@ ArgumentCompletionInfo ArgumentParser::determineCompletionInfo( } } - completion.relevantArgs.sort(compareArgs); return completion; } +/*! + * \brief Returns the suggestion string printed in error case due to unknown arguments. + */ +string ArgumentParser::findSuggestions(int argc, const char *const *argv, unsigned int cursorPos, const ArgumentReader &reader) const +{ + // determine completion info + const auto completionInfo(determineCompletionInfo(argc, argv, cursorPos, reader)); + + // find best suggestions limiting the results to 2 + const auto *const unknownArg(*reader.argv); + const auto unknownArgSize(strlen(unknownArg)); + multiset bestSuggestions; + // -> consider relevant arguments + for (const Argument *const arg : completionInfo.relevantArgs) { + ArgumentSuggestion(unknownArg, unknownArgSize, arg->name()).addTo(bestSuggestions, 2); + } + // -> consider relevant values + for (const Argument *const arg : completionInfo.relevantPreDefinedValues) { + for (const char *i = arg->preDefinedCompletionValues(); *i; ++i) { + const char *const wordStart(i); + const char *wordEnd(wordStart + 1); + for (; *wordEnd && *wordEnd != ' '; ++wordEnd) + ; + ArgumentSuggestion(unknownArg, unknownArgSize, wordStart, static_cast(wordEnd - wordStart)).addTo(bestSuggestions, 2); + i = wordEnd; + } + } + + // format suggestion + string suggestionStr; + if (const auto suggestionCount = bestSuggestions.size()) { + // allocate memory + size_t requiredSize = 15; + for (const auto &suggestion : bestSuggestions) { + requiredSize += suggestion.suggestionSize + 2; + } + suggestionStr.reserve(requiredSize); + + // add each suggestion to end up with something like "Did you mean status (1), pause (3), cat (4), edit (5) or rescan-all (8)?" + suggestionStr += "\nDid you mean "; + size_t i = 0; + for (const auto &suggestion : bestSuggestions) { + if (++i == suggestionCount) { + suggestionStr += " or "; + } else if (i > 1) { + suggestionStr += ", "; + } + suggestionStr.append(suggestion.suggestion, suggestion.suggestionSize); + } + suggestionStr += '?'; + } + return suggestionStr; +} + /*! * \brief Prints the bash completion for the specified arguments and the specified \a lastPath. * \remarks Arguments must have been parsed before with readSpecifiedArgs(). When calling this method, completionMode must * be set to true. */ -void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsigned int currentWordIndex, const ArgumentReader &reader) +void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsigned int currentWordIndex, const ArgumentReader &reader) const { - // determine completion info - const auto completionInfo(determineCompletionInfo(argc, argv, currentWordIndex, reader)); + // determine completion info and sort relevant arguments + const auto completionInfo([&] { + auto clutteredCompletionInfo(determineCompletionInfo(argc, argv, currentWordIndex, reader)); + clutteredCompletionInfo.relevantArgs.sort(compareArgs); + return clutteredCompletionInfo; + }()); // read the "opening" (started but not finished argument denotation) const char *opening = nullptr; diff --git a/application/argumentparser.h b/application/argumentparser.h index 3215f87..fcb4536 100644 --- a/application/argumentparser.h +++ b/application/argumentparser.h @@ -404,8 +404,10 @@ public: private: // declare internal operations IF_DEBUG_BUILD(void verifyArgs(const ArgumentVector &args);) - ArgumentCompletionInfo determineCompletionInfo(int argc, const char *const *argv, unsigned int currentWordIndex, const ArgumentReader &reader); - void printBashCompletion(int argc, const char *const *argv, unsigned int cursorPos, const ArgumentReader &reader); + ArgumentCompletionInfo determineCompletionInfo( + int argc, const char *const *argv, unsigned int currentWordIndex, const ArgumentReader &reader) const; + std::string findSuggestions(int argc, const char *const *argv, unsigned int cursorPos, const ArgumentReader &reader) const; + void printBashCompletion(int argc, const char *const *argv, unsigned int cursorPos, const ArgumentReader &reader) const; void checkConstraints(const ArgumentVector &args); static void invokeCallbacks(const ArgumentVector &args); diff --git a/tests/argumentparsertests.cpp b/tests/argumentparsertests.cpp index f25ce22..e0e50bc 100644 --- a/tests/argumentparsertests.cpp +++ b/tests/argumentparsertests.cpp @@ -196,7 +196,7 @@ void ArgumentParserTests::testParsing() parser.parseArgs(6, argv3); CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { - CPPUNIT_ASSERT_EQUAL("The specified argument \"album\" is unknown."s, string(e.what())); + CPPUNIT_ASSERT_EQUAL("The specified argument \"album\" is unknown.\nDid you mean get or help?"s, string(e.what())); } // warning about unknown argument @@ -300,7 +300,7 @@ void ArgumentParserTests::testParsing() CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); - CPPUNIT_ASSERT_EQUAL("The specified argument \"-f\" is unknown."s, string(e.what())); + CPPUNIT_ASSERT_EQUAL("The specified argument \"-f\" is unknown.\nDid you mean get or help?"s, string(e.what())); } // equation sign syntax @@ -400,7 +400,7 @@ void ArgumentParserTests::testParsing() CPPUNIT_FAIL("Exception expected."); } catch (const Failure &e) { CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); - CPPUNIT_ASSERT_EQUAL("The specified argument \"--hel\" is unknown."s, string(e.what())); + CPPUNIT_ASSERT_EQUAL("The specified argument \"--hel\" is unknown.\nDid you mean help or get?"s, string(e.what())); } // nested operations