Improve argument parser

- Fix completion of values already containing '='
- Fix completion when current word contains '='
- Improve formatting of help
- Fix typo
This commit is contained in:
Martchus 2016-07-31 23:20:31 +02:00
parent 17d22be584
commit df8d942e1c
4 changed files with 123 additions and 72 deletions

View File

@ -4,6 +4,7 @@
#include "../conversion/stringconversion.h"
#include "../io/path.h"
#include "../io/ansiescapecodes.h"
#include <algorithm>
#include <iostream>
@ -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);
});

View File

@ -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<ValueCompletionBehavior>(static_cast<unsigned char>(lhs) | static_cast<unsigned char>(rhs));
@ -65,50 +66,51 @@ constexpr bool operator&(ValueCompletionBehavior lhs, ValueCompletionBehavior rh
{
return static_cast<bool>(static_cast<unsigned char>(lhs) & static_cast<unsigned char>(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<Argument *> parentPath, Argument *parent);
ArgumentOccurrence(std::size_t index);
ArgumentOccurrence(std::size_t index, const std::vector<Argument *> 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<const char *> 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<Argument *> 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<Argument *> parentPath, Argument *parent) :
inline ArgumentOccurrence::ArgumentOccurrence(std::size_t index, const std::vector<Argument *> parentPath, Argument *parent) :
index(index),
path(parentPath)
{
@ -122,7 +124,7 @@ class LIB_EXPORT Argument
friend class ArgumentParser;
public:
typedef std::function <void (const ArgumentOccurance &)> CallbackFunction;
typedef std::function <void (const ArgumentOccurrence &)> 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<const char *> &values(std::size_t occurrance = 0) const;
const std::vector<const char *> &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<const char *> &valueNames() const;
void setValueNames(std::initializer_list<const char *> 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<Argument *> &path(std::size_t occurrance = 0) const;
const std::vector<Argument *> &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<const char *> m_valueNames;
bool m_implicit;
std::vector<ArgumentOccurance> m_occurances;
std::vector<ArgumentOccurrence> 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<const char *> &Argument::values(std::size_t occurrance) const
inline const std::vector<const char *> &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<std::size_t>(-1)
|| (m_occurances[occurrance].values.size() >= static_cast<std::size_t>(m_requiredValueCount));
|| (m_occurrences[occurrence].values.size() >= static_cast<std::size_t>(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 *> &Argument::path(std::size_t occurrance) const
inline const std::vector<Argument *> &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();
}
/*!

View File

@ -49,6 +49,11 @@ enum class Direction : char
Backward = 'D'
};
inline void setStyle(std::ostream &stream, TextAttribute displayAttribute = TextAttribute::Reset)
{
stream << '\e' << '[' << static_cast<char>(displayAttribute) << 'm';
}
inline void setStyle(std::ostream &stream, Color color,
ColorContext context = ColorContext::Foreground,
TextAttribute displayAttribute = TextAttribute::Reset)

View File

@ -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<size_t>(0), occurance.index);
CPPUNIT_ASSERT(occurance.path.empty());
CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(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<size_t>(0), occurrence.index);
CPPUNIT_ASSERT(occurrence.path.empty());
CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(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");