From df8d942e1c26b8d3c739129c59feea4912c25fdb Mon Sep 17 00:00:00 2001 From: Martchus Date: Sun, 31 Jul 2016 23:20:31 +0200 Subject: [PATCH] Improve argument parser - Fix completion of values already containing '=' - Fix completion when current word contains '=' - Improve formatting of help - Fix typo --- application/argumentparser.cpp | 94 +++++++++++++++++++++++++--------- application/argumentparser.h | 84 +++++++++++++++--------------- io/ansiescapecodes.h | 5 ++ tests/argumentparsertests.cpp | 12 ++--- 4 files changed, 123 insertions(+), 72 deletions(-) diff --git a/application/argumentparser.cpp b/application/argumentparser.cpp index 0fcd8ca..0970188 100644 --- a/application/argumentparser.cpp +++ b/application/argumentparser.cpp @@ -4,6 +4,7 @@ #include "../conversion/stringconversion.h" #include "../io/path.h" +#include "../io/ansiescapecodes.h" #include #include @@ -89,7 +90,7 @@ Argument::~Argument() {} /*! - * \brief Returns the first parameter value of the first occurance of the argument. + * \brief Returns the first parameter value of the first occurrence of the argument. * \remarks * - If the argument is not present and the an environment variable has been set * using setEnvironmentVariable() the value of the specified variable will be returned. @@ -97,8 +98,8 @@ Argument::~Argument() */ const char *Argument::firstValue() const { - if(!m_occurances.empty() && !m_occurances.front().values.empty()) { - return m_occurances.front().values.front(); + if(!m_occurrences.empty() && !m_occurrences.front().values.empty()) { + return m_occurrences.front().values.front(); } else if(m_environmentVar) { return getenv(m_environmentVar); } else { @@ -112,6 +113,7 @@ const char *Argument::firstValue() const void Argument::printInfo(ostream &os, unsigned char indentionLevel) const { for(unsigned char i = 0; i < indentionLevel; ++i) os << ' ' << ' '; + EscapeCodes::setStyle(os, EscapeCodes::TextAttribute::Bold); if(notEmpty(name())) { os << '-' << '-' << name(); } @@ -121,6 +123,7 @@ void Argument::printInfo(ostream &os, unsigned char indentionLevel) const if(abbreviation()) { os << '-' << abbreviation(); } + EscapeCodes::setStyle(os); if(requiredValueCount()) { unsigned int valueNamesPrint = 0; for(auto i = valueNames().cbegin(), end = valueNames().cend(); i != end && valueNamesPrint < requiredValueCount(); ++i) { @@ -331,6 +334,7 @@ void ArgumentParser::addMainArgument(Argument *argument) */ void ArgumentParser::printHelp(ostream &os) const { + EscapeCodes::setStyle(os, EscapeCodes::TextAttribute::Bold); if(applicationName && *applicationName) { os << applicationName; if(applicationVersion && *applicationVersion) { @@ -343,9 +347,11 @@ void ArgumentParser::printHelp(ostream &os) const if((applicationName && *applicationName) || (applicationVersion && *applicationVersion)) { os << '\n' << '\n'; } + EscapeCodes::setStyle(os); if(!m_mainArgs.empty()) { - os << "Available arguments:\n"; - for(const auto *arg : m_mainArgs) { + os << "Available arguments:"; + for(const Argument *arg : m_mainArgs) { + os << '\n'; arg->printInfo(os); } } @@ -414,7 +420,7 @@ void ArgumentParser::parseArgs(int argc, const char *const *argv) } else { // no arguments specified -> flag default argument as present if one is assigned if(m_defaultArg) { - m_defaultArg->m_occurances.emplace_back(0); + m_defaultArg->m_occurrences.emplace_back(0); } } checkConstraints(m_mainArgs); @@ -518,10 +524,10 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index, if(matchingArg) { // an argument matched the specified denotation - matchingArg->m_occurances.emplace_back(index, parentPath, parentArg); + matchingArg->m_occurrences.emplace_back(index, parentPath, parentArg); // prepare reading parameter values - values = &matchingArg->m_occurances.back().values; + values = &matchingArg->m_occurrences.back().values; if(equationPos) { values->push_back(equationPos + 1); } @@ -567,7 +573,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index, if(!index) { for(Argument *arg : args) { if(arg->denotesOperation() && arg->name() && !strcmp(arg->name(), *argv)) { - (matchingArg = arg)->m_occurances.emplace_back(index, parentPath, parentArg); + (matchingArg = arg)->m_occurrences.emplace_back(index, parentPath, parentArg); ++index, ++argv; break; } @@ -578,7 +584,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index, // use the first default argument which is not already present for(Argument *arg : args) { if(arg->isImplicit() && !arg->isPresent()) { - (matchingArg = arg)->m_occurances.emplace_back(index, parentPath, parentArg); + (matchingArg = arg)->m_occurrences.emplace_back(index, parentPath, parentArg); break; } } @@ -591,7 +597,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index, } // prepare reading parameter values - values = &matchingArg->m_occurances.back().values; + values = &matchingArg->m_occurrences.back().values; // read sub arguments ++m_actualArgc, lastArg = lastArgInLevel = matchingArg; @@ -733,10 +739,34 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi // read the "opening" (started but not finished argument denotation) const char *opening = nullptr; - size_t openingLen; + string compoundOpening; + size_t openingLen, compoundOpeningStartLen = 0; unsigned char openingDenotationType = Value; if(argc && nextArgumentOrValue) { - opening = (currentWordIndex < argc ? argv[currentWordIndex] : *lastSpecifiedArg); + if(currentWordIndex < argc) { + opening = argv[currentWordIndex]; + // For some reasons completions for eg. "set --values disk=1 tag=a" are splitted so the + // equation sign is an own argument ("set --values disk = 1 tag = a"). + // This is not how values are treated by the argument parser. Hence the opening + // must be joined again. In this case only the part after the equation sign needs to be + // provided for completion so compoundOpeningStartLen is set to number of characters to skip. + size_t minCurrentWordIndex = (lastDetectedArg ? lastDetectedArgIndex : 0); + if(currentWordIndex > minCurrentWordIndex && !strcmp(opening, "=")) { + compoundOpening.reserve(compoundOpeningStartLen = strlen(argv[--currentWordIndex]) + 1); + compoundOpening = argv[currentWordIndex]; + compoundOpening += '='; + } else if(currentWordIndex > (minCurrentWordIndex + 1) && !strcmp(argv[currentWordIndex - 1], "=")) { + compoundOpening.reserve((compoundOpeningStartLen = strlen(argv[currentWordIndex -= 2]) + 1) + strlen(opening)); + compoundOpening = argv[currentWordIndex]; + compoundOpening += '='; + compoundOpening += opening; + } + if(!compoundOpening.empty()) { + opening = compoundOpening.data(); + } + } else { + opening = *lastSpecifiedArg; + } *opening == '-' && (++opening, ++openingDenotationType) && *opening == '-' && (++opening, ++openingDenotationType); openingLen = strlen(opening); @@ -752,24 +782,31 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi bool appendEquationSign = arg->valueCompletionBehaviour() & ValueCompletionBehavior::AppendEquationSign; if(argc && currentWordIndex <= lastSpecifiedArgIndex && opening) { if(openingDenotationType == Value) { - bool wordStart = true, ok = false; + bool wordStart = true, ok = false, equationSignAlreadyPresent = false; + int wordIndex = 0; 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'); + wordIndex = 0; + } else if((wordStart = (*i == ' ') || (*i == '\n'))) { + equationSignAlreadyPresent = false; + } else if(*i == '=') { + equationSignAlreadyPresent = true; } if(ok) { - cout << *i; - ++i; - if(appendEquationSign) { + if(!compoundOpeningStartLen || wordIndex >= compoundOpeningStartLen) { + cout << *i; + } + ++i, ++wordIndex; + if(appendEquationSign && !equationSignAlreadyPresent) { switch(*i) { case ' ': case '\n': case '\0': cout << '='; noWhitespace = true; + equationSignAlreadyPresent = false; } } } else { @@ -779,11 +816,18 @@ void ArgumentParser::printBashCompletion(int argc, const char *const *argv, unsi cout << ' '; } } else if(appendEquationSign) { + bool equationSignAlreadyPresent = false; for(const char *i = arg->preDefinedCompletionValues(); *i;) { cout << *i; switch(*(++i)) { + case '=': + equationSignAlreadyPresent = true; + break; case ' ': case '\n': case '\0': - cout << '='; + if(!equationSignAlreadyPresent) { + cout << '='; + equationSignAlreadyPresent = false; + } } } } else { @@ -964,16 +1008,16 @@ void ArgumentParser::checkConstraints(const ArgumentVector &args) * \brief Invokes the callbacks for the specified \a args. * \remarks * - Checks the callbacks for sub arguments, too. - * - Invokes the assigned callback methods for each occurance of + * - Invokes the assigned callback methods for each occurrence of * the argument. */ void ArgumentParser::invokeCallbacks(const ArgumentVector &args) { for(const Argument *arg : args) { - // invoke the callback for each occurance of the argument + // invoke the callback for each occurrence of the argument if(arg->m_callbackFunction) { - for(const auto &occurance : arg->m_occurances) { - arg->m_callbackFunction(occurance); + for(const auto &occurrence : arg->m_occurrences) { + arg->m_callbackFunction(occurrence); } } // invoke the callbacks for sub arguments recursively @@ -993,7 +1037,7 @@ void ArgumentParser::invokeCallbacks(const ArgumentVector &args) HelpArgument::HelpArgument(ArgumentParser &parser) : Argument("help", 'h', "shows this information") { - setCallback([&parser] (const ArgumentOccurance &) { + setCallback([&parser] (const ArgumentOccurrence &) { CMD_UTILS_START_CONSOLE; parser.printHelp(cout); }); diff --git a/application/argumentparser.h b/application/argumentparser.h index 39b89ae..afbfab0 100644 --- a/application/argumentparser.h +++ b/application/argumentparser.h @@ -48,14 +48,15 @@ enum class UnknownArgumentBehavior */ enum class ValueCompletionBehavior : unsigned char { - None = 0, - PreDefinedValues = 2, - Files = 4, - Directories = 8, - FileSystemIfNoPreDefinedValues = 16, - AppendEquationSign = 32 + None = 0, /**< no auto-completion */ + PreDefinedValues = 2, /**< values assigned with Argument::setPreDefinedCompletionValues() */ + Files = 4, /**< files */ + Directories = 8, /**< directories */ + FileSystemIfNoPreDefinedValues = 16, /**< files and directories but only if no values have been assigned (default behavior) */ + AppendEquationSign = 32 /**< an equation sign is appended to values which not contain an equation sign already */ }; +/// \cond constexpr ValueCompletionBehavior operator|(ValueCompletionBehavior lhs, ValueCompletionBehavior rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); @@ -65,50 +66,51 @@ constexpr bool operator&(ValueCompletionBehavior lhs, ValueCompletionBehavior rh { return static_cast(static_cast(lhs) & static_cast(rhs)); } +/// \endcond Argument LIB_EXPORT *firstPresentUncombinableArg(const ArgumentVector &args, const Argument *except); /*! - * \brief The ArgumentOccurance struct holds argument values for an occurance of an argument. + * \brief The ArgumentOccurrence struct holds argument values for an occurrence of an argument. */ -struct LIB_EXPORT ArgumentOccurance +struct LIB_EXPORT ArgumentOccurrence { - ArgumentOccurance(std::size_t index); - ArgumentOccurance(std::size_t index, const std::vector parentPath, Argument *parent); + ArgumentOccurrence(std::size_t index); + ArgumentOccurrence(std::size_t index, const std::vector parentPath, Argument *parent); /*! - * \brief The index of the occurance. This is not necessarily the index in the argv array. + * \brief The index of the occurrence. This is not necessarily the index in the argv array. */ std::size_t index; /*! - * \brief The parameter values which have been specified after the occurance of the argument. + * \brief The parameter values which have been specified after the occurrence of the argument. */ std::vector values; /*! - * \brief The "path" of the occurance (the parent elements which have been specified before). - * \remarks Empty for top-level occurances. + * \brief The "path" of the occurrence (the parent elements which have been specified before). + * \remarks Empty for top-level occurrences. */ std::vector path; }; /*! - * \brief Constructs an argument occurance for the specified \a index. + * \brief Constructs an argument occurrence for the specified \a index. */ -inline ArgumentOccurance::ArgumentOccurance(std::size_t index) : +inline ArgumentOccurrence::ArgumentOccurrence(std::size_t index) : index(index) {} /*! - * \brief Constructs an argument occurance. + * \brief Constructs an argument occurrence. * \param index Specifies the index. * \param parentPath Specifies the path of \a parent. - * \param parent Specifies the parent which might be nullptr for top-level occurances. + * \param parent Specifies the parent which might be nullptr for top-level occurrences. * - * The path of the new occurance is built from the specified \a parentPath and \a parent. + * The path of the new occurrence is built from the specified \a parentPath and \a parent. */ -inline ArgumentOccurance::ArgumentOccurance(std::size_t index, const std::vector parentPath, Argument *parent) : +inline ArgumentOccurrence::ArgumentOccurrence(std::size_t index, const std::vector parentPath, Argument *parent) : index(index), path(parentPath) { @@ -122,7 +124,7 @@ class LIB_EXPORT Argument friend class ArgumentParser; public: - typedef std::function CallbackFunction; + typedef std::function CallbackFunction; Argument(const char *name, char abbreviation = '\0', const char *description = nullptr, const char *example = nullptr); ~Argument(); @@ -137,21 +139,21 @@ public: void setDescription(const char *description); const char *example() const; void setExample(const char *example); - const std::vector &values(std::size_t occurrance = 0) const; + const std::vector &values(std::size_t occurrence = 0) const; const char *firstValue() const; std::size_t requiredValueCount() const; void setRequiredValueCount(std::size_t requiredValueCount); const std::vector &valueNames() const; void setValueNames(std::initializer_list valueNames); void appendValueName(const char *valueName); - bool allRequiredValuesPresent(std::size_t occurrance = 0) const; + bool allRequiredValuesPresent(std::size_t occurrence = 0) const; bool isPresent() const; std::size_t occurrences() const; - std::size_t index(std::size_t occurrance) const; + std::size_t index(std::size_t occurrence) 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; + const std::vector &path(std::size_t occurrence = 0) const; bool isRequired() const; void setRequired(bool required); bool isCombinable() const; @@ -189,7 +191,7 @@ private: std::size_t m_requiredValueCount; std::vector m_valueNames; bool m_implicit; - std::vector m_occurances; + std::vector m_occurrences; ArgumentVector m_subArgs; CallbackFunction m_callbackFunction; ArgumentVector m_parents; @@ -346,14 +348,14 @@ inline void Argument::setExample(const char *example) } /*! - * \brief Returns the parameter values for the specified \a occurrance of argument. + * \brief Returns the parameter values for the specified \a occurrence of argument. * \remarks * - The values are set by the parser when parsing the command line arguments. - * - The specified \a occurance must be less than occurrences(). + * - The specified \a occurrence must be less than occurrences(). */ -inline const std::vector &Argument::values(std::size_t occurrance) const +inline const std::vector &Argument::values(std::size_t occurrence) const { - return m_occurances[occurrance].values; + return m_occurrences[occurrence].values; } /*! @@ -435,10 +437,10 @@ inline void Argument::appendValueName(const char *valueName) /*! * \brief Returns an indication whether all required values are present. */ -inline bool Argument::allRequiredValuesPresent(std::size_t occurrance) const +inline bool Argument::allRequiredValuesPresent(std::size_t occurrence) const { return m_requiredValueCount == static_cast(-1) - || (m_occurances[occurrance].values.size() >= static_cast(m_requiredValueCount)); + || (m_occurrences[occurrence].values.size() >= static_cast(m_requiredValueCount)); } /*! @@ -464,7 +466,7 @@ inline void Argument::setImplicit(bool implicit) */ inline bool Argument::isPresent() const { - return !m_occurances.empty(); + return !m_occurrences.empty(); } /*! @@ -472,15 +474,15 @@ inline bool Argument::isPresent() const */ inline std::size_t Argument::occurrences() const { - return m_occurances.size(); + return m_occurrences.size(); } /*! * \brief Returns the indices of the argument's occurences which could be detected when parsing. */ -inline std::size_t Argument::index(std::size_t occurrance) const +inline std::size_t Argument::index(std::size_t occurrence) const { - return m_occurances[occurrance].index; + return m_occurrences[occurrence].index; } /*! @@ -515,11 +517,11 @@ inline void Argument::setConstraints(std::size_t minOccurrences, std::size_t max } /*! - * \brief Returns the path of the specified \a occurrance. + * \brief Returns the path of the specified \a occurrence. */ -inline const std::vector &Argument::path(std::size_t occurrance) const +inline const std::vector &Argument::path(std::size_t occurrence) const { - return m_occurances[occurrance].path; + return m_occurrences[occurrence].path; } /*! @@ -607,7 +609,7 @@ inline void Argument::setDenotesOperation(bool denotesOperation) /*! * \brief Sets a \a callback function which will be called by the parser if * the argument could be found and no parsing errors occured. - * \remarks The \a callback will be called for each occurrance of the argument. + * \remarks The \a callback will be called for each occurrence of the argument. */ inline void Argument::setCallback(Argument::CallbackFunction callback) { @@ -699,7 +701,7 @@ inline void Argument::setPreDefinedCompletionValues(const char *preDefinedComple */ inline void Argument::reset() { - m_occurances.clear(); + m_occurrences.clear(); } /*! diff --git a/io/ansiescapecodes.h b/io/ansiescapecodes.h index 4ad4356..67b1c44 100644 --- a/io/ansiescapecodes.h +++ b/io/ansiescapecodes.h @@ -49,6 +49,11 @@ enum class Direction : char Backward = 'D' }; +inline void setStyle(std::ostream &stream, TextAttribute displayAttribute = TextAttribute::Reset) +{ + stream << '\e' << '[' << static_cast(displayAttribute) << 'm'; +} + inline void setStyle(std::ostream &stream, Color color, ColorContext context = ColorContext::Foreground, TextAttribute displayAttribute = TextAttribute::Reset) diff --git a/tests/argumentparsertests.cpp b/tests/argumentparsertests.cpp index 08988c8..81dd79c 100644 --- a/tests/argumentparsertests.cpp +++ b/tests/argumentparsertests.cpp @@ -314,12 +314,12 @@ void ArgumentParserTests::testCallbacks() ArgumentParser parser; Argument callbackArg("with-callback", 't', "callback test"); callbackArg.setRequiredValueCount(2); - callbackArg.setCallback([] (const ArgumentOccurance &occurance) { - CPPUNIT_ASSERT_EQUAL(static_cast(0), occurance.index); - CPPUNIT_ASSERT(occurance.path.empty()); - CPPUNIT_ASSERT_EQUAL(static_cast(2), occurance.values.size()); - CPPUNIT_ASSERT(!strcmp(occurance.values[0], "val1")); - CPPUNIT_ASSERT(!strcmp(occurance.values[1], "val2")); + callbackArg.setCallback([] (const ArgumentOccurrence &occurrence) { + CPPUNIT_ASSERT_EQUAL(static_cast(0), occurrence.index); + CPPUNIT_ASSERT(occurrence.path.empty()); + CPPUNIT_ASSERT_EQUAL(static_cast(2), occurrence.values.size()); + CPPUNIT_ASSERT(!strcmp(occurrence.values[0], "val1")); + CPPUNIT_ASSERT(!strcmp(occurrence.values[1], "val2")); throw 42; }); Argument noCallbackArg("no-callback", 'l', "callback test");