Add support for bash completion
This commit is contained in:
parent
209b953754
commit
03094a1f61
|
@ -72,12 +72,14 @@ set(CMAKE_MODULE_FILES
|
||||||
cmake/modules/TemplateFinder.cmake
|
cmake/modules/TemplateFinder.cmake
|
||||||
cmake/modules/Doxygen.cmake
|
cmake/modules/Doxygen.cmake
|
||||||
cmake/modules/ListToString.cmake
|
cmake/modules/ListToString.cmake
|
||||||
|
cmake/modules/ShellCompletion.cmake
|
||||||
)
|
)
|
||||||
set(CMAKE_TEMPLATE_FILES
|
set(CMAKE_TEMPLATE_FILES
|
||||||
cmake/templates/Config.cmake.in
|
cmake/templates/Config.cmake.in
|
||||||
cmake/templates/config.h.in
|
cmake/templates/config.h.in
|
||||||
cmake/templates/desktop.in
|
cmake/templates/desktop.in
|
||||||
cmake/templates/doxygen.in
|
cmake/templates/doxygen.in
|
||||||
|
cmake/templates/bash-completion.sh.in
|
||||||
)
|
)
|
||||||
if(MINGW)
|
if(MINGW)
|
||||||
list(APPEND CMAKE_TEMPLATE_FILES
|
list(APPEND CMAKE_TEMPLATE_FILES
|
||||||
|
|
|
@ -3,17 +3,21 @@
|
||||||
#include "./failure.h"
|
#include "./failure.h"
|
||||||
|
|
||||||
#include "../conversion/stringconversion.h"
|
#include "../conversion/stringconversion.h"
|
||||||
#include "../misc/random.h"
|
#include "../io/path.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#ifdef LOGGING_ENABLED
|
||||||
|
# include <fstream>
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
using namespace ConversionUtilities;
|
using namespace ConversionUtilities;
|
||||||
|
using namespace IoUtilities;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \namespace ApplicationUtilities
|
* \namespace ApplicationUtilities
|
||||||
|
@ -71,7 +75,9 @@ Argument::Argument(const char *name, char abbreviation, const char *description,
|
||||||
m_denotesOperation(false),
|
m_denotesOperation(false),
|
||||||
m_requiredValueCount(0),
|
m_requiredValueCount(0),
|
||||||
m_implicit(false),
|
m_implicit(false),
|
||||||
m_isMainArg(false)
|
m_isMainArg(false),
|
||||||
|
m_valueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::Files | ValueCompletionBehavior::Directories | ValueCompletionBehavior::FileSystemIfNoPreDefinedValues),
|
||||||
|
m_preDefinedCompletionValues(nullptr)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -230,15 +236,6 @@ Argument *Argument::conflictsWithArgument() const
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief Resets occurrences and values.
|
|
||||||
*/
|
|
||||||
void Argument::reset()
|
|
||||||
{
|
|
||||||
m_indices.clear();
|
|
||||||
m_values.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \class ApplicationUtilities::ArgumentParser
|
* \class ApplicationUtilities::ArgumentParser
|
||||||
* \brief The ArgumentParser class provides a means for handling command line arguments.
|
* \brief The ArgumentParser class provides a means for handling command line arguments.
|
||||||
|
@ -258,7 +255,7 @@ void Argument::reset()
|
||||||
ArgumentParser::ArgumentParser() :
|
ArgumentParser::ArgumentParser() :
|
||||||
m_actualArgc(0),
|
m_actualArgc(0),
|
||||||
m_executable(nullptr),
|
m_executable(nullptr),
|
||||||
m_ignoreUnknownArgs(false),
|
m_unknownArgBehavior(UnknownArgumentBehavior::Fail),
|
||||||
m_defaultArg(nullptr)
|
m_defaultArg(nullptr)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
@ -374,21 +371,57 @@ Argument *ArgumentParser::findArg(const ArgumentVector &arguments, const Argumen
|
||||||
*/
|
*/
|
||||||
void ArgumentParser::parseArgs(int argc, const char *const *argv)
|
void ArgumentParser::parseArgs(int argc, const char *const *argv)
|
||||||
{
|
{
|
||||||
|
#ifdef LOGGING_ENABLED
|
||||||
|
{
|
||||||
|
fstream logFile("/tmp/args.log", ios_base::out);
|
||||||
|
for(const char *const *i = argv, *const *end = argv + argc; i != end; ++i) {
|
||||||
|
logFile << *i << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
IF_DEBUG_BUILD(verifyArgs(m_mainArgs);)
|
IF_DEBUG_BUILD(verifyArgs(m_mainArgs);)
|
||||||
m_actualArgc = 0;
|
m_actualArgc = 0;
|
||||||
if(argc) {
|
if(argc) {
|
||||||
|
// the first argument is the executable name
|
||||||
m_executable = *argv;
|
m_executable = *argv;
|
||||||
|
|
||||||
|
// check for further arguments
|
||||||
if(--argc) {
|
if(--argc) {
|
||||||
|
// if the first argument (after executable name) is "--bash-completion-for" bash completion for the following arguments is requested
|
||||||
|
bool completionMode = !strcmp(*++argv, "--bash-completion-for");
|
||||||
|
unsigned int currentWordIndex;
|
||||||
|
if(completionMode) {
|
||||||
|
// the first argument after "--bash-completion-for" is the index of the current word
|
||||||
|
try {
|
||||||
|
currentWordIndex = (--argc ? stringToNumber<unsigned int, string>(*(++argv)) : 0);
|
||||||
|
++argv, --argc;
|
||||||
|
} catch(const ConversionException &) {
|
||||||
|
currentWordIndex = argc - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// those variables are modified by readSpecifiedArgs() and reflect the current reading position
|
||||||
size_t index = 0;
|
size_t index = 0;
|
||||||
++argv;
|
Argument *lastDetectedArgument = nullptr;
|
||||||
vector<Argument *> path;
|
|
||||||
path.reserve(4);
|
// read specified arguments
|
||||||
readSpecifiedArgs(m_mainArgs, index, argv, argv + argc, path);
|
try {
|
||||||
|
const char *const *argv2 = argv;
|
||||||
|
readSpecifiedArgs(m_mainArgs, index, argv2, argv + (completionMode ? min(static_cast<unsigned int>(argc), currentWordIndex + 1) : static_cast<unsigned int>(argc)), lastDetectedArgument, completionMode);
|
||||||
|
} catch(const Failure &) {
|
||||||
|
if(!completionMode) {
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(completionMode) {
|
||||||
|
printBashCompletion(argc, argv, currentWordIndex, lastDetectedArgument);
|
||||||
|
exit(0); // prevent the applicaton to continue with the regular execution
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// no arguments specified -> set default argument as present
|
// no arguments specified -> flag default argument as present if one is assigned
|
||||||
if(m_defaultArg) {
|
if(m_defaultArg) {
|
||||||
m_defaultArg->m_indices.push_back(0);
|
m_defaultArg->m_occurances.emplace_back(0);
|
||||||
m_defaultArg->m_values.emplace_back();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
checkConstraints(m_mainArgs);
|
checkConstraints(m_mainArgs);
|
||||||
|
@ -442,19 +475,21 @@ void ApplicationUtilities::ArgumentParser::verifyArgs(const ArgumentVector &args
|
||||||
* \brief Reads the specified commands line arguments.
|
* \brief Reads the specified commands line arguments.
|
||||||
* \remarks Results are stored in Argument instances added as main arguments and sub arguments.
|
* \remarks Results are stored in Argument instances added as main arguments and sub arguments.
|
||||||
*/
|
*/
|
||||||
void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char *const *&argv, const char *const *end, std::vector<Argument *> ¤tPath)
|
void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char *const *&argv, const char *const *end, Argument *&lastArg, bool completionMode)
|
||||||
{
|
{
|
||||||
Argument *lastArg = nullptr;
|
Argument *const parentArg = lastArg;
|
||||||
|
const vector<Argument *> &parentPath = parentArg ? parentArg->path(parentArg->occurrences() - 1) : vector<Argument *>();
|
||||||
|
Argument *lastArgInLevel = nullptr;
|
||||||
vector<const char *> *values = nullptr;
|
vector<const char *> *values = nullptr;
|
||||||
while(argv != end) {
|
while(argv != end) {
|
||||||
if(values && lastArg->requiredValueCount() != static_cast<size_t>(-1) && values->size() < lastArg->requiredValueCount()) {
|
if(values && lastArgInLevel->requiredValueCount() != static_cast<size_t>(-1) && values->size() < lastArgInLevel->requiredValueCount()) {
|
||||||
// there are still values to read
|
// there are still values to read
|
||||||
values->emplace_back(*argv);
|
values->emplace_back(*argv);
|
||||||
++index, ++argv;
|
++index, ++argv;
|
||||||
} else {
|
} else {
|
||||||
// determine denotation type
|
// determine denotation type
|
||||||
const char *argDenotation = *argv;
|
const char *argDenotation = *argv;
|
||||||
if(!*argDenotation && (!lastArg || values->size() >= lastArg->requiredValueCount())) {
|
if(!*argDenotation && (!lastArgInLevel || values->size() >= lastArgInLevel->requiredValueCount())) {
|
||||||
// skip empty arguments
|
// skip empty arguments
|
||||||
++index, ++argv;
|
++index, ++argv;
|
||||||
continue;
|
continue;
|
||||||
|
@ -469,7 +504,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
|
||||||
size_t argDenLen;
|
size_t argDenLen;
|
||||||
if(argDenotationType != Value) {
|
if(argDenotationType != Value) {
|
||||||
const char *const equationPos = strchr(argDenotation, '=');
|
const char *const equationPos = strchr(argDenotation, '=');
|
||||||
for(argDenLen = equationPos ? static_cast<size_t>(equationPos - argDenotation) : strlen(argDenotation); ; matchingArg = nullptr) {
|
for(argDenLen = equationPos ? static_cast<size_t>(equationPos - argDenotation) : strlen(argDenotation); argDenLen; matchingArg = nullptr) {
|
||||||
// search for arguments by abbreviation or name depending on the denotation type
|
// search for arguments by abbreviation or name depending on the denotation type
|
||||||
if(argDenotationType == Abbreviation) {
|
if(argDenotationType == Abbreviation) {
|
||||||
for(Argument *arg : args) {
|
for(Argument *arg : args) {
|
||||||
|
@ -481,7 +516,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for(Argument *arg : args) {
|
for(Argument *arg : args) {
|
||||||
if(arg->name() && !strncmp(arg->name(), argDenotation, argDenLen)) {
|
if(arg->name() && !strncmp(arg->name(), argDenotation, argDenLen) && *(arg->name() + argDenLen) == '\0') {
|
||||||
matchingArg = arg;
|
matchingArg = arg;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -490,21 +525,18 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
|
||||||
|
|
||||||
if(matchingArg) {
|
if(matchingArg) {
|
||||||
// an argument matched the specified denotation
|
// an argument matched the specified denotation
|
||||||
matchingArg->m_indices.push_back(index);
|
matchingArg->m_occurances.emplace_back(index, parentPath, parentArg);
|
||||||
|
|
||||||
// prepare reading parameter values
|
// prepare reading parameter values
|
||||||
matchingArg->m_values.emplace_back();
|
values = &matchingArg->m_occurances.back().values;
|
||||||
values = &matchingArg->m_values.back();
|
|
||||||
if(equationPos) {
|
if(equationPos) {
|
||||||
values->push_back(equationPos + 1);
|
values->push_back(equationPos + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// read sub arguments if no abbreviated argument follows
|
// read sub arguments if no abbreviated argument follows
|
||||||
++index, ++m_actualArgc, lastArg = matchingArg;
|
++index, ++m_actualArgc, lastArg = lastArgInLevel = matchingArg;
|
||||||
if(argDenotationType != Abbreviation || (!*++argDenotation && argDenotation != equationPos)) {
|
if(argDenotationType != Abbreviation || (!*++argDenotation && argDenotation != equationPos)) {
|
||||||
currentPath.push_back(matchingArg);
|
readSpecifiedArgs(matchingArg->m_subArgs, index, ++argv, end, lastArg, completionMode);
|
||||||
readSpecifiedArgs(matchingArg->m_subArgs, index, ++argv, end, currentPath);
|
|
||||||
currentPath.pop_back();
|
|
||||||
break;
|
break;
|
||||||
} // else: another abbreviated argument follows
|
} // else: another abbreviated argument follows
|
||||||
} else {
|
} else {
|
||||||
|
@ -516,8 +548,8 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
|
||||||
if(!matchingArg) {
|
if(!matchingArg) {
|
||||||
if(argDenotationType != Value) {
|
if(argDenotationType != Value) {
|
||||||
// unknown argument might be a sibling of the parent element
|
// unknown argument might be a sibling of the parent element
|
||||||
for(auto parentArgument = currentPath.crbegin(), end = currentPath.crend(); parentArgument != end; ) {
|
for(auto parentArgument = parentPath.crbegin(), pathEnd = parentPath.crend(); ; ++parentArgument) {
|
||||||
for(Argument *sibling : (++parentArgument != end ? (*parentArgument)->subArguments() : m_mainArgs)) {
|
for(Argument *sibling : (parentArgument != pathEnd ? (*parentArgument)->subArguments() : m_mainArgs)) {
|
||||||
if(sibling->occurrences() < sibling->maxOccurrences()) {
|
if(sibling->occurrences() < sibling->maxOccurrences()) {
|
||||||
if((argDenotationType == Abbreviation && (sibling->abbreviation() && sibling->abbreviation() == *argDenotation))
|
if((argDenotationType == Abbreviation && (sibling->abbreviation() && sibling->abbreviation() == *argDenotation))
|
||||||
|| (sibling->name() && !strncmp(sibling->name(), argDenotation, argDenLen))) {
|
|| (sibling->name() && !strncmp(sibling->name(), argDenotation, argDenLen))) {
|
||||||
|
@ -525,10 +557,13 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if(parentArgument == pathEnd) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if(lastArg && values->size() < lastArg->requiredValueCount()) {
|
if(lastArgInLevel && values->size() < lastArgInLevel->requiredValueCount()) {
|
||||||
// unknown argument might just be a parameter of the last argument
|
// unknown argument might just be a parameter of the last argument
|
||||||
values->emplace_back(abbreviationFound ? argDenotation : *argv);
|
values->emplace_back(abbreviationFound ? argDenotation : *argv);
|
||||||
++index, ++argv;
|
++index, ++argv;
|
||||||
|
@ -536,21 +571,21 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
|
||||||
}
|
}
|
||||||
|
|
||||||
// first value might denote "operation"
|
// first value might denote "operation"
|
||||||
if(currentPath.empty()) {
|
if(!index) {
|
||||||
for(Argument *arg : args) {
|
for(Argument *arg : args) {
|
||||||
if(arg->denotesOperation() && arg->name() && !strcmp(arg->name(), *argv)) {
|
if(arg->denotesOperation() && arg->name() && !strcmp(arg->name(), *argv)) {
|
||||||
(matchingArg = arg)->m_indices.push_back(index);
|
(matchingArg = arg)->m_occurances.emplace_back(index, parentPath, parentArg);
|
||||||
++index, ++argv;
|
++index, ++argv;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!matchingArg) {
|
if(!matchingArg && (!completionMode || (argv + 1 != end))) {
|
||||||
// use the first default argument which is not already present
|
// use the first default argument which is not already present
|
||||||
for(Argument *arg : args) {
|
for(Argument *arg : args) {
|
||||||
if(arg->isImplicit() && !arg->isPresent()) {
|
if(arg->isImplicit() && !arg->isPresent()) {
|
||||||
(matchingArg = arg)->m_indices.push_back(index);
|
(matchingArg = arg)->m_occurances.emplace_back(index, parentPath, parentArg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -558,28 +593,33 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
|
||||||
|
|
||||||
if(matchingArg) {
|
if(matchingArg) {
|
||||||
// an argument matched the specified denotation
|
// an argument matched the specified denotation
|
||||||
if(lastArg == matchingArg) {
|
if(lastArgInLevel == matchingArg) {
|
||||||
break;
|
break; // TODO: why?
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare reading parameter values
|
// prepare reading parameter values
|
||||||
matchingArg->m_values.emplace_back();
|
values = &matchingArg->m_occurances.back().values;
|
||||||
values = &matchingArg->m_values.back();
|
|
||||||
|
|
||||||
// read sub arguments
|
// read sub arguments
|
||||||
++m_actualArgc, lastArg = matchingArg;
|
++m_actualArgc, lastArg = lastArgInLevel = matchingArg;
|
||||||
currentPath.push_back(matchingArg);
|
readSpecifiedArgs(matchingArg->m_subArgs, index, argv, end, lastArg, completionMode);
|
||||||
readSpecifiedArgs(matchingArg->m_subArgs, index, argv, end, currentPath);
|
|
||||||
currentPath.pop_back();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(currentPath.empty()) {
|
if(!parentArg) {
|
||||||
if(m_ignoreUnknownArgs) {
|
if(completionMode) {
|
||||||
cerr << "The specified argument \"" << *argv << "\" is unknown and will be ignored." << endl;
|
|
||||||
++index, ++argv;
|
++index, ++argv;
|
||||||
} else {
|
} else {
|
||||||
throw Failure("The specified argument \"" + string(*argv) + "\" is unknown and will be ignored.");
|
switch(m_unknownArgBehavior) {
|
||||||
|
case UnknownArgumentBehavior::Warn:
|
||||||
|
cerr << "The specified argument \"" << *argv << "\" is unknown and will be ignored." << endl;
|
||||||
|
FALLTHROUGH;
|
||||||
|
case UnknownArgumentBehavior::Ignore:
|
||||||
|
++index, ++argv;
|
||||||
|
break;
|
||||||
|
case UnknownArgumentBehavior::Fail:
|
||||||
|
throw Failure("The specified argument \"" + string(*argv) + "\" is unknown and will be ignored.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return; // unknown argument name or abbreviation found -> continue with parent level
|
return; // unknown argument name or abbreviation found -> continue with parent level
|
||||||
|
@ -588,6 +628,293 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*!
|
||||||
|
* \brief Returns whether \a arg1 should be listed before \a arg2 when
|
||||||
|
* printing completion.
|
||||||
|
*
|
||||||
|
* Arguments are sorted by name (ascending order). However, all arguments
|
||||||
|
* denoting an operation are listed before all other arguments.
|
||||||
|
*/
|
||||||
|
bool compareArgs(const Argument *arg1, const Argument *arg2)
|
||||||
|
{
|
||||||
|
if(arg1->denotesOperation() && !arg2->denotesOperation()) {
|
||||||
|
return true;
|
||||||
|
} else if(!arg1->denotesOperation() && arg2->denotesOperation()) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return strcmp(arg1->name(), arg2->name()) < 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Inserts the specified \a siblings in the \a target list.
|
||||||
|
* \remarks Only inserts siblings which could still occur at least once more.
|
||||||
|
*/
|
||||||
|
void insertSiblings(const ArgumentVector &siblings, list<const Argument *> &target)
|
||||||
|
{
|
||||||
|
bool onlyCombinable = false;
|
||||||
|
for(const Argument *sibling : siblings) {
|
||||||
|
if(sibling->isPresent() && !sibling->isCombinable()) {
|
||||||
|
onlyCombinable = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(const Argument *sibling : siblings) {
|
||||||
|
if((!onlyCombinable || sibling->isCombinable()) && sibling->occurrences() < sibling->maxOccurrences()) {
|
||||||
|
target.push_back(sibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \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 Argument *lastDetectedArg)
|
||||||
|
{
|
||||||
|
// variables to store relevant completions (arguments, pre-defined values, files/dirs)
|
||||||
|
list<const Argument *> relevantArgs, relevantPreDefinedValues;
|
||||||
|
bool completeFiles = false, completeDirs = false, noWhitespace = false;
|
||||||
|
|
||||||
|
// get the last argument the argument parser was able to detect successfully
|
||||||
|
size_t lastDetectedArgIndex;
|
||||||
|
vector<Argument *> lastDetectedArgPath;
|
||||||
|
if(lastDetectedArg) {
|
||||||
|
lastDetectedArgIndex = lastDetectedArg->index(lastDetectedArg->occurrences() - 1);
|
||||||
|
lastDetectedArgPath = lastDetectedArg->path(lastDetectedArg->occurrences() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nextArgumentOrValue;
|
||||||
|
const char *const *lastSpecifiedArg;
|
||||||
|
unsigned int lastSpecifiedArgIndex;
|
||||||
|
if(argc) {
|
||||||
|
// determine last arg omitting trailing empty args
|
||||||
|
lastSpecifiedArgIndex = static_cast<unsigned int>(argc) - 1;
|
||||||
|
lastSpecifiedArg = argv + lastSpecifiedArgIndex;
|
||||||
|
for(; lastSpecifiedArg >= argv && **lastSpecifiedArg == '\0'; --lastSpecifiedArg, --lastSpecifiedArgIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lastDetectedArg && lastDetectedArg->isPresent()) {
|
||||||
|
if((nextArgumentOrValue = currentWordIndex > lastDetectedArgIndex)) {
|
||||||
|
// parameter values of the last arg are possible completions
|
||||||
|
auto currentValueCount = lastDetectedArg->values(lastDetectedArg->occurrences() - 1).size();
|
||||||
|
if(currentValueCount) {
|
||||||
|
currentValueCount -= (currentWordIndex - lastDetectedArgIndex);
|
||||||
|
}
|
||||||
|
if(lastDetectedArg->requiredValueCount() == static_cast<size_t>(-1) || (currentValueCount < lastDetectedArg->requiredValueCount())) {
|
||||||
|
if(lastDetectedArg->valueCompletionBehaviour() & ValueCompletionBehavior::PreDefinedValues) {
|
||||||
|
relevantPreDefinedValues.push_back(lastDetectedArg);
|
||||||
|
}
|
||||||
|
if(!(lastDetectedArg->valueCompletionBehaviour() & ValueCompletionBehavior::FileSystemIfNoPreDefinedValues) || !lastDetectedArg->preDefinedCompletionValues()) {
|
||||||
|
completeFiles = completeFiles || lastDetectedArg->valueCompletionBehaviour() & ValueCompletionBehavior::Files;
|
||||||
|
completeDirs = completeDirs || lastDetectedArg->valueCompletionBehaviour() & ValueCompletionBehavior::Directories;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(lastDetectedArg->requiredValueCount() == static_cast<size_t>(-1) || lastDetectedArg->values(lastDetectedArg->occurrences() - 1).size() >= lastDetectedArg->requiredValueCount()) {
|
||||||
|
// sub arguments of the last arg are possible completions
|
||||||
|
for(const Argument *subArg : lastDetectedArg->subArguments()) {
|
||||||
|
if(subArg->occurrences() < subArg->maxOccurrences()) {
|
||||||
|
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);
|
||||||
|
if(parentArgument == end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// since the argument could be detected (hopefully unambiguously?) just return it for "final completion"
|
||||||
|
relevantArgs.push_back(lastDetectedArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
nextArgumentOrValue = true;
|
||||||
|
insertSiblings(m_mainArgs, relevantArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the "opening" (started but not finished argument denotation)
|
||||||
|
const char *opening = nullptr;
|
||||||
|
size_t openingLen;
|
||||||
|
unsigned char openingDenotationType = Value;
|
||||||
|
if(argc && nextArgumentOrValue) {
|
||||||
|
opening = (currentWordIndex < argc ? argv[currentWordIndex] : *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) {
|
||||||
|
if(arg->preDefinedCompletionValues()) {
|
||||||
|
bool appendEquationSign = arg->valueCompletionBehaviour() & ValueCompletionBehavior::AppendEquationSign;
|
||||||
|
if(argc && currentWordIndex <= lastSpecifiedArgIndex && opening) {
|
||||||
|
if(openingDenotationType == Value) {
|
||||||
|
bool wordStart = true, ok = false;
|
||||||
|
for(const char *i = arg->preDefinedCompletionValues(), *end = opening + openingLen; *i;) {
|
||||||
|
if(wordStart) {
|
||||||
|
const char *i1 = i, *i2 = opening;
|
||||||
|
for(; *i1 && i2 != end && *i1 == *i2; ++i1, ++i2);
|
||||||
|
ok = (i2 == end);
|
||||||
|
wordStart = false;
|
||||||
|
} else {
|
||||||
|
wordStart = (*i == ' ') || (*i == '\n');
|
||||||
|
}
|
||||||
|
if(ok) {
|
||||||
|
cout << *i;
|
||||||
|
++i;
|
||||||
|
if(appendEquationSign) {
|
||||||
|
switch(*i) {
|
||||||
|
case ' ': case '\n': case '\0':
|
||||||
|
cout << '=';
|
||||||
|
noWhitespace = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cout << ' ';
|
||||||
|
}
|
||||||
|
} else if(appendEquationSign) {
|
||||||
|
for(const char *i = arg->preDefinedCompletionValues(); *i;) {
|
||||||
|
cout << *i;
|
||||||
|
switch(*(++i)) {
|
||||||
|
case ' ': case '\n': case '\0':
|
||||||
|
cout << '=';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cout << arg->preDefinedCompletionValues() << ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -> completions for further arguments
|
||||||
|
for(const Argument *arg : relevantArgs) {
|
||||||
|
if(argc && currentWordIndex <= lastSpecifiedArgIndex && opening) {
|
||||||
|
switch(openingDenotationType) {
|
||||||
|
case Value:
|
||||||
|
if(!arg->denotesOperation() || strncmp(arg->name(), opening, openingLen)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Abbreviation:
|
||||||
|
break;
|
||||||
|
case FullName:
|
||||||
|
if(strncmp(arg->name(), opening, openingLen)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(openingDenotationType == Abbreviation && opening) {
|
||||||
|
cout << '-' << opening << arg->abbreviation() << ' ';
|
||||||
|
} else if(arg->denotesOperation() && (!actualArgumentCount() || (currentWordIndex == 0 && (!lastDetectedArg || (lastDetectedArg->isPresent() && lastDetectedArgIndex == 0))))) {
|
||||||
|
cout << arg->name() << ' ';
|
||||||
|
} else {
|
||||||
|
cout << '-' << '-' << arg->name() << ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -> completions for files and dirs
|
||||||
|
// -> 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) {
|
||||||
|
// the "opening" might contain escaped characters which need to be unescaped first
|
||||||
|
string unescapedOpening(opening);
|
||||||
|
findAndReplace<string>(unescapedOpening, "\\ ", " ");
|
||||||
|
findAndReplace<string>(unescapedOpening, "\\,", ",");
|
||||||
|
findAndReplace<string>(unescapedOpening, "\\[", "[");
|
||||||
|
findAndReplace<string>(unescapedOpening, "\\]", "]");
|
||||||
|
findAndReplace<string>(unescapedOpening, "\\!", "!");
|
||||||
|
findAndReplace<string>(unescapedOpening, "\\#", "#");
|
||||||
|
findAndReplace<string>(unescapedOpening, "\\$", "$");
|
||||||
|
// determine the "directory" part
|
||||||
|
string dir = directory(unescapedOpening);
|
||||||
|
if(dir.empty()) {
|
||||||
|
actualDir = ".";
|
||||||
|
} else {
|
||||||
|
if(dir[0] == '\"' || dir[0] == '\'') {
|
||||||
|
dir.erase(0, 1);
|
||||||
|
}
|
||||||
|
if(dir.size() > 1 && (dir[dir.size() - 2] == '\"' || dir[dir.size() - 2] == '\'')) {
|
||||||
|
dir.erase(dir.size() - 2, 1);
|
||||||
|
}
|
||||||
|
actualDir = move(dir);
|
||||||
|
}
|
||||||
|
// determine the "file" part
|
||||||
|
string file = fileName(unescapedOpening);
|
||||||
|
if(file[0] == '\"' || file[0] == '\'') {
|
||||||
|
file.erase(0, 1);
|
||||||
|
}
|
||||||
|
if(file.size() > 1 && (file[dir.size() - 2] == '\"' || dir[file.size() - 2] == '\'')) {
|
||||||
|
file.erase(file.size() - 2, 1);
|
||||||
|
}
|
||||||
|
actualFile = move(file);
|
||||||
|
}
|
||||||
|
// -> completion for files
|
||||||
|
if(completeFiles) {
|
||||||
|
if(argc && currentWordIndex <= lastSpecifiedArgIndex && opening) {
|
||||||
|
for(const string &dirEntry : directoryEntries(actualDir.c_str(), DirectoryEntryType::File)) {
|
||||||
|
if(startsWith(dirEntry, actualFile)) {
|
||||||
|
cout << '\'';
|
||||||
|
if(actualDir != ".") {
|
||||||
|
cout << actualDir;
|
||||||
|
}
|
||||||
|
cout << dirEntry << '\'' << ' ';
|
||||||
|
haveFileOrDirCompletions = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for(const string &dirEntry : directoryEntries(".", DirectoryEntryType::File)) {
|
||||||
|
cout << dirEntry << ' ';
|
||||||
|
haveFileOrDirCompletions = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// -> completion for dirs
|
||||||
|
if(completeDirs) {
|
||||||
|
if(argc && currentWordIndex <= lastSpecifiedArgIndex && opening) {
|
||||||
|
for(const string &dirEntry : directoryEntries(actualDir.c_str(), DirectoryEntryType::Directory)) {
|
||||||
|
if(startsWith(dirEntry, actualFile)) {
|
||||||
|
cout << '\'';
|
||||||
|
if(actualDir != ".") {
|
||||||
|
cout << actualDir;
|
||||||
|
}
|
||||||
|
cout << dirEntry << '\'' << ' ';
|
||||||
|
haveFileOrDirCompletions = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for(const string &dirEntry : directoryEntries(".", DirectoryEntryType::Directory)) {
|
||||||
|
cout << '\'' << dirEntry << '/' << '\'' << ' ';
|
||||||
|
haveFileOrDirCompletions = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cout << ')';
|
||||||
|
|
||||||
|
// ensure file or dir completions are formatted appropriately
|
||||||
|
if(haveFileOrDirCompletions) {
|
||||||
|
cout << "; compopt -o filenames";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure trailing whitespace is ommitted
|
||||||
|
if(noWhitespace) {
|
||||||
|
cout << "; compopt -o nospace";
|
||||||
|
}
|
||||||
|
|
||||||
|
cout << endl;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Checks the constrains of the specified \a args.
|
* \brief Checks the constrains of the specified \a args.
|
||||||
|
@ -652,8 +979,8 @@ void ArgumentParser::invokeCallbacks(const ArgumentVector &args)
|
||||||
for(const Argument *arg : args) {
|
for(const Argument *arg : args) {
|
||||||
// invoke the callback for each occurance of the argument
|
// invoke the callback for each occurance of the argument
|
||||||
if(arg->m_callbackFunction) {
|
if(arg->m_callbackFunction) {
|
||||||
for(const auto &valuesOfOccurance : arg->m_values) {
|
for(const auto &occurance : arg->m_occurances) {
|
||||||
arg->m_callbackFunction(valuesOfOccurance);
|
arg->m_callbackFunction(occurance.values);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// invoke the callbacks for sub arguments recursively
|
// invoke the callbacks for sub arguments recursively
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
# include <cassert>
|
# include <cassert>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
class ArgumentParserTests;
|
||||||
|
|
||||||
namespace ApplicationUtilities {
|
namespace ApplicationUtilities {
|
||||||
|
|
||||||
LIB_EXPORT extern const char *applicationName;
|
LIB_EXPORT extern const char *applicationName;
|
||||||
|
@ -30,8 +32,65 @@ typedef std::initializer_list<Argument *> ArgumentInitializerList;
|
||||||
typedef std::vector<Argument *> ArgumentVector;
|
typedef std::vector<Argument *> ArgumentVector;
|
||||||
typedef std::function<bool (Argument *)> ArgumentPredicate;
|
typedef std::function<bool (Argument *)> ArgumentPredicate;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief The UnknownArgumentBehavior enum specifies the behavior of the argument parser when an unknown
|
||||||
|
* argument is detected.
|
||||||
|
*/
|
||||||
|
enum class UnknownArgumentBehavior
|
||||||
|
{
|
||||||
|
Ignore, /**< Unknown arguments are ignored without warnings. */
|
||||||
|
Warn, /**< A warning is printed to std::cerr if an unknown argument is detected. */
|
||||||
|
Fail /**< Further parsing is aborted and an ApplicationUtilities::Failure instance with an error message is thrown. */
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief The ValueCompletionBehavior enum specifies the items to be considered when generating completion for an argument value.
|
||||||
|
*/
|
||||||
|
enum class ValueCompletionBehavior : unsigned char
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
PreDefinedValues = 2,
|
||||||
|
Files = 4,
|
||||||
|
Directories = 8,
|
||||||
|
FileSystemIfNoPreDefinedValues = 16,
|
||||||
|
AppendEquationSign = 32
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr ValueCompletionBehavior operator|(ValueCompletionBehavior lhs, ValueCompletionBehavior rhs)
|
||||||
|
{
|
||||||
|
return static_cast<ValueCompletionBehavior>(static_cast<unsigned char>(lhs) | static_cast<unsigned char>(rhs));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool operator&(ValueCompletionBehavior lhs, ValueCompletionBehavior rhs)
|
||||||
|
{
|
||||||
|
return static_cast<bool>(static_cast<unsigned char>(lhs) & static_cast<unsigned char>(rhs));
|
||||||
|
}
|
||||||
|
|
||||||
Argument LIB_EXPORT *firstPresentUncombinableArg(const ArgumentVector &args, const Argument *except);
|
Argument LIB_EXPORT *firstPresentUncombinableArg(const ArgumentVector &args, const Argument *except);
|
||||||
|
|
||||||
|
struct LIB_EXPORT ArgumentOccurance
|
||||||
|
{
|
||||||
|
ArgumentOccurance(std::size_t index);
|
||||||
|
ArgumentOccurance(std::size_t index, const std::vector<Argument *> parentPath, Argument *parent);
|
||||||
|
|
||||||
|
std::size_t index;
|
||||||
|
std::vector<const char *> values;
|
||||||
|
std::vector<Argument *> path;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline ArgumentOccurance::ArgumentOccurance(std::size_t index) :
|
||||||
|
index(index)
|
||||||
|
{}
|
||||||
|
|
||||||
|
inline ArgumentOccurance::ArgumentOccurance(std::size_t index, const std::vector<Argument *> parentPath, Argument *parent) :
|
||||||
|
index(index),
|
||||||
|
path(parentPath)
|
||||||
|
{
|
||||||
|
if(parent) {
|
||||||
|
path.push_back(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class LIB_EXPORT Argument
|
class LIB_EXPORT Argument
|
||||||
{
|
{
|
||||||
friend class ArgumentParser;
|
friend class ArgumentParser;
|
||||||
|
@ -59,10 +118,11 @@ public:
|
||||||
bool allRequiredValuesPresent(std::size_t occurrance = 0) const;
|
bool allRequiredValuesPresent(std::size_t occurrance = 0) const;
|
||||||
bool isPresent() const;
|
bool isPresent() const;
|
||||||
std::size_t occurrences() const;
|
std::size_t occurrences() const;
|
||||||
const std::vector<std::size_t> &indices() const;
|
std::size_t index(std::size_t occurrance) const;
|
||||||
std::size_t minOccurrences() const;
|
std::size_t minOccurrences() const;
|
||||||
std::size_t maxOccurrences() const;
|
std::size_t maxOccurrences() const;
|
||||||
void setConstraints(std::size_t minOccurrences, std::size_t maxOccurrences);
|
void setConstraints(std::size_t minOccurrences, std::size_t maxOccurrences);
|
||||||
|
const std::vector<Argument *> &path(std::size_t occurrance = 0) const;
|
||||||
bool isRequired() const;
|
bool isRequired() const;
|
||||||
void setRequired(bool required);
|
void setRequired(bool required);
|
||||||
bool isCombinable() const;
|
bool isCombinable() const;
|
||||||
|
@ -80,6 +140,10 @@ public:
|
||||||
const ArgumentVector parents() const;
|
const ArgumentVector parents() const;
|
||||||
bool isMainArgument() const;
|
bool isMainArgument() const;
|
||||||
bool isParentPresent() const;
|
bool isParentPresent() const;
|
||||||
|
ValueCompletionBehavior valueCompletionBehaviour() const;
|
||||||
|
void setValueCompletionBehavior(ValueCompletionBehavior valueCompletionBehaviour);
|
||||||
|
const char *preDefinedCompletionValues() const;
|
||||||
|
void setPreDefinedCompletionValues(const char *preDefinedCompletionValues);
|
||||||
Argument *conflictsWithArgument() const;
|
Argument *conflictsWithArgument() const;
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
|
@ -95,12 +159,47 @@ private:
|
||||||
std::size_t m_requiredValueCount;
|
std::size_t m_requiredValueCount;
|
||||||
std::vector<const char *> m_valueNames;
|
std::vector<const char *> m_valueNames;
|
||||||
bool m_implicit;
|
bool m_implicit;
|
||||||
std::vector<std::size_t> m_indices;
|
std::vector<ArgumentOccurance> m_occurances;
|
||||||
std::vector<std::vector<const char *> > m_values;
|
|
||||||
ArgumentVector m_subArgs;
|
ArgumentVector m_subArgs;
|
||||||
CallbackFunction m_callbackFunction;
|
CallbackFunction m_callbackFunction;
|
||||||
ArgumentVector m_parents;
|
ArgumentVector m_parents;
|
||||||
bool m_isMainArg;
|
bool m_isMainArg;
|
||||||
|
ValueCompletionBehavior m_valueCompletionBehavior;
|
||||||
|
const char *m_preDefinedCompletionValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LIB_EXPORT ArgumentParser
|
||||||
|
{
|
||||||
|
friend ArgumentParserTests;
|
||||||
|
public:
|
||||||
|
ArgumentParser();
|
||||||
|
|
||||||
|
const ArgumentVector &mainArguments() const;
|
||||||
|
void setMainArguments(const ArgumentInitializerList &mainArguments);
|
||||||
|
void addMainArgument(Argument *argument);
|
||||||
|
void printHelp(std::ostream &os) const;
|
||||||
|
Argument *findArg(const ArgumentPredicate &predicate) const;
|
||||||
|
static Argument *findArg(const ArgumentVector &arguments, const ArgumentPredicate &predicate);
|
||||||
|
void parseArgs(int argc, const char *const *argv);
|
||||||
|
unsigned int actualArgumentCount() const;
|
||||||
|
const char *executable() const;
|
||||||
|
UnknownArgumentBehavior unknownArgumentBehavior() const;
|
||||||
|
void setUnknownArgumentBehavior(UnknownArgumentBehavior behavior);
|
||||||
|
Argument *defaultArgument() const;
|
||||||
|
void setDefaultArgument(Argument *argument);
|
||||||
|
|
||||||
|
private:
|
||||||
|
IF_DEBUG_BUILD(void verifyArgs(const ArgumentVector &args);)
|
||||||
|
void readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char *const *&argv, const char *const *end, Argument *&lastArg, bool completionMode = false);
|
||||||
|
void printBashCompletion(int argc, const char * const *argv, unsigned int cursorPos, const Argument *lastDetectedArg);
|
||||||
|
void checkConstraints(const ArgumentVector &args);
|
||||||
|
void invokeCallbacks(const ArgumentVector &args);
|
||||||
|
|
||||||
|
ArgumentVector m_mainArgs;
|
||||||
|
unsigned int m_actualArgc;
|
||||||
|
const char *m_executable;
|
||||||
|
UnknownArgumentBehavior m_unknownArgBehavior;
|
||||||
|
Argument *m_defaultArg;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -207,7 +306,7 @@ inline void Argument::setExample(const char *example)
|
||||||
*/
|
*/
|
||||||
inline const std::vector<const char *> &Argument::values(std::size_t occurrance) const
|
inline const std::vector<const char *> &Argument::values(std::size_t occurrance) const
|
||||||
{
|
{
|
||||||
return m_values[occurrance];
|
return m_occurances[occurrance].values;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -292,7 +391,7 @@ inline void Argument::appendValueName(const char *valueName)
|
||||||
inline bool Argument::allRequiredValuesPresent(std::size_t occurrance) const
|
inline bool Argument::allRequiredValuesPresent(std::size_t occurrance) const
|
||||||
{
|
{
|
||||||
return m_requiredValueCount == static_cast<std::size_t>(-1)
|
return m_requiredValueCount == static_cast<std::size_t>(-1)
|
||||||
|| (m_values[occurrance].size() >= static_cast<std::size_t>(m_requiredValueCount));
|
|| (m_occurances[occurrance].values.size() >= static_cast<std::size_t>(m_requiredValueCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -318,7 +417,7 @@ inline void Argument::setImplicit(bool implicit)
|
||||||
*/
|
*/
|
||||||
inline bool Argument::isPresent() const
|
inline bool Argument::isPresent() const
|
||||||
{
|
{
|
||||||
return !m_indices.empty();
|
return !m_occurances.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -326,15 +425,15 @@ inline bool Argument::isPresent() const
|
||||||
*/
|
*/
|
||||||
inline std::size_t Argument::occurrences() const
|
inline std::size_t Argument::occurrences() const
|
||||||
{
|
{
|
||||||
return m_indices.size();
|
return m_occurances.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns the indices of the argument's occurences which could be detected when parsing.
|
* \brief Returns the indices of the argument's occurences which could be detected when parsing.
|
||||||
*/
|
*/
|
||||||
inline const std::vector<std::size_t> &Argument::indices() const
|
inline std::size_t Argument::index(std::size_t occurrance) const
|
||||||
{
|
{
|
||||||
return m_indices;
|
return m_occurances[occurrance].index;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -368,6 +467,14 @@ inline void Argument::setConstraints(std::size_t minOccurrences, std::size_t max
|
||||||
m_maxOccurrences = maxOccurrences;
|
m_maxOccurrences = maxOccurrences;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Returns the path of the specified \a occurrance.
|
||||||
|
*/
|
||||||
|
inline const std::vector<Argument *> &Argument::path(std::size_t occurrance) const
|
||||||
|
{
|
||||||
|
return m_occurances[occurrance].path;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns an indication whether the argument is mandatory.
|
* \brief Returns an indication whether the argument is mandatory.
|
||||||
*
|
*
|
||||||
|
@ -508,37 +615,45 @@ inline bool Argument::isMainArgument() const
|
||||||
return m_isMainArg;
|
return m_isMainArg;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LIB_EXPORT ArgumentParser
|
/*!
|
||||||
|
* \brief Returns the items to be considered when generating completion for the values.
|
||||||
|
*/
|
||||||
|
inline ValueCompletionBehavior Argument::valueCompletionBehaviour() const
|
||||||
{
|
{
|
||||||
public:
|
return m_valueCompletionBehavior;
|
||||||
ArgumentParser();
|
}
|
||||||
|
|
||||||
const ArgumentVector &mainArguments() const;
|
/*!
|
||||||
void setMainArguments(const ArgumentInitializerList &mainArguments);
|
* \brief Sets the items to be considered when generating completion for the values.
|
||||||
void addMainArgument(Argument *argument);
|
*/
|
||||||
void printHelp(std::ostream &os) const;
|
inline void Argument::setValueCompletionBehavior(ValueCompletionBehavior completionValues)
|
||||||
Argument *findArg(const ArgumentPredicate &predicate) const;
|
{
|
||||||
static Argument *findArg(const ArgumentVector &arguments, const ArgumentPredicate &predicate);
|
m_valueCompletionBehavior = completionValues;
|
||||||
void parseArgs(int argc, const char *const *argv);
|
}
|
||||||
unsigned int actualArgumentCount() const;
|
|
||||||
const char *executable() const;
|
|
||||||
bool areUnknownArgumentsIgnored() const;
|
|
||||||
void setIgnoreUnknownArguments(bool ignore);
|
|
||||||
Argument *defaultArgument() const;
|
|
||||||
void setDefaultArgument(Argument *argument);
|
|
||||||
|
|
||||||
private:
|
/*!
|
||||||
IF_DEBUG_BUILD(void verifyArgs(const ArgumentVector &args);)
|
* \brief Returns the assigned values used when generating completion for the values.
|
||||||
void readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char *const *&argv, const char *const *end, std::vector<Argument *> ¤tPath);
|
*/
|
||||||
void checkConstraints(const ArgumentVector &args);
|
inline const char *Argument::preDefinedCompletionValues() const
|
||||||
void invokeCallbacks(const ArgumentVector &args);
|
{
|
||||||
|
return m_preDefinedCompletionValues;
|
||||||
|
}
|
||||||
|
|
||||||
ArgumentVector m_mainArgs;
|
/*!
|
||||||
unsigned int m_actualArgc;
|
* \brief Assignes the values to be used when generating completion for the values.
|
||||||
const char *m_executable;
|
*/
|
||||||
bool m_ignoreUnknownArgs;
|
inline void Argument::setPreDefinedCompletionValues(const char *preDefinedCompletionValues)
|
||||||
Argument *m_defaultArg;
|
{
|
||||||
};
|
m_preDefinedCompletionValues = preDefinedCompletionValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Resets occurrences (indices, values and paths).
|
||||||
|
*/
|
||||||
|
inline void Argument::reset()
|
||||||
|
{
|
||||||
|
m_occurances.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns the main arguments.
|
* \brief Returns the main arguments.
|
||||||
|
@ -566,35 +681,23 @@ inline const char *ArgumentParser::executable() const
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Returns an indication whether unknown arguments detected
|
* \brief Returns how unknown arguments are treated.
|
||||||
* when parsing should be ignored.
|
|
||||||
*
|
*
|
||||||
* If unknown arguments are not ignored the parser will throw a
|
* The default value is UnknownArgumentBehavior::Fail.
|
||||||
* Failure when an unknown argument is detected.
|
|
||||||
* Otherwise only a warning will be shown.
|
|
||||||
*
|
|
||||||
* The default value is false.
|
|
||||||
*
|
|
||||||
* \sa setIgnoreUnknownArguments()
|
|
||||||
*/
|
*/
|
||||||
inline bool ArgumentParser::areUnknownArgumentsIgnored() const
|
inline UnknownArgumentBehavior ArgumentParser::unknownArgumentBehavior() const
|
||||||
{
|
{
|
||||||
return m_ignoreUnknownArgs;
|
return m_unknownArgBehavior;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Sets whether the parser should ignore unknown arguments
|
* \brief Sets how unknown arguments are treated.
|
||||||
* when parsing.
|
|
||||||
*
|
*
|
||||||
* If set to false the parser should throw a Failure object
|
* The default value is UnknownArgumentBehavior::Fail.
|
||||||
* when an unknown argument is found. Otherwise only a warning
|
|
||||||
* will be printed.
|
|
||||||
*
|
|
||||||
* \sa areUnknownArgumentsIgnored()
|
|
||||||
*/
|
*/
|
||||||
inline void ArgumentParser::setIgnoreUnknownArguments(bool ignore)
|
inline void ArgumentParser::setUnknownArgumentBehavior(UnknownArgumentBehavior behavior)
|
||||||
{
|
{
|
||||||
m_ignoreUnknownArgs = ignore;
|
m_unknownArgBehavior = behavior;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#ifdef PLATFORM_WINDOWS
|
#ifdef PLATFORM_WINDOWS
|
||||||
#include <windows.h>
|
# include <windows.h>
|
||||||
#include <fcntl.h>
|
# include <fcntl.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
set(SHELL_COMPLETION_ENABLED "yes" CACHE STRING "controls whether shell completion is enabled")
|
||||||
|
set(BASH_COMPLETION_ENABLED "yes" CACHE STRING "controls whether bash completion is enabled")
|
||||||
|
|
||||||
|
if(${SHELL_COMPLETION_ENABLED} STREQUAL "yes")
|
||||||
|
|
||||||
|
if(NOT COMPLETION_META_PROJECT_NAME)
|
||||||
|
set(COMPLETION_META_PROJECT_NAME ${META_PROJECT_NAME})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# add bash completion (currently the only supported shell completion)
|
||||||
|
if(${BASH_COMPLETION_ENABLED} STREQUAL "yes")
|
||||||
|
# find bash-completion.sh template
|
||||||
|
include(TemplateFinder)
|
||||||
|
find_template_file("bash-completion.sh" CPP_UTILITIES BASH_COMPLETION_TEMPLATE_FILE)
|
||||||
|
|
||||||
|
# generate wrapper script for bash completion
|
||||||
|
configure_file(
|
||||||
|
"${BASH_COMPLETION_TEMPLATE_FILE}"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/bash-completion/completions/${META_PROJECT_NAME}"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
# add install target bash completion
|
||||||
|
install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bash-completion/completions"
|
||||||
|
DESTINATION "share/${META_PROJECT_NAME}/share"
|
||||||
|
COMPONENT bash-completion
|
||||||
|
)
|
||||||
|
if(NOT TARGET install-bash-completion)
|
||||||
|
add_custom_target(install-bash-completion
|
||||||
|
COMMAND "${CMAKE_COMMAND}" -DCMAKE_INSTALL_COMPONENT=bash-completion -P "${CMAKE_BINARY_DIR}/cmake_install.cmake"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
endif()
|
|
@ -0,0 +1,6 @@
|
||||||
|
_@META_PROJECT_NAME@()
|
||||||
|
{
|
||||||
|
eval "$(@TARGET_EXECUTABLE@ --bash-completion-for "$((COMP_CWORD - 1))" "${COMP_WORDS[@]:1}")"
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
complete -F _@META_PROJECT_NAME@ @COMPLETION_META_PROJECT_NAME@
|
|
@ -4,6 +4,8 @@
|
||||||
#include "../application/failure.h"
|
#include "../application/failure.h"
|
||||||
#include "../application/fakeqtconfigarguments.h"
|
#include "../application/fakeqtconfigarguments.h"
|
||||||
|
|
||||||
|
#include "../io/path.h"
|
||||||
|
|
||||||
#include "resources/config.h"
|
#include "resources/config.h"
|
||||||
|
|
||||||
#include <cppunit/extensions/HelperMacros.h>
|
#include <cppunit/extensions/HelperMacros.h>
|
||||||
|
@ -25,6 +27,7 @@ class ArgumentParserTests : public TestFixture
|
||||||
CPPUNIT_TEST(testArgument);
|
CPPUNIT_TEST(testArgument);
|
||||||
CPPUNIT_TEST(testParsing);
|
CPPUNIT_TEST(testParsing);
|
||||||
CPPUNIT_TEST(testCallbacks);
|
CPPUNIT_TEST(testCallbacks);
|
||||||
|
CPPUNIT_TEST(testBashCompletion);
|
||||||
CPPUNIT_TEST_SUITE_END();
|
CPPUNIT_TEST_SUITE_END();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -34,6 +37,7 @@ public:
|
||||||
void testArgument();
|
void testArgument();
|
||||||
void testParsing();
|
void testParsing();
|
||||||
void testCallbacks();
|
void testCallbacks();
|
||||||
|
void testBashCompletion();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void callback();
|
void callback();
|
||||||
|
@ -126,7 +130,7 @@ void ArgumentParserTests::testParsing()
|
||||||
CPPUNIT_ASSERT_THROW(displayTagInfoArg.values().at(3), out_of_range);
|
CPPUNIT_ASSERT_THROW(displayTagInfoArg.values().at(3), out_of_range);
|
||||||
|
|
||||||
// define the same arguments in a different way
|
// define the same arguments in a different way
|
||||||
const char *argv2[] = {"tageditor", "", "-p", "album", "title", "diskpos", "", "--file", "somefile"};
|
const char *argv2[] = {"tageditor", "", "-p", "album", "title", "diskpos", "", "--files", "somefile"};
|
||||||
// reparse the args
|
// reparse the args
|
||||||
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
|
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
|
||||||
parser.parseArgs(9, argv2);
|
parser.parseArgs(9, argv2);
|
||||||
|
@ -145,7 +149,7 @@ void ArgumentParserTests::testParsing()
|
||||||
CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
|
CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
|
||||||
|
|
||||||
// forget "get"/"-p"
|
// forget "get"/"-p"
|
||||||
const char *argv3[] = {"tageditor", "album", "title", "diskpos", "--file", "somefile"};
|
const char *argv3[] = {"tageditor", "album", "title", "diskpos", "--files", "somefile"};
|
||||||
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
|
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
|
||||||
|
|
||||||
// a parsing error should occur because the argument "album" is not defined
|
// a parsing error should occur because the argument "album" is not defined
|
||||||
|
@ -156,9 +160,9 @@ void ArgumentParserTests::testParsing()
|
||||||
CPPUNIT_ASSERT(!strcmp(e.what(), "The specified argument \"album\" is unknown and will be ignored."));
|
CPPUNIT_ASSERT(!strcmp(e.what(), "The specified argument \"album\" is unknown and will be ignored."));
|
||||||
}
|
}
|
||||||
|
|
||||||
// repeat the test, but this time just ignore the undefined argument
|
// repeat the test, but this time just ignore the undefined argument printing a warning
|
||||||
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
|
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
|
||||||
parser.setIgnoreUnknownArguments(true);
|
parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Warn);
|
||||||
// redirect stderr to check whether warnings are printed correctly
|
// redirect stderr to check whether warnings are printed correctly
|
||||||
stringstream buffer;
|
stringstream buffer;
|
||||||
streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf());
|
streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf());
|
||||||
|
@ -170,10 +174,10 @@ void ArgumentParserTests::testParsing()
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
CPPUNIT_ASSERT(!strcmp(buffer.str().data(), "The specified argument \"album\" is unknown and will be ignored.\n"
|
CPPUNIT_ASSERT(!strcmp(buffer.str().data(), "The specified argument \"album\" is unknown and will be ignored.\n"
|
||||||
"The specified argument \"title\" is unknown and will be ignored.\n"
|
"The specified argument \"title\" is unknown and will be ignored.\n"
|
||||||
"The specified argument \"diskpos\" is unknown and will be ignored.\n"
|
"The specified argument \"diskpos\" is unknown and will be ignored.\n"
|
||||||
"The specified argument \"--file\" is unknown and will be ignored.\n"
|
"The specified argument \"--files\" is unknown and will be ignored.\n"
|
||||||
"The specified argument \"somefile\" is unknown and will be ignored.\n"));
|
"The specified argument \"somefile\" is unknown and will be ignored.\n"));
|
||||||
// none of the arguments should be present now
|
// none of the arguments should be present now
|
||||||
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
|
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
|
||||||
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
|
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
|
||||||
|
@ -184,7 +188,7 @@ void ArgumentParserTests::testParsing()
|
||||||
// test abbreviations like "-vf"
|
// test abbreviations like "-vf"
|
||||||
const char *argv4[] = {"tageditor", "-i", "-vf", "test"};
|
const char *argv4[] = {"tageditor", "-i", "-vf", "test"};
|
||||||
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
|
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
|
||||||
parser.setIgnoreUnknownArguments(false);
|
parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Fail);
|
||||||
parser.parseArgs(4, argv4);
|
parser.parseArgs(4, argv4);
|
||||||
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
|
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
|
||||||
CPPUNIT_ASSERT(displayFileInfoArg.isPresent());
|
CPPUNIT_ASSERT(displayFileInfoArg.isPresent());
|
||||||
|
@ -287,6 +291,9 @@ void ArgumentParserTests::testParsing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Tests whether callbacks are called correctly.
|
||||||
|
*/
|
||||||
void ArgumentParserTests::testCallbacks()
|
void ArgumentParserTests::testCallbacks()
|
||||||
{
|
{
|
||||||
ArgumentParser parser;
|
ArgumentParser parser;
|
||||||
|
@ -315,3 +322,150 @@ void ArgumentParserTests::testCallbacks()
|
||||||
const char *argv2[] = {"test", "-l", "val1", "val2"};
|
const char *argv2[] = {"test", "-l", "val1", "val2"};
|
||||||
parser.parseArgs(4, argv2);
|
parser.parseArgs(4, argv2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Tests bash completion.
|
||||||
|
* \remarks This tests makes assumptions about the order and the exact output format
|
||||||
|
* which should be improved.
|
||||||
|
*/
|
||||||
|
void ArgumentParserTests::testBashCompletion()
|
||||||
|
{
|
||||||
|
ArgumentParser parser;
|
||||||
|
HelpArgument helpArg(parser);
|
||||||
|
Argument verboseArg("verbose", 'v', "be verbose");
|
||||||
|
verboseArg.setCombinable(true);
|
||||||
|
Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
|
||||||
|
filesArg.setRequiredValueCount(-1);
|
||||||
|
filesArg.setCombinable(true);
|
||||||
|
Argument nestedSubArg("nested-sub", '\0', "nested sub arg");
|
||||||
|
Argument subArg("sub", '\0', "sub arg");
|
||||||
|
subArg.setSubArguments({&nestedSubArg});
|
||||||
|
Argument displayFileInfoArg("display-file-info", 'i', "displays general file information");
|
||||||
|
displayFileInfoArg.setDenotesOperation(true);
|
||||||
|
displayFileInfoArg.setSubArguments({&filesArg, &verboseArg, &subArg});
|
||||||
|
Argument fieldsArg("fields", '\0', "specifies the fields");
|
||||||
|
fieldsArg.setRequiredValueCount(-1);
|
||||||
|
fieldsArg.setPreDefinedCompletionValues("title album artist trackpos");
|
||||||
|
fieldsArg.setImplicit(true);
|
||||||
|
Argument valuesArg("values", '\0', "specifies the fields");
|
||||||
|
valuesArg.setRequiredValueCount(-1);
|
||||||
|
valuesArg.setPreDefinedCompletionValues("title album artist trackpos");
|
||||||
|
valuesArg.setImplicit(true);
|
||||||
|
valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
|
||||||
|
Argument getArg("get", 'g', "gets tag values");
|
||||||
|
getArg.setSubArguments({&fieldsArg, &filesArg});
|
||||||
|
Argument setArg("set", 's', "sets tag values");
|
||||||
|
setArg.setSubArguments({&valuesArg, &filesArg});
|
||||||
|
parser.setMainArguments({&helpArg, &displayFileInfoArg, &getArg, &setArg});
|
||||||
|
|
||||||
|
size_t index = 0;
|
||||||
|
Argument *lastDetectedArg = nullptr;
|
||||||
|
|
||||||
|
// redirect cout to custom buffer
|
||||||
|
stringstream buffer;
|
||||||
|
streambuf *regularCoutBuffer = cout.rdbuf(buffer.rdbuf());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// should fail because operation flags are not set
|
||||||
|
const char *const argv1[] = {"se"};
|
||||||
|
const char *const *argv = argv1;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv1 + 1, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(1, argv1, 0, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=()\n"), buffer.str());
|
||||||
|
|
||||||
|
// with correct operation arg flags
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string());
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
getArg.setDenotesOperation(true), setArg.setDenotesOperation(true);
|
||||||
|
argv = argv1;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv1 + 1, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(1, argv1, 0, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(set )\n"), buffer.str());
|
||||||
|
|
||||||
|
// argument is already specified
|
||||||
|
const char *const argv2[] = {"set"};
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string());
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
argv = argv2;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv2 + 1, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(1, argv2, 0, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(set )\n"), buffer.str());
|
||||||
|
|
||||||
|
// advance the cursor position -> the completion should propose the next argument
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string()), setArg.reset();
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
argv = argv2;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv2 + 1, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(1, argv2, 1, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(--files --values )\n"), buffer.str());
|
||||||
|
|
||||||
|
// specifying no args should propose all main arguments
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string()), getArg.reset(), setArg.reset();
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
argv = nullptr;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, nullptr, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(0, nullptr, 0, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(display-file-info get set --help )\n"), buffer.str());
|
||||||
|
|
||||||
|
// values
|
||||||
|
const char *const argv3[] = {"get", "--fields"};
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string()), getArg.reset(), setArg.reset();
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
argv = argv3;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv3 + 2, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(2, argv3, 2, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(title album artist trackpos --files )\n"), buffer.str());
|
||||||
|
|
||||||
|
// values with equation sign, one letter already present
|
||||||
|
const char *const argv4[] = {"set", "--values", "a"};
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string()), getArg.reset(), setArg.reset();
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
argv = argv4;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv4 + 3, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(3, argv4, 2, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(album= artist= ); compopt -o nospace\n"), buffer.str());
|
||||||
|
|
||||||
|
// file names
|
||||||
|
string iniFilePath = TestUtilities::testFilePath("test.ini");
|
||||||
|
iniFilePath.resize(iniFilePath.size() - 3);
|
||||||
|
const char *const argv5[] = {"get", "--files", iniFilePath.c_str()};
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string()), getArg.reset(), setArg.reset();
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
argv = argv5;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv5 + 3, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(3, argv5, 2, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL("COMPREPLY=('" + iniFilePath + "ini' ); compopt -o filenames\n", buffer.str());
|
||||||
|
|
||||||
|
// sub arguments
|
||||||
|
const char *const argv6[] = {"set", "--"};
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string()), setArg.reset(), valuesArg.reset(), filesArg.reset();
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
argv = argv6;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv6 + 2, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(2, argv6, 1, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(--files --values )\n"), buffer.str());
|
||||||
|
|
||||||
|
// nested sub arguments
|
||||||
|
const char *const argv7[] = {"-i", "--sub", "--"};
|
||||||
|
index = 0, lastDetectedArg = nullptr, buffer.str(string()), setArg.reset(), valuesArg.reset(), filesArg.reset();
|
||||||
|
cout.rdbuf(buffer.rdbuf());
|
||||||
|
argv = argv7;
|
||||||
|
parser.readSpecifiedArgs(parser.m_mainArgs, index, argv, argv7 + 3, lastDetectedArg, true);
|
||||||
|
parser.printBashCompletion(3, argv7, 2, lastDetectedArg);
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
CPPUNIT_ASSERT_EQUAL(string("COMPREPLY=(--files --nested-sub --verbose )\n"), buffer.str());
|
||||||
|
|
||||||
|
} catch(...) {
|
||||||
|
cout.rdbuf(regularCoutBuffer);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue