Add support for bash completion

This commit is contained in:
Martchus 2016-07-03 22:36:48 +02:00
parent 209b953754
commit 03094a1f61
7 changed files with 748 additions and 121 deletions

View File

@ -72,12 +72,14 @@ set(CMAKE_MODULE_FILES
cmake/modules/TemplateFinder.cmake
cmake/modules/Doxygen.cmake
cmake/modules/ListToString.cmake
cmake/modules/ShellCompletion.cmake
)
set(CMAKE_TEMPLATE_FILES
cmake/templates/Config.cmake.in
cmake/templates/config.h.in
cmake/templates/desktop.in
cmake/templates/doxygen.in
cmake/templates/bash-completion.sh.in
)
if(MINGW)
list(APPEND CMAKE_TEMPLATE_FILES

View File

@ -3,17 +3,21 @@
#include "./failure.h"
#include "../conversion/stringconversion.h"
#include "../misc/random.h"
#include "../io/path.h"
#include <algorithm>
#include <iostream>
#include <string>
#include <sstream>
#include <cstring>
#ifdef LOGGING_ENABLED
# include <fstream>
#endif
using namespace std;
using namespace std::placeholders;
using namespace ConversionUtilities;
using namespace IoUtilities;
/*!
* \namespace ApplicationUtilities
@ -71,7 +75,9 @@ Argument::Argument(const char *name, char abbreviation, const char *description,
m_denotesOperation(false),
m_requiredValueCount(0),
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;
}
/*!
* \brief Resets occurrences and values.
*/
void Argument::reset()
{
m_indices.clear();
m_values.clear();
}
/*!
* \class ApplicationUtilities::ArgumentParser
* \brief The ArgumentParser class provides a means for handling command line arguments.
@ -258,7 +255,7 @@ void Argument::reset()
ArgumentParser::ArgumentParser() :
m_actualArgc(0),
m_executable(nullptr),
m_ignoreUnknownArgs(false),
m_unknownArgBehavior(UnknownArgumentBehavior::Fail),
m_defaultArg(nullptr)
{}
@ -374,21 +371,57 @@ Argument *ArgumentParser::findArg(const ArgumentVector &arguments, const Argumen
*/
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);)
m_actualArgc = 0;
if(argc) {
// the first argument is the executable name
m_executable = *argv;
// check for further arguments
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;
++argv;
vector<Argument *> path;
path.reserve(4);
readSpecifiedArgs(m_mainArgs, index, argv, argv + argc, path);
Argument *lastDetectedArgument = nullptr;
// read specified arguments
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 {
// no arguments specified -> set default argument as present
// no arguments specified -> flag default argument as present if one is assigned
if(m_defaultArg) {
m_defaultArg->m_indices.push_back(0);
m_defaultArg->m_values.emplace_back();
m_defaultArg->m_occurances.emplace_back(0);
}
}
checkConstraints(m_mainArgs);
@ -442,19 +475,21 @@ void ApplicationUtilities::ArgumentParser::verifyArgs(const ArgumentVector &args
* \brief Reads the specified commands line 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 *> &currentPath)
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;
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
values->emplace_back(*argv);
++index, ++argv;
} else {
// determine denotation type
const char *argDenotation = *argv;
if(!*argDenotation && (!lastArg || values->size() >= lastArg->requiredValueCount())) {
if(!*argDenotation && (!lastArgInLevel || values->size() >= lastArgInLevel->requiredValueCount())) {
// skip empty arguments
++index, ++argv;
continue;
@ -469,7 +504,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
size_t argDenLen;
if(argDenotationType != Value) {
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
if(argDenotationType == Abbreviation) {
for(Argument *arg : args) {
@ -481,7 +516,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
}
} else {
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;
break;
}
@ -490,21 +525,18 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
if(matchingArg) {
// an argument matched the specified denotation
matchingArg->m_indices.push_back(index);
matchingArg->m_occurances.emplace_back(index, parentPath, parentArg);
// prepare reading parameter values
matchingArg->m_values.emplace_back();
values = &matchingArg->m_values.back();
values = &matchingArg->m_occurances.back().values;
if(equationPos) {
values->push_back(equationPos + 1);
}
// 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)) {
currentPath.push_back(matchingArg);
readSpecifiedArgs(matchingArg->m_subArgs, index, ++argv, end, currentPath);
currentPath.pop_back();
readSpecifiedArgs(matchingArg->m_subArgs, index, ++argv, end, lastArg, completionMode);
break;
} // else: another abbreviated argument follows
} else {
@ -516,8 +548,8 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
if(!matchingArg) {
if(argDenotationType != Value) {
// unknown argument might be a sibling of the parent element
for(auto parentArgument = currentPath.crbegin(), end = currentPath.crend(); parentArgument != end; ) {
for(Argument *sibling : (++parentArgument != end ? (*parentArgument)->subArguments() : m_mainArgs)) {
for(auto parentArgument = parentPath.crbegin(), pathEnd = parentPath.crend(); ; ++parentArgument) {
for(Argument *sibling : (parentArgument != pathEnd ? (*parentArgument)->subArguments() : m_mainArgs)) {
if(sibling->occurrences() < sibling->maxOccurrences()) {
if((argDenotationType == Abbreviation && (sibling->abbreviation() && sibling->abbreviation() == *argDenotation))
|| (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
values->emplace_back(abbreviationFound ? argDenotation : *argv);
++index, ++argv;
@ -536,21 +571,21 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
}
// first value might denote "operation"
if(currentPath.empty()) {
if(!index) {
for(Argument *arg : args) {
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;
break;
}
}
}
if(!matchingArg) {
if(!matchingArg && (!completionMode || (argv + 1 != end))) {
// use the first default argument which is not already present
for(Argument *arg : args) {
if(arg->isImplicit() && !arg->isPresent()) {
(matchingArg = arg)->m_indices.push_back(index);
(matchingArg = arg)->m_occurances.emplace_back(index, parentPath, parentArg);
break;
}
}
@ -558,28 +593,33 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
if(matchingArg) {
// an argument matched the specified denotation
if(lastArg == matchingArg) {
break;
if(lastArgInLevel == matchingArg) {
break; // TODO: why?
}
// prepare reading parameter values
matchingArg->m_values.emplace_back();
values = &matchingArg->m_values.back();
values = &matchingArg->m_occurances.back().values;
// read sub arguments
++m_actualArgc, lastArg = matchingArg;
currentPath.push_back(matchingArg);
readSpecifiedArgs(matchingArg->m_subArgs, index, argv, end, currentPath);
currentPath.pop_back();
++m_actualArgc, lastArg = lastArgInLevel = matchingArg;
readSpecifiedArgs(matchingArg->m_subArgs, index, argv, end, lastArg, completionMode);
continue;
}
if(currentPath.empty()) {
if(m_ignoreUnknownArgs) {
cerr << "The specified argument \"" << *argv << "\" is unknown and will be ignored." << endl;
if(!parentArg) {
if(completionMode) {
++index, ++argv;
} 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 {
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.
@ -652,8 +979,8 @@ void ArgumentParser::invokeCallbacks(const ArgumentVector &args)
for(const Argument *arg : args) {
// invoke the callback for each occurance of the argument
if(arg->m_callbackFunction) {
for(const auto &valuesOfOccurance : arg->m_values) {
arg->m_callbackFunction(valuesOfOccurance);
for(const auto &occurance : arg->m_occurances) {
arg->m_callbackFunction(occurance.values);
}
}
// invoke the callbacks for sub arguments recursively

View File

@ -10,6 +10,8 @@
# include <cassert>
#endif
class ArgumentParserTests;
namespace ApplicationUtilities {
LIB_EXPORT extern const char *applicationName;
@ -30,8 +32,65 @@ typedef std::initializer_list<Argument *> ArgumentInitializerList;
typedef std::vector<Argument *> ArgumentVector;
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);
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
{
friend class ArgumentParser;
@ -59,10 +118,11 @@ public:
bool allRequiredValuesPresent(std::size_t occurrance = 0) const;
bool isPresent() 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 maxOccurrences() const;
void setConstraints(std::size_t minOccurrences, std::size_t maxOccurrences);
const std::vector<Argument *> &path(std::size_t occurrance = 0) const;
bool isRequired() const;
void setRequired(bool required);
bool isCombinable() const;
@ -80,6 +140,10 @@ public:
const ArgumentVector parents() const;
bool isMainArgument() const;
bool isParentPresent() const;
ValueCompletionBehavior valueCompletionBehaviour() const;
void setValueCompletionBehavior(ValueCompletionBehavior valueCompletionBehaviour);
const char *preDefinedCompletionValues() const;
void setPreDefinedCompletionValues(const char *preDefinedCompletionValues);
Argument *conflictsWithArgument() const;
void reset();
@ -95,12 +159,47 @@ private:
std::size_t m_requiredValueCount;
std::vector<const char *> m_valueNames;
bool m_implicit;
std::vector<std::size_t> m_indices;
std::vector<std::vector<const char *> > m_values;
std::vector<ArgumentOccurance> m_occurances;
ArgumentVector m_subArgs;
CallbackFunction m_callbackFunction;
ArgumentVector m_parents;
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
{
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
{
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
{
return !m_indices.empty();
return !m_occurances.empty();
}
/*!
@ -326,15 +425,15 @@ inline bool Argument::isPresent() 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.
*/
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;
}
/*!
* \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.
*
@ -508,37 +615,45 @@ inline bool Argument::isMainArgument() const
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:
ArgumentParser();
return m_valueCompletionBehavior;
}
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;
bool areUnknownArgumentsIgnored() const;
void setIgnoreUnknownArguments(bool ignore);
Argument *defaultArgument() const;
void setDefaultArgument(Argument *argument);
/*!
* \brief Sets the items to be considered when generating completion for the values.
*/
inline void Argument::setValueCompletionBehavior(ValueCompletionBehavior completionValues)
{
m_valueCompletionBehavior = completionValues;
}
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, std::vector<Argument *> &currentPath);
void checkConstraints(const ArgumentVector &args);
void invokeCallbacks(const ArgumentVector &args);
/*!
* \brief Returns the assigned values used when generating completion for the values.
*/
inline const char *Argument::preDefinedCompletionValues() const
{
return m_preDefinedCompletionValues;
}
ArgumentVector m_mainArgs;
unsigned int m_actualArgc;
const char *m_executable;
bool m_ignoreUnknownArgs;
Argument *m_defaultArg;
};
/*!
* \brief Assignes the values to be used when generating completion for the values.
*/
inline void Argument::setPreDefinedCompletionValues(const char *preDefinedCompletionValues)
{
m_preDefinedCompletionValues = preDefinedCompletionValues;
}
/*!
* \brief Resets occurrences (indices, values and paths).
*/
inline void Argument::reset()
{
m_occurances.clear();
}
/*!
* \brief Returns the main arguments.
@ -566,35 +681,23 @@ inline const char *ArgumentParser::executable() const
}
/*!
* \brief Returns an indication whether unknown arguments detected
* when parsing should be ignored.
* \brief Returns how unknown arguments are treated.
*
* If unknown arguments are not ignored the parser will throw a
* Failure when an unknown argument is detected.
* Otherwise only a warning will be shown.
*
* The default value is false.
*
* \sa setIgnoreUnknownArguments()
* The default value is UnknownArgumentBehavior::Fail.
*/
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
* when parsing.
* \brief Sets how unknown arguments are treated.
*
* If set to false the parser should throw a Failure object
* when an unknown argument is found. Otherwise only a warning
* will be printed.
*
* \sa areUnknownArgumentsIgnored()
* The default value is UnknownArgumentBehavior::Fail.
*/
inline void ArgumentParser::setIgnoreUnknownArguments(bool ignore)
inline void ArgumentParser::setUnknownArgumentBehavior(UnknownArgumentBehavior behavior)
{
m_ignoreUnknownArgs = ignore;
m_unknownArgBehavior = behavior;
}
/*!

View File

@ -4,8 +4,8 @@
#include <iostream>
#ifdef PLATFORM_WINDOWS
#include <windows.h>
#include <fcntl.h>
# include <windows.h>
# include <fcntl.h>
#endif
using namespace std;

View File

@ -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()

View File

@ -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@

View File

@ -4,6 +4,8 @@
#include "../application/failure.h"
#include "../application/fakeqtconfigarguments.h"
#include "../io/path.h"
#include "resources/config.h"
#include <cppunit/extensions/HelperMacros.h>
@ -25,6 +27,7 @@ class ArgumentParserTests : public TestFixture
CPPUNIT_TEST(testArgument);
CPPUNIT_TEST(testParsing);
CPPUNIT_TEST(testCallbacks);
CPPUNIT_TEST(testBashCompletion);
CPPUNIT_TEST_SUITE_END();
public:
@ -34,6 +37,7 @@ public:
void testArgument();
void testParsing();
void testCallbacks();
void testBashCompletion();
private:
void callback();
@ -126,7 +130,7 @@ void ArgumentParserTests::testParsing()
CPPUNIT_ASSERT_THROW(displayTagInfoArg.values().at(3), out_of_range);
// 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
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
parser.parseArgs(9, argv2);
@ -145,7 +149,7 @@ void ArgumentParserTests::testParsing()
CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
// 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();
// 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."));
}
// 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();
parser.setIgnoreUnknownArguments(true);
parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Warn);
// redirect stderr to check whether warnings are printed correctly
stringstream buffer;
streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf());
@ -170,10 +174,10 @@ void ArgumentParserTests::testParsing()
throw;
}
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 \"diskpos\" is unknown and will be ignored.\n"
"The specified argument \"--file\" is unknown and will be ignored.\n"
"The specified argument \"somefile\" 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 \"--files\" 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
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
@ -184,7 +188,7 @@ void ArgumentParserTests::testParsing()
// test abbreviations like "-vf"
const char *argv4[] = {"tageditor", "-i", "-vf", "test"};
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
parser.setIgnoreUnknownArguments(false);
parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Fail);
parser.parseArgs(4, argv4);
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(displayFileInfoArg.isPresent());
@ -287,6 +291,9 @@ void ArgumentParserTests::testParsing()
}
}
/*!
* \brief Tests whether callbacks are called correctly.
*/
void ArgumentParserTests::testCallbacks()
{
ArgumentParser parser;
@ -315,3 +322,150 @@ void ArgumentParserTests::testCallbacks()
const char *argv2[] = {"test", "-l", "val1", "val2"};
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;
}
}