Use Levenshtein algo to provide suggestions if arg not found
This commit is contained in:
parent
44f0206a13
commit
8ef92cbf47
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue