diff --git a/CMakeLists.txt b/CMakeLists.txt index 46a9712..b9c305f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/application/argumentparser.cpp b/application/argumentparser.cpp index 217a97b..839158b 100644 --- a/application/argumentparser.cpp +++ b/application/argumentparser.cpp @@ -3,17 +3,21 @@ #include "./failure.h" #include "../conversion/stringconversion.h" -#include "../misc/random.h" +#include "../io/path.h" #include #include #include #include #include +#ifdef LOGGING_ENABLED +# include +#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(*(++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 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(argc), currentWordIndex + 1) : static_cast(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 ¤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 &parentPath = parentArg ? parentArg->path(parentArg->occurrences() - 1) : vector(); + Argument *lastArgInLevel = nullptr; vector *values = nullptr; while(argv != end) { - if(values && lastArg->requiredValueCount() != static_cast(-1) && values->size() < lastArg->requiredValueCount()) { + if(values && lastArgInLevel->requiredValueCount() != static_cast(-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(equationPos - argDenotation) : strlen(argDenotation); ; matchingArg = nullptr) { + for(argDenLen = equationPos ? static_cast(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 &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 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 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(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(-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(-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(unescapedOpening, "\\ ", " "); + findAndReplace(unescapedOpening, "\\,", ","); + findAndReplace(unescapedOpening, "\\[", "["); + findAndReplace(unescapedOpening, "\\]", "]"); + findAndReplace(unescapedOpening, "\\!", "!"); + findAndReplace(unescapedOpening, "\\#", "#"); + findAndReplace(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 diff --git a/application/argumentparser.h b/application/argumentparser.h index 87b0b6b..532c89a 100644 --- a/application/argumentparser.h +++ b/application/argumentparser.h @@ -10,6 +10,8 @@ # include #endif +class ArgumentParserTests; + namespace ApplicationUtilities { LIB_EXPORT extern const char *applicationName; @@ -30,8 +32,65 @@ typedef std::initializer_list ArgumentInitializerList; typedef std::vector ArgumentVector; typedef std::function 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(static_cast(lhs) | static_cast(rhs)); +} + +constexpr bool operator&(ValueCompletionBehavior lhs, ValueCompletionBehavior rhs) +{ + return static_cast(static_cast(lhs) & static_cast(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 parentPath, Argument *parent); + + std::size_t index; + std::vector values; + std::vector path; +}; + +inline ArgumentOccurance::ArgumentOccurance(std::size_t index) : + index(index) +{} + +inline ArgumentOccurance::ArgumentOccurance(std::size_t index, const std::vector 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 &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 &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 m_valueNames; bool m_implicit; - std::vector m_indices; - std::vector > m_values; + std::vector 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 &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(-1) - || (m_values[occurrance].size() >= static_cast(m_requiredValueCount)); + || (m_occurances[occurrance].values.size() >= static_cast(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 &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::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 ¤tPath); - 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; } /*! diff --git a/application/commandlineutils.cpp b/application/commandlineutils.cpp index 4ef01da..f47cdd7 100644 --- a/application/commandlineutils.cpp +++ b/application/commandlineutils.cpp @@ -4,8 +4,8 @@ #include #ifdef PLATFORM_WINDOWS -#include -#include +# include +# include #endif using namespace std; diff --git a/cmake/modules/ShellCompletion.cmake b/cmake/modules/ShellCompletion.cmake new file mode 100644 index 0000000..6dc1e4e --- /dev/null +++ b/cmake/modules/ShellCompletion.cmake @@ -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() diff --git a/cmake/templates/bash-completion.sh.in b/cmake/templates/bash-completion.sh.in new file mode 100755 index 0000000..601e6f7 --- /dev/null +++ b/cmake/templates/bash-completion.sh.in @@ -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@ diff --git a/tests/argumentparsertests.cpp b/tests/argumentparsertests.cpp index f521c41..e189085 100644 --- a/tests/argumentparsertests.cpp +++ b/tests/argumentparsertests.cpp @@ -4,6 +4,8 @@ #include "../application/failure.h" #include "../application/fakeqtconfigarguments.h" +#include "../io/path.h" + #include "resources/config.h" #include @@ -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; + } +}