Use Levenshtein algo to provide suggestions if arg not found

This commit is contained in:
Martchus 2018-05-07 21:59:23 +02:00
parent 44f0206a13
commit 8ef92cbf47
3 changed files with 139 additions and 31 deletions

View File

@ -7,11 +7,13 @@
#include "../conversion/stringconversion.h" #include "../conversion/stringconversion.h"
#include "../io/ansiescapecodes.h" #include "../io/ansiescapecodes.h"
#include "../io/path.h" #include "../io/path.h"
#include "../misc/levenshtein.h"
#include <algorithm> #include <algorithm>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
#include <set>
#include <sstream> #include <sstream>
#include <string> #include <string>
@ -36,6 +38,72 @@ enum ArgumentDenotationType : unsigned char {
FullName = 2 /**< full argument name */ 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<Argument *> lastDetectedArgPath;
list<const Argument *> relevantArgs;
list<const Argument *> 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<ArgumentSuggestion> &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<ArgumentSuggestion> &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 * \class ArgumentReader
* \brief The ArgumentReader class internally encapsulates the process of reading command line arguments. * \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()); const bool allArgsProcessed(reader.read());
NoColorArgument::apply(); NoColorArgument::apply();
if (!completionMode && !allArgsProcessed) { if (!completionMode && !allArgsProcessed) {
const auto suggestions(findSuggestions(argc, argv, currentWordIndex, reader));
throw Failure(argsToString("The specified argument \"", *reader.argv, "\" is unknown.")); throw Failure(argsToString("The specified argument \"", *reader.argv, "\" is unknown.", suggestions));
} }
if (completionMode) { if (completionMode) {
@ -1015,30 +1083,11 @@ void insertSiblings(const ArgumentVector &siblings, list<const Argument *> &targ
} }
} }
struct ArgumentCompletionInfo {
ArgumentCompletionInfo(const ArgumentReader &reader);
const Argument *const lastDetectedArg;
size_t lastDetectedArgIndex = 0;
vector<Argument *> lastDetectedArgPath;
list<const Argument *> relevantArgs;
list<const Argument *> 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. * \brief Determines arguments relevant for Bash completion or suggestions in case of typo.
*/ */
ArgumentCompletionInfo ArgumentParser::determineCompletionInfo( 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); ArgumentCompletionInfo completion(reader);
@ -1132,19 +1181,76 @@ ArgumentCompletionInfo ArgumentParser::determineCompletionInfo(
} }
} }
completion.relevantArgs.sort(compareArgs);
return completion; 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<ArgumentSuggestion> 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<size_t>(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. * \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 * \remarks Arguments must have been parsed before with readSpecifiedArgs(). When calling this method, completionMode must
* be set to true. * 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 // determine completion info and sort relevant arguments
const auto completionInfo(determineCompletionInfo(argc, argv, currentWordIndex, reader)); 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) // read the "opening" (started but not finished argument denotation)
const char *opening = nullptr; const char *opening = nullptr;

View File

@ -404,8 +404,10 @@ public:
private: private:
// declare internal operations // declare internal operations
IF_DEBUG_BUILD(void verifyArgs(const ArgumentVector &args);) IF_DEBUG_BUILD(void verifyArgs(const ArgumentVector &args);)
ArgumentCompletionInfo determineCompletionInfo(int argc, const char *const *argv, unsigned int currentWordIndex, const ArgumentReader &reader); ArgumentCompletionInfo determineCompletionInfo(
void printBashCompletion(int argc, const char *const *argv, unsigned int cursorPos, const ArgumentReader &reader); 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); void checkConstraints(const ArgumentVector &args);
static void invokeCallbacks(const ArgumentVector &args); static void invokeCallbacks(const ArgumentVector &args);

View File

@ -196,7 +196,7 @@ void ArgumentParserTests::testParsing()
parser.parseArgs(6, argv3); parser.parseArgs(6, argv3);
CPPUNIT_FAIL("Exception expected."); CPPUNIT_FAIL("Exception expected.");
} catch (const Failure &e) { } 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 // warning about unknown argument
@ -300,7 +300,7 @@ void ArgumentParserTests::testParsing()
CPPUNIT_FAIL("Exception expected."); CPPUNIT_FAIL("Exception expected.");
} catch (const Failure &e) { } catch (const Failure &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); 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 // equation sign syntax
@ -400,7 +400,7 @@ void ArgumentParserTests::testParsing()
CPPUNIT_FAIL("Exception expected."); CPPUNIT_FAIL("Exception expected.");
} catch (const Failure &e) { } catch (const Failure &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent()); 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 // nested operations