Refactor ArgumentParser::printBashCompletion()

This commit is contained in:
Martchus 2018-05-06 00:26:42 +02:00
parent 3c04514649
commit 872ee49979
2 changed files with 140 additions and 109 deletions

View File

@ -1015,57 +1015,80 @@ void insertSiblings(const ArgumentVector &siblings, list<const Argument *> &targ
}
}
/*!
* \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)
{
// variables to store relevant completions (arguments, pre-defined values, files/dirs)
list<const Argument *> relevantArgs, relevantPreDefinedValues;
bool completeFiles = false, completeDirs = false, noWhitespace = false;
struct ArgumentCompletionInfo {
ArgumentCompletionInfo(const ArgumentReader &reader);
// get the last argument the argument parser was able to detect successfully
const Argument *const lastDetectedArg = reader.lastArg;
size_t lastDetectedArgIndex;
const Argument *const lastDetectedArg;
size_t lastDetectedArgIndex = 0;
vector<Argument *> lastDetectedArgPath;
if (lastDetectedArg) {
lastDetectedArgIndex = reader.lastArgDenotation - argv;
lastDetectedArgPath = lastDetectedArg->path(lastDetectedArg->occurrences() - 1);
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.
*/
ArgumentCompletionInfo ArgumentParser::determineCompletionInfo(
int argc, const char *const *argv, unsigned int currentWordIndex, const ArgumentReader &reader)
{
ArgumentCompletionInfo completion(reader);
// determine last detected arg
if (completion.lastDetectedArg) {
completion.lastDetectedArgIndex = reader.lastArgDenotation - argv;
completion.lastDetectedArgPath = completion.lastDetectedArg->path(completion.lastDetectedArg->occurrences() - 1);
}
// determine last arg, omitting trailing empty args
const char *const *lastSpecifiedArg;
unsigned int lastSpecifiedArgIndex;
if (argc) {
lastSpecifiedArgIndex = static_cast<unsigned int>(argc) - 1;
lastSpecifiedArg = argv + lastSpecifiedArgIndex;
for (; lastSpecifiedArg >= argv && **lastSpecifiedArg == '\0'; --lastSpecifiedArg, --lastSpecifiedArgIndex)
completion.lastSpecifiedArgIndex = static_cast<unsigned int>(argc) - 1;
completion.lastSpecifiedArg = argv + completion.lastSpecifiedArgIndex;
for (; completion.lastSpecifiedArg >= argv && **completion.lastSpecifiedArg == '\0';
--completion.lastSpecifiedArg, --completion.lastSpecifiedArgIndex)
;
}
// determine arguments relevant for completion
bool nextArgumentOrValue;
if (lastDetectedArg && lastDetectedArg->isPresent()) {
if ((nextArgumentOrValue = (currentWordIndex > lastDetectedArgIndex))) {
// define function to add parameter values of argument as possible completions
const auto addValueCompletionsForArg = [&relevantPreDefinedValues, &completeFiles, &completeDirs](const Argument *arg) {
if (arg->valueCompletionBehaviour() & ValueCompletionBehavior::PreDefinedValues) {
relevantPreDefinedValues.push_back(arg);
// just return main arguments if no args detected
if (!completion.lastDetectedArg || !completion.lastDetectedArg->isPresent()) {
completion.nextArgumentOrValue = true;
insertSiblings(m_mainArgs, completion.relevantArgs);
completion.relevantArgs.sort(compareArgs);
return completion;
}
if (!(arg->valueCompletionBehaviour() & ValueCompletionBehavior::FileSystemIfNoPreDefinedValues)
|| !arg->preDefinedCompletionValues()) {
completeFiles = completeFiles || arg->valueCompletionBehaviour() & ValueCompletionBehavior::Files;
completeDirs = completeDirs || arg->valueCompletionBehaviour() & ValueCompletionBehavior::Directories;
completion.nextArgumentOrValue = currentWordIndex > completion.lastDetectedArgIndex;
if (!completion.nextArgumentOrValue) {
// since the argument could be detected (hopefully unambiguously?) just return it for "final completion"
completion.relevantArgs.push_back(completion.lastDetectedArg);
completion.relevantArgs.sort(compareArgs);
return completion;
}
// define function to add parameter values of argument as possible completions
const auto addValueCompletionsForArg = [&completion](const Argument *arg) {
if (arg->valueCompletionBehaviour() & ValueCompletionBehavior::PreDefinedValues) {
completion.relevantPreDefinedValues.push_back(arg);
}
if (!(arg->valueCompletionBehaviour() & ValueCompletionBehavior::FileSystemIfNoPreDefinedValues) || !arg->preDefinedCompletionValues()) {
completion.completeFiles = completion.completeFiles || arg->valueCompletionBehaviour() & ValueCompletionBehavior::Files;
completion.completeDirs = completion.completeDirs || arg->valueCompletionBehaviour() & ValueCompletionBehavior::Directories;
}
};
// detect number of specified values
auto currentValueCount = lastDetectedArg->values(lastDetectedArg->occurrences() - 1).size();
auto currentValueCount = completion.lastDetectedArg->values(completion.lastDetectedArg->occurrences() - 1).size();
// ignore values which are specified after the current word
if (currentValueCount) {
const auto currentWordIndexRelativeToLastDetectedArg = currentWordIndex - lastDetectedArgIndex;
const auto currentWordIndexRelativeToLastDetectedArg = currentWordIndex - completion.lastDetectedArgIndex;
if (currentValueCount > currentWordIndexRelativeToLastDetectedArg) {
currentValueCount -= currentWordIndexRelativeToLastDetectedArg;
} else {
@ -1075,8 +1098,8 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi
// add value completions for implicit child if there are no value specified and there are no values required by the
// last detected argument itself
if (!currentValueCount && !lastDetectedArg->requiredValueCount()) {
for (const Argument *child : lastDetectedArg->subArguments()) {
if (!currentValueCount && !completion.lastDetectedArg->requiredValueCount()) {
for (const Argument *child : completion.lastDetectedArg->subArguments()) {
if (child->isImplicit() && child->requiredValueCount()) {
addValueCompletionsForArg(child);
break;
@ -1085,44 +1108,50 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi
}
// add value completions for last argument if there are further values required
if (lastDetectedArg->requiredValueCount() == Argument::varValueCount || (currentValueCount < lastDetectedArg->requiredValueCount())) {
addValueCompletionsForArg(lastDetectedArg);
if (completion.lastDetectedArg->requiredValueCount() == Argument::varValueCount
|| (currentValueCount < completion.lastDetectedArg->requiredValueCount())) {
addValueCompletionsForArg(completion.lastDetectedArg);
}
if (lastDetectedArg->requiredValueCount() == Argument::varValueCount
|| lastDetectedArg->values(lastDetectedArg->occurrences() - 1).size() >= lastDetectedArg->requiredValueCount()) {
if (completion.lastDetectedArg->requiredValueCount() == Argument::varValueCount
|| completion.lastDetectedArg->values(completion.lastDetectedArg->occurrences() - 1).size()
>= completion.lastDetectedArg->requiredValueCount()) {
// sub arguments of the last arg are possible completions
for (const Argument *subArg : lastDetectedArg->subArguments()) {
for (const Argument *subArg : completion.lastDetectedArg->subArguments()) {
if (subArg->occurrences() < subArg->maxOccurrences()) {
relevantArgs.push_back(subArg);
completion.relevantArgs.push_back(subArg);
}
}
// siblings of parents are possible completions as well
for (auto parentArgument = lastDetectedArgPath.crbegin(), end = lastDetectedArgPath.crend();; ++parentArgument) {
insertSiblings(parentArgument != end ? (*parentArgument)->subArguments() : m_mainArgs, relevantArgs);
for (auto parentArgument = completion.lastDetectedArgPath.crbegin(), end = completion.lastDetectedArgPath.crend();; ++parentArgument) {
insertSiblings(parentArgument != end ? (*parentArgument)->subArguments() : m_mainArgs, completion.relevantArgs);
if (parentArgument == end) {
break;
}
}
}
} else {
// since the argument could be detected (hopefully unambiguously?) just return it for "final completion"
relevantArgs.push_back(lastDetectedArg);
completion.relevantArgs.sort(compareArgs);
return completion;
}
} else {
// no arguments detected -> just use main arguments for completion
nextArgumentOrValue = true;
insertSiblings(m_mainArgs, relevantArgs);
}
/*!
* \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)
{
// determine completion info
const auto completionInfo(determineCompletionInfo(argc, argv, currentWordIndex, reader));
// read the "opening" (started but not finished argument denotation)
const char *opening = nullptr;
string compoundOpening;
size_t openingLen, compoundOpeningStartLen = 0;
unsigned char openingDenotationType = Value;
if (argc && nextArgumentOrValue) {
if (argc && completionInfo.nextArgumentOrValue) {
if (currentWordIndex < static_cast<unsigned int>(argc)) {
opening = argv[currentWordIndex];
// For some reason completions for eg. "set --values disk=1 tag=a" are splitted so the
@ -1130,7 +1159,7 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi
// This is not how values are treated by the argument parser. Hence the opening
// must be joined again. In this case only the part after the equation sign needs to be
// provided for completion so compoundOpeningStartLen is set to number of characters to skip.
size_t minCurrentWordIndex = (lastDetectedArg ? lastDetectedArgIndex : 0);
size_t minCurrentWordIndex = (completionInfo.lastDetectedArg ? completionInfo.lastDetectedArgIndex : 0);
if (currentWordIndex > minCurrentWordIndex && !strcmp(opening, "=")) {
compoundOpening.reserve(compoundOpeningStartLen = strlen(argv[--currentWordIndex]) + 1);
compoundOpening = argv[currentWordIndex];
@ -1145,24 +1174,23 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi
opening = compoundOpening.data();
}
} else {
opening = *lastSpecifiedArg;
opening = *completionInfo.lastSpecifiedArg;
}
*opening == '-' && (++opening, ++openingDenotationType) && *opening == '-' && (++opening, ++openingDenotationType);
openingLen = strlen(opening);
}
relevantArgs.sort(compareArgs);
// print "COMPREPLY" bash array
cout << "COMPREPLY=(";
// -> completions for parameter values
for (const Argument *arg : relevantPreDefinedValues) {
bool noWhitespace = false;
for (const Argument *arg : completionInfo.relevantPreDefinedValues) {
if (arg->valueCompletionBehaviour() & ValueCompletionBehavior::InvokeCallback && arg->m_callbackFunction) {
arg->m_callbackFunction(arg->isPresent() ? arg->m_occurrences.front() : ArgumentOccurrence(Argument::varValueCount));
}
if (arg->preDefinedCompletionValues()) {
bool appendEquationSign = arg->valueCompletionBehaviour() & ValueCompletionBehavior::AppendEquationSign;
if (argc && currentWordIndex <= lastSpecifiedArgIndex && opening) {
if (argc && currentWordIndex <= completionInfo.lastSpecifiedArgIndex && opening) {
if (openingDenotationType == Value) {
bool wordStart = true, ok = false, equationSignAlreadyPresent = false;
size_t wordIndex = 0;
@ -1247,8 +1275,8 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi
}
}
// -> completions for further arguments
for (const Argument *arg : relevantArgs) {
if (argc && currentWordIndex <= lastSpecifiedArgIndex && opening) {
for (const Argument *arg : completionInfo.relevantArgs) {
if (argc && currentWordIndex <= completionInfo.lastSpecifiedArgIndex && opening) {
switch (openingDenotationType) {
case Value:
if (!arg->denotesOperation() || strncmp(arg->name(), opening, openingLen)) {
@ -1264,10 +1292,10 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi
}
}
if (opening && openingDenotationType == Abbreviation && !nextArgumentOrValue) {
if (opening && openingDenotationType == Abbreviation && !completionInfo.nextArgumentOrValue) {
// TODO: add test for this case
cout << '\'' << '-' << opening << arg->abbreviation() << '\'' << ' ';
} else if (lastDetectedArg && reader.argDenotationType == Abbreviation && !nextArgumentOrValue) {
} else if (completionInfo.lastDetectedArg && reader.argDenotationType == Abbreviation && !completionInfo.nextArgumentOrValue) {
if (reader.argv == reader.end) {
cout << '\'' << *(reader.argv - 1) << '\'' << ' ';
}
@ -1281,7 +1309,7 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi
// -> if there's already an "opening", determine the dir part and the file part
string actualDir, actualFile;
bool haveFileOrDirCompletions = false;
if (argc && currentWordIndex == lastSpecifiedArgIndex && opening) {
if (argc && currentWordIndex == completionInfo.lastSpecifiedArgIndex && opening) {
// the "opening" might contain escaped characters which need to be unescaped first (let's hope this covers all possible escapings)
string unescapedOpening(opening);
findAndReplace<string>(unescapedOpening, "\\ ", " ");
@ -1320,15 +1348,15 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi
// -> completion for files and dirs
DirectoryEntryType entryTypes = DirectoryEntryType::None;
if (completeFiles) {
if (completionInfo.completeFiles) {
entryTypes |= DirectoryEntryType::File;
}
if (completeDirs) {
if (completionInfo.completeDirs) {
entryTypes |= DirectoryEntryType::Directory;
}
if (entryTypes != DirectoryEntryType::None) {
const string replace("'"), with("'\"'\"'");
if (argc && currentWordIndex <= lastSpecifiedArgIndex && opening) {
if (argc && currentWordIndex <= completionInfo.lastSpecifiedArgIndex && opening) {
list<string> entries = directoryEntries(actualDir.c_str(), entryTypes);
findAndReplace(actualDir, replace, with);
for (string &dirEntry : entries) {

View File

@ -365,6 +365,8 @@ template <typename... TargetType> std::vector<std::tuple<TargetType...>> Argumen
return res;
}
struct ArgumentCompletionInfo;
class CPP_UTILITIES_EXPORT ArgumentParser {
friend ArgumentParserTests;
friend ArgumentReader;
@ -402,6 +404,7 @@ 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);
void checkConstraints(const ArgumentVector &args);
static void invokeCallbacks(const ArgumentVector &args);