Improve argument parser

- Fix some implementation details
- Extend tests
This commit is contained in:
Martchus 2016-06-14 00:43:32 +02:00
parent 526cbc5282
commit 79ce6e9aa6
3 changed files with 162 additions and 98 deletions

View File

@ -61,7 +61,7 @@ Argument::Argument(const char *name, char abbreviation, const char *description,
m_combinable(false),
m_denotesOperation(false),
m_requiredValueCount(0),
m_default(false),
m_implicit(false),
m_isMainArg(false)
{}
@ -248,24 +248,44 @@ void Argument::reset()
*/
ArgumentParser::ArgumentParser() :
m_actualArgc(0),
m_currentDirectory(nullptr),
m_ignoreUnknownArgs(false)
m_executable(nullptr),
m_ignoreUnknownArgs(false),
m_defaultArg(nullptr)
{}
/*!
* \brief Sets the main arguments for the parser. The parser will use these argument definitions
* to when parsing the command line arguments and when printing help information.
*
* \remarks
* The parser does not take ownership. Do not destroy the arguments as long as they are used as
* main arguments.
* - The parser does not take ownership. Do not destroy the arguments as long as they are used as
* main arguments.
* - Sets the first specified argument as default argument if none is assigned yet and the
* first argument has no mandatory sub arguments.
*/
void ArgumentParser::setMainArguments(const ArgumentInitializerList &mainArguments)
{
for(Argument *arg : mainArguments) {
arg->m_isMainArg = true;
if(mainArguments.size()) {
for(Argument *arg : mainArguments) {
arg->m_isMainArg = true;
}
m_mainArgs.assign(mainArguments);
if(!m_defaultArg) {
if(!(*mainArguments.begin())->requiredValueCount()) {
bool subArgsRequired = false;
for(Argument *subArg : (*mainArguments.begin())->subArguments()) {
if(subArg->isRequired()) {
subArgsRequired = true;
break;
}
}
if(!subArgsRequired) {
m_defaultArg = *mainArguments.begin();
}
}
}
} else {
m_mainArgs.clear();
}
m_mainArgs.assign(mainArguments);
}
/*!
@ -347,15 +367,23 @@ void ArgumentParser::parseArgs(int argc, const char *argv[])
{
IF_DEBUG_BUILD(verifyArgs(m_mainArgs);)
m_actualArgc = 0;
if(argc > 0) {
m_currentDirectory = *argv;
size_t index = 0;
++argv;
readSpecifiedArgs(m_mainArgs, index, argv, argv + argc - 1);
if(argc) {
m_executable = *argv;
if(--argc) {
size_t index = 0;
++argv;
readSpecifiedArgs(m_mainArgs, index, argv, argv + argc);
} else {
// no arguments specified -> set default argument as present
if(m_defaultArg) {
m_defaultArg->m_indices.push_back(0);
m_defaultArg->m_values.emplace_back();
}
}
checkConstraints(m_mainArgs);
invokeCallbacks(m_mainArgs);
} else {
m_currentDirectory = nullptr;
m_executable = nullptr;
}
}
@ -382,13 +410,13 @@ void ApplicationUtilities::ArgumentParser::verifyArgs(const ArgumentVector &args
abbreviations.reserve(args.size());
vector<string> names;
names.reserve(args.size());
bool hasDefault = false;
bool hasImplicit = false;
for(const Argument *arg : args) {
assert(find(verifiedArgs.cbegin(), verifiedArgs.cend(), arg) == verifiedArgs.cend());
verifiedArgs.push_back(arg);
assert(arg->isMainArgument() || !arg->denotesOperation());
assert(!arg->isDefault() || !hasDefault);
hasDefault |= arg->isDefault();
assert(!arg->isImplicit() || !hasImplicit);
hasImplicit |= arg->isImplicit();
assert(!arg->abbreviation() || find(abbreviations.cbegin(), abbreviations.cend(), arg->abbreviation()) == abbreviations.cend());
abbreviations.push_back(arg->abbreviation());
assert(!arg->name() || find(names.cbegin(), names.cend(), arg->name()) == names.cend());
@ -403,7 +431,7 @@ 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 **&argv, const char **end)
void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char **&argv, const char **end, unsigned int level)
{
enum ArgumentDenotationType : unsigned char {
Value = 0, // parameter value
@ -411,7 +439,6 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
FullName = 2 // full argument name
};
bool isTopLevel = index == 0;
Argument *lastArg = nullptr;
vector<const char *> *values = nullptr;
while(argv != end) {
@ -464,7 +491,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
// read sub arguments if no abbreviated argument follows
++index, ++m_actualArgc, lastArg = matchingArg;
if(argDenotationType != Abbreviation || (!*++argDenotation && argDenotation != equationPos)) {
readSpecifiedArgs(matchingArg->m_subArgs, index, ++argv, end);
readSpecifiedArgs(matchingArg->m_subArgs, index, ++argv, end, level + 1);
break;
} // else: another abbreviated argument follows
} else {
@ -481,7 +508,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
continue;
} else {
// first value might denote "operation"
if(isTopLevel) {
if(!level) {
for(Argument *arg : args) {
if(arg->denotesOperation() && arg->name() && !strcmp(arg->name(), *argv)) {
(matchingArg = arg)->m_indices.push_back(index);
@ -494,7 +521,7 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
if(!matchingArg) {
// use the first default argument
for(Argument *arg : args) {
if(arg->isDefault()) {
if(arg->isImplicit()) {
(matchingArg = arg)->m_indices.push_back(index);
break;
}
@ -512,11 +539,11 @@ void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index,
// read sub arguments if no abbreviated argument follows
++m_actualArgc, lastArg = matchingArg;
readSpecifiedArgs(matchingArg->m_subArgs, index, argv, end);
readSpecifiedArgs(matchingArg->m_subArgs, index, argv, end, level + 1);
continue;
}
}
if(isTopLevel) {
if(!level) {
if(m_ignoreUnknownArgs) {
cerr << "The specified argument \"" << *argv << "\" is unknown and will be ignored." << endl;
++index, ++argv;
@ -557,7 +584,7 @@ void ArgumentParser::checkConstraints(const ArgumentVector &args)
throw Failure("The argument \"" + string(conflictingArgument->name()) + "\" can not be combined with \"" + arg->name() + "\".");
}
for(size_t i = 0; i != occurrences; ++i) {
if(!arg->allRequiredValuesPresent(occurrences)) {
if(!arg->allRequiredValuesPresent(i)) {
stringstream ss(stringstream::in | stringstream::out);
ss << "Not all parameter for argument \"" << arg->name() << "\" ";
if(i) {
@ -566,8 +593,7 @@ void ArgumentParser::checkConstraints(const ArgumentVector &args)
ss << "provided. You have to provide the following parameter:";
size_t valueNamesPrint = 0;
for(const auto &name : arg->m_valueNames) {
ss << "\n" << name;
++valueNamesPrint;
ss << ' ' << name, ++valueNamesPrint;
}
if(arg->m_requiredValueCount != static_cast<size_t>(-1)) {
while(valueNamesPrint < arg->m_requiredValueCount) {
@ -596,11 +622,7 @@ void ArgumentParser::invokeCallbacks(const ArgumentVector &args)
// invoke the callback for each occurance of the argument
if(arg->m_callbackFunction) {
for(const auto &valuesOfOccurance : arg->m_values) {
if(arg->isDefault() && valuesOfOccurance.empty()) {
arg->m_callbackFunction(arg->defaultValues());
} else {
arg->m_callbackFunction(valuesOfOccurance);
}
arg->m_callbackFunction(valuesOfOccurance);
}
}
// invoke the callbacks for sub arguments recursively

View File

@ -57,10 +57,6 @@ public:
void setValueNames(std::initializer_list<const char *> valueNames);
void appendValueName(const char *valueName);
bool allRequiredValuesPresent(std::size_t occurrance = 0) const;
bool isDefault() const;
void setDefault(bool isDefault);
const std::vector<const char *> &defaultValues() const;
void setDefaultValues(const std::initializer_list<const char *> &defaultValues);
bool isPresent() const;
std::size_t occurrences() const;
const std::vector<std::size_t> &indices() const;
@ -98,8 +94,7 @@ private:
bool m_denotesOperation;
std::size_t m_requiredValueCount;
std::vector<const char *> m_valueNames;
bool m_default;
std::vector<const char *> m_defaultValues;
bool m_implicit;
std::vector<std::size_t> m_indices;
std::vector<std::vector<const char *> > m_values;
ArgumentVector m_subArgs;
@ -297,62 +292,25 @@ 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_default && m_defaultValues.size() >= static_cast<std::size_t>(m_requiredValueCount));
|| (m_values[occurrance].size() >= static_cast<std::size_t>(m_requiredValueCount));
}
/*!
* \brief Returns an indication whether the argument is a default argument.
*
* A default argument will be flagged as present when parsing arguments even
* if it is not actually present and there is no uncombinable argument present
* and the it is a main argument or the parent is present.
*
* The callback function will be invoked in this case as the argument where
* actually present.
*
* The default value (for this property) is false.
*
* \sa setDefault()
* \brief Returns an indication whether the argument is an implicit argument.
* \sa setImplicit()
*/
inline bool Argument::isDefault() const
inline bool Argument::isImplicit() const
{
return m_default;
return m_implicit;
}
/*!
* \brief Sets whether the argument is a default argument.
* \sa isDefault()
* \brief Sets whether the argument is an implicit argument.
* \sa isImplicit()
*/
inline void Argument::setDefault(bool isDefault)
inline void Argument::setImplicit(bool implicit)
{
m_default = isDefault;
}
/*!
* \brief Returns the default values.
* \sa isDefault()
* \sa setDefault()
* \sa setDefaultValues()
*/
inline const std::vector<const char *> &Argument::defaultValues() const
{
return m_defaultValues;
}
/*!
* \brief Sets the default values.
*
* There must be as many default values as required values
* if the argument is a default argument.
*
* \sa isDefault()
* \sa setDefault()
* \sa defaultValues()
*/
inline void Argument::setDefaultValues(const std::initializer_list<const char *> &defaultValues)
{
m_defaultValues = defaultValues;
m_implicit = implicit;
}
/*!
@ -564,20 +522,23 @@ public:
void parseArgs(int argc, char *argv[]);
void parseArgs(int argc, const char *argv[]);
unsigned int actualArgumentCount() const;
const char *currentDirectory() const;
const char *executable() const;
bool areUnknownArgumentsIgnored() const;
void setIgnoreUnknownArguments(bool ignore);
Argument *defaultArgument() const;
void setDefaultArgument(Argument *argument);
private:
IF_DEBUG_BUILD(void verifyArgs(const ArgumentVector &args);)
void readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char **&argv, const char **end);
void readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char **&argv, const char **end, unsigned int level = 0);
void checkConstraints(const ArgumentVector &args);
void invokeCallbacks(const ArgumentVector &args);
ArgumentVector m_mainArgs;
unsigned int m_actualArgc;
const char *m_currentDirectory;
const char *m_executable;
bool m_ignoreUnknownArgs;
Argument *m_defaultArg;
};
/*!
@ -606,11 +567,11 @@ inline unsigned int ArgumentParser::actualArgumentCount() const
}
/*!
* \brief Returns the current directory.
* \brief Returns the name of the current executable.
*/
inline const char *ArgumentParser::currentDirectory() const
inline const char *ArgumentParser::executable() const
{
return m_currentDirectory;
return m_executable;
}
/*!
@ -645,6 +606,24 @@ inline void ArgumentParser::setIgnoreUnknownArguments(bool ignore)
m_ignoreUnknownArgs = ignore;
}
/*!
* \brief Returns the default argument.
* \remarks The default argument is assumed to be present if no other arguments have been specified.
*/
inline Argument *ArgumentParser::defaultArgument() const
{
return m_defaultArg;
}
/*!
* \brief Sets the default argument.
* \remarks The default argument is assumed to be present if no other arguments have been specified.
*/
inline void ArgumentParser::setDefaultArgument(Argument *argument)
{
m_defaultArg = argument;
}
class LIB_EXPORT HelpArgument : public Argument
{
public:

View File

@ -68,9 +68,8 @@ void ArgumentParserTests::testArgument()
*/
void ArgumentParserTests::testParsing()
{
// setup parser with some test argument definitions
ArgumentParser parser;
// add some test argument definitions
SET_APPLICATION_INFO;
QT_CONFIG_ARGUMENTS qtConfigArgs;
HelpArgument helpArg(parser);
@ -94,7 +93,7 @@ void ArgumentParserTests::testParsing()
Argument fieldsArg("fields", '\0', "specifies the fields");
fieldsArg.setRequiredValueCount(-1);
fieldsArg.setValueNames({"title", "album", "artist", "trackpos"});
fieldsArg.setDefault(true);
fieldsArg.setImplicit(true);
Argument displayTagInfoArg("get", 'p', "displays the values of all specified tag fields (displays all fields if none specified)");
displayTagInfoArg.setDenotesOperation(true);
displayTagInfoArg.setSubArguments({&fieldsArg, &filesArg, &verboseArg});
@ -115,8 +114,9 @@ void ArgumentParserTests::testParsing()
filesArg.setCombinable(true);
parser.parseArgs(7, argv);
// check results
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
CPPUNIT_ASSERT(!strcmp(parser.currentDirectory(), "tageditor"));
CPPUNIT_ASSERT(!strcmp(parser.executable(), "tageditor"));
CPPUNIT_ASSERT(!verboseArg.isPresent());
CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
CPPUNIT_ASSERT(fieldsArg.isPresent());
@ -131,6 +131,7 @@ void ArgumentParserTests::testParsing()
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
parser.parseArgs(7, argv2);
// check results again
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
CPPUNIT_ASSERT(!verboseArg.isPresent());
CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
@ -157,8 +158,23 @@ void ArgumentParserTests::testParsing()
// repeat the test, but this time just ignore the undefined argument
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
parser.setIgnoreUnknownArguments(true);
parser.parseArgs(6, argv3);
// redirect stderr to check whether warnings are printed correctly
stringstream buffer;
streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf());
try {
parser.parseArgs(6, argv3);
cerr.rdbuf(regularCerrBuffer);
} catch(...) {
cerr.rdbuf(regularCerrBuffer);
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"));
// none of the arguments should be present now
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
CPPUNIT_ASSERT(!fieldsArg.isPresent());
@ -169,6 +185,7 @@ void ArgumentParserTests::testParsing()
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
parser.setIgnoreUnknownArguments(false);
parser.parseArgs(4, argv4);
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(displayFileInfoArg.isPresent());
CPPUNIT_ASSERT(verboseArg.isPresent());
CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
@ -183,6 +200,7 @@ void ArgumentParserTests::testParsing()
parser.parseArgs(4, argv4);
CPPUNIT_FAIL("Exception expected.");
} catch(const Failure &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" mustn't be specified more than 1 time."));
}
@ -190,11 +208,13 @@ void ArgumentParserTests::testParsing()
displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset();
verboseArg.setConstraints(0, -1);
parser.parseArgs(4, argv4);
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
// make verbose mandatory
verboseArg.setRequired(true);
displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset();
parser.parseArgs(4, argv4);
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
// make it complain about missing argument
const char *argv5[] = {"tageditor", "-i", "-f", "test"};
@ -203,6 +223,7 @@ void ArgumentParserTests::testParsing()
parser.parseArgs(4, argv5);
CPPUNIT_FAIL("Exception expected.");
} catch(const Failure &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" must be specified at least 1 time."));
}
@ -210,16 +231,59 @@ void ArgumentParserTests::testParsing()
const char *argv6[] = {"tageditor", "-g"};
displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset();
parser.parseArgs(2, argv6);
CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent());
// it should not be possible to specify -f without -i or -p
const char *argv7[] = {"tageditor", "-f", "test"};
displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset();
displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(), qtConfigArgs.qtWidgetsGuiArg().reset();
try {
parser.parseArgs(3, argv7);
CPPUNIT_FAIL("Exception expected.");
} catch(const Failure &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!strcmp(e.what(), "The specified argument \"-f\" is unknown and will be ignored."));
}
// test default argument
const char *argv8[] = {"tageditor"};
displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset();
parser.parseArgs(1, argv8);
CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
CPPUNIT_ASSERT(!verboseArg.isPresent());
CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
CPPUNIT_ASSERT(!filesArg.isPresent());
CPPUNIT_ASSERT(!fileArg.isPresent());
// test required value count constraint with sufficient number of provided parameters
qtConfigArgs.qtWidgetsGuiArg().reset();
fieldsArg.setRequiredValueCount(3);
verboseArg.setRequired(false);
parser.parseArgs(7, argv2);
// this should still work without complaints
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
CPPUNIT_ASSERT(!verboseArg.isPresent());
CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
CPPUNIT_ASSERT(fieldsArg.isPresent());
CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album"));
CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title"));
CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos"));
CPPUNIT_ASSERT_THROW(displayTagInfoArg.values().at(3), out_of_range);
CPPUNIT_ASSERT(filesArg.isPresent());
CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
// test required value count constraint with insufficient number of provided parameters
displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset();
fieldsArg.setRequiredValueCount(4);
const char *argv9[] = {"tageditor", "-p", "album", "title", "diskpos"};
try {
parser.parseArgs(5, argv9);
CPPUNIT_FAIL("Exception expected.");
} catch(const Failure &e) {
CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
CPPUNIT_ASSERT(!strcmp(e.what(), "Not all parameter for argument \"fields\" provided. You have to provide the following parameter: title album artist trackpos"));
}
}
void ArgumentParserTests::testCallbacks()
@ -249,5 +313,4 @@ void ArgumentParserTests::testCallbacks()
callbackArg.reset();
const char *argv2[] = {"test", "-l", "val1", "val2"};
parser.parseArgs(4, argv2);
}