diff --git a/CMakeLists.txt b/CMakeLists.txt index 530180d..49c3686 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,6 +58,7 @@ set(TEST_SRC_FILES tests/conversiontests.cpp tests/iotests.cpp tests/chronotests.cpp + tests/argumentparsertests.cpp ) set(CMAKE_MODULE_FILES diff --git a/README.md b/README.md index d904c46..89f52a2 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ make DESTDIR="/temporary/install/location" install Building for Windows with Mingw-w64 cross compiler can be utilized using a small [cmake wrapper from Fedora](https://aur.archlinux.org/cgit/aur.git/tree/mingw-cmake.sh?h=mingw-w64-cmake): ``` -${_arch}-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="/final/install/location" "path/to/projectdirectory" +${_arch}-cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="/final/install/location" "path/to/source/directory" make DESTDIR="/temporary/install/location" install-mingw-w64-strip ``` * To create the \*.ico file for the application icon ffmpeg/avconv is required. @@ -72,5 +72,4 @@ PKGBUILD files to build for Windows using the Mingw-w64 compiler are also includ is currently disabled. Linking against cppunit built using new libstdc++ ABI isn't possible. ## TODO -- rewrite argument parser (the API might change slightly) - remove unused features diff --git a/application/argumentparser.cpp b/application/argumentparser.cpp index 26ea765..8f8d150 100644 --- a/application/argumentparser.cpp +++ b/application/argumentparser.cpp @@ -6,20 +6,23 @@ #include "../misc/random.h" #include -#include #include +#include #include -#include +#include using namespace std; using namespace std::placeholders; +using namespace ConversionUtilities; /*! - \namespace ApplicationUtilities - \brief Contains currently only ArgumentParser and related classes. -*/ + * \namespace ApplicationUtilities + * \brief Contains currently only ArgumentParser and related classes. + */ namespace ApplicationUtilities { +/// \cond + const char *applicationName = nullptr; const char *applicationAuthor = nullptr; const char *applicationVersion = nullptr; @@ -30,6 +33,8 @@ inline bool notEmpty(const char *str) return str && *str; } +/// \endcond + /*! * \class ApplicationUtilities::Argument * \brief The Argument class is a wrapper for command line argument information. @@ -46,18 +51,17 @@ inline bool notEmpty(const char *str) * The \a name and the abbreviation mustn't contain any whitespaces. * The \a name mustn't be empty. The \a abbreviation and the \a description might be empty. */ -Argument::Argument(const char *name, const char *abbreviation, const char *description, const char *example) : +Argument::Argument(const char *name, char abbreviation, const char *description, const char *example) : m_name(name), m_abbreviation(abbreviation), m_description(description), m_example(example), - m_required(false), + m_minOccurrences(0), + m_maxOccurrences(1), m_combinable(false), - m_implicit(false), m_denotesOperation(false), m_requiredValueCount(0), m_default(false), - m_present(false), m_isMainArg(false) {} @@ -74,42 +78,41 @@ void Argument::printInfo(ostream &os, unsigned char indentionLevel) const { for(unsigned char i = 0; i < indentionLevel; ++i) os << " "; if(notEmpty(name())) { - os << "--" << name(); + os << '-' << '-' << name(); } - if(notEmpty(name()) && notEmpty(abbreviation())) { - os << ", "; + if(notEmpty(name()) && abbreviation()) { + os << ',' << ' '; } - if(notEmpty(abbreviation())) { - os << "-" << abbreviation(); + if(abbreviation()) { + os << '-' << abbreviation(); } - if(requiredValueCount() > 0) { - int valueNamesPrint = 0; + if(requiredValueCount() != 0) { + unsigned int valueNamesPrint = 0; for(auto i = valueNames().cbegin(), end = valueNames().cend(); i != end && valueNamesPrint < requiredValueCount(); ++i) { - os << " [" << *i << "]"; + os << ' ' << '[' << *i << ']'; ++valueNamesPrint; } - for(; valueNamesPrint < requiredValueCount(); ++valueNamesPrint) { - os << " [value " << (valueNamesPrint + 1) << "]"; + if(requiredValueCount() == static_cast(-1)) { + os << " ..."; + } else { + for(; valueNamesPrint < requiredValueCount(); ++valueNamesPrint) { + os << " [value " << (valueNamesPrint + 1) << ']'; + } } - } else if(requiredValueCount() < 0) { - for(auto i = valueNames().cbegin(), end = valueNames().cend(); i != end; ++i) { - os << " [" << *i << "]"; - } - os << " ..."; } ++indentionLevel; if(notEmpty(description())) { os << endl; - for(unsigned char i = 0; i < indentionLevel; ++i) os << " "; + for(unsigned char i = 0; i < indentionLevel; ++i) os << ' ' << ' '; os << description(); } if(isRequired()) { os << endl; - for(unsigned char i = 0; i < indentionLevel; ++i) os << " "; + for(unsigned char i = 0; i < indentionLevel; ++i) os << ' ' << ' '; os << "This argument is required."; } if(notEmpty(example())) { - for(unsigned char i = 0; i < indentionLevel; ++i) os << " "; + for(unsigned char i = 0; i < indentionLevel; ++i) os << ' ' << ' '; os << endl << "Usage: " << example(); } os << endl; @@ -182,31 +185,15 @@ void Argument::addSubArgument(Argument *arg) } /*! - * \brief Returns the names of the parents in the form "parent1", "parent2, "parent3", ... - * Returns an empty string if this Argument has no parents. - * \sa parents() - */ -string Argument::parentNames() const -{ - string res; - if(m_parents.size()) { - vector names; - names.reserve(m_parents.size()); - for(const Argument *parent : m_parents) { - names.push_back(parent->name()); - } - res.assign(ConversionUtilities::joinStrings(names, ", ")); - } - return res; -} - -/*! - * \brief Returns true if at least one of the parents is present. - * Returns false if this argument has no parents or none of its parents is present. + * \brief Returns whether at least one parent argument is present. + * \remarks Returns always true for main arguments. */ bool Argument::isParentPresent() const { - for(Argument *parent : m_parents) { + if(isMainArgument()) { + return true; + } + for(const Argument *parent : m_parents) { if(parent->isPresent()) { return true; } @@ -234,6 +221,15 @@ 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. @@ -252,6 +248,7 @@ Argument *Argument::conflictsWithArgument() const */ ArgumentParser::ArgumentParser() : m_actualArgc(0), + m_currentDirectory(nullptr), m_ignoreUnknownArgs(false) {} @@ -338,293 +335,277 @@ Argument *ArgumentParser::findArg(const ArgumentVector &arguments, const Argumen return nullptr; // no argument matches } +/*! + * \brief Parses the specified command line arguments. + * \remarks + * - The results are stored in the Argument instances assigned as main arguments and sub arguments. + * - Calls the assigned callbacks if no constraints are violated. + * \throws Throws Failure if the specified arguments violate the constraints defined + * by the Argument instances. + */ +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); + checkConstraints(m_mainArgs); + invokeCallbacks(m_mainArgs); + } else { + m_currentDirectory = nullptr; + } +} + #ifdef DEBUG_BUILD /*! - * \brief This method is used to verify the setup of the command line parser before parsing. + * \brief Verifies the specified \a argument definitions. * - * This function will throw std::invalid_argument when a mismatch is detected: - * - An argument is used as main argument and as sub argument at the same time. - * - An argument abbreviation is used more then once. - * - An argument name is used more then once. - * If none of these conditions are met, this method will do nothing. + * Asserts that + * - The same argument has not been added twice to the same parent. + * - Only one argument within a parent is default or implicit. + * - Only main arguments denote operations. + * - Argument abbreviations are unique within one parent. + * - Argument names are unique within one parent. * - * \remarks Usually you don't need to call this function manually because it is called by - * parse() automatically befor the parsing is performed. + * \remarks + * - Verifies the sub arguments, too. + * - For debugging purposes only; hence only available in debug builds. */ -void ArgumentParser::verifySetup() const +void ApplicationUtilities::ArgumentParser::verifyArgs(const ArgumentVector &args) { - vector verifiedArgs; - vector abbreviations; + vector verifiedArgs; + verifiedArgs.reserve(args.size()); + vector abbreviations; + abbreviations.reserve(args.size()); vector names; - const Argument *implicitArg = nullptr; - function checkArguments; - checkArguments = [&verifiedArgs, &abbreviations, &names, &checkArguments, &implicitArg, this] (const ArgumentVector &args) { - for(Argument *arg : args) { - if(find(verifiedArgs.cbegin(), verifiedArgs.cend(), arg) != verifiedArgs.cend()) { - continue; // do not verify the same argument twice - } - if(arg->isMainArgument() && arg->parents().size()) { - throw invalid_argument("Argument \"" + string(arg->name()) + "\" can not be used as main argument and sub argument at the same time."); - } - if(notEmpty(arg->abbreviation()) && find(abbreviations.cbegin(), abbreviations.cend(), arg->abbreviation()) != abbreviations.cend()) { - throw invalid_argument("Abbreviation \"" + string(arg->abbreviation()) + "\" has been used more then once."); - } - if(find(names.cbegin(), names.cend(), arg->name()) != names.cend()) { - throw invalid_argument("Name \"" + string(arg->name()) + "\" has been used more then once."); - } - if(arg->isDefault() && arg->requiredValueCount() > 0 && arg->defaultValues().size() < static_cast(arg->requiredValueCount())) { - throw invalid_argument("Default argument \"" + string(arg->name()) + "\" doesn't provide the required number of default values."); - } - if(arg->isImplicit()) { - if(implicitArg) { - throw invalid_argument("The arguments \"" + string(implicitArg->name()) + "\" and \"" + string(arg->name()) + "\" can not be both implicit."); - } else { - implicitArg = arg; - } - } - if(arg->abbreviation()) { - abbreviations.push_back(arg->abbreviation()); - } - if(arg->name()) { - names.push_back(arg->name()); - } - verifiedArgs.push_back(arg); - checkArguments(arg->subArguments()); - } - }; - checkArguments(m_mainArgs); + names.reserve(args.size()); + bool hasDefault = 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->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()); + assert(arg->requiredValueCount() == 0 || arg->subArguments().size() == 0); + names.emplace_back(arg->name()); + verifyArgs(arg->subArguments()); + } } #endif /*! - * \brief This method invokes verifySetup() before parsing. See its do documentation for more - * information about execptions that might be thrown to indicate an invalid setup of the parser. - * - * If the parser is setup properly this method will parse the given command line arguments using - * the previsously set argument definitions. - * If one of the previsously defined arguments has been found, its present flag will be set to true - * and the parser reads all values tied to this argument. - * If an argument has been found which does not match any of the previous definitions it will be - * considered as unknown. - * If the given command line arguments are not valid according the defined arguments an - * Failure will be thrown. + * \brief Reads the specified commands line arguments. + * \remarks Results are stored in Argument instances added as main arguments and sub arguments. */ -void ArgumentParser::parseArgs(int argc, char *argv[]) +void ArgumentParser::readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char **&argv, const char **end) { - // initiate parser - IF_DEBUG_BUILD(verifySetup();) - m_actualArgc = 0; // reset actual agument count - unsigned int actualArgc = 0; - int valuesToRead = 0; - // read current directory - if(argc >= 1) { - m_currentDirectory = string(*argv); - } else { - m_currentDirectory.clear(); - } - // function for iterating through all arguments - function &)> foreachArg; - foreachArg = [&foreachArg] (Argument *parent, const ArgumentVector &args, const function &proc) { - for(Argument *arg : args) { - proc(parent, arg); - foreachArg(arg, arg->subArguments(), proc); - } + enum ArgumentDenotationType : unsigned char { + Value = 0, // parameter value + Abbreviation = 1, // argument abbreviation + FullName = 2 // full argument name }; - // parse given arguments - if(argc >= 2) { - Argument *currentArg = nullptr; - // iterate through given arguments - for(char **i = argv + 1, **end = argv + argc; i != end; ++i) { - string givenArg(*i); // convert argument to string - if(!givenArg.empty()) { // skip empty entries - if(valuesToRead <= 0 && givenArg.size() > 1 && givenArg.front() == '-') { - // we have no values left to read and the given arguments starts with '-' - // -> the next "value" is a main argument or a sub argument - ArgumentPredicate pred; - string givenId; - size_t equationPos = givenArg.find('='); - if(givenArg.size() > 2 && givenArg[1] == '-') { - // the argument starts with '--' - // -> the full argument name has been provided - givenId = givenArg.substr(2, equationPos - 2); - pred = [&givenId, equationPos] (Argument *arg) { - return arg->name() && arg->name() == givenId; - }; - } else { - // the argument starts with a single '-' - // -> the abbreviation has been provided - givenId = givenArg.substr(1, equationPos - 1); - pred = [&givenId, equationPos] (Argument *arg) { - return arg->abbreviation() && arg->abbreviation() == givenId; - }; - } - // find the corresponding instance of the Argument class - currentArg = findArg(pred); - if(currentArg) { - // the corresponding instance of Argument class has been found - if(currentArg->m_present) { - // the argument has been provided more then once - throw Failure("The argument \"" + string(currentArg->name()) + "\" has been specified more than one time."); - } else { - // set present flag of argument - currentArg->m_present = true; - ++actualArgc; // we actually found an argument - // now we might need to read values tied to that argument - valuesToRead = currentArg->requiredValueCount(); - if(equationPos != string::npos) { - // a value has been specified using the --argument=value syntax - string value = givenArg.substr(equationPos + 1); - if(valuesToRead != 0) { - currentArg->m_values.push_back(value); - if(valuesToRead > 0) { - --valuesToRead; - } - } else { - throw Failure("Invalid extra information \"" + value + "\" (specified using \"--argument=value\" syntax) for the argument \"" + currentArg->name() + "\" given."); - } + + bool isTopLevel = index == 0; + Argument *lastArg = nullptr; + vector *values = nullptr; + while(argv != end) { + if(values && lastArg->requiredValueCount() != static_cast(-1) && values->size() < lastArg->requiredValueCount()) { + // there are still values to read + values->emplace_back(*argv); + ++index, ++argv; + } else { + // determine denotation type + const char *argDenotation = *argv; + bool abbreviationFound = false; + unsigned char argDenotationType = Value; + *argDenotation == '-' && (++argDenotation, ++argDenotationType) + && *argDenotation == '-' && (++argDenotation, ++argDenotationType); + + // try to find matching Argument instance + Argument *matchingArg = nullptr; + if(argDenotationType != Value) { + const char *const equationPos = strchr(argDenotation, '='); + for(const auto argDenLen = equationPos ? static_cast(equationPos - argDenotation) : strlen(argDenotation); ; matchingArg = nullptr) { + // search for arguments by abbreviation or name depending on the denotation type + if(argDenotationType == Abbreviation) { + for(Argument *arg : args) { + if(arg->abbreviation() && arg->abbreviation() == *argDenotation) { + matchingArg = arg; + abbreviationFound = true; + break; } } } else { - // the given argument seems to be unknown - if(valuesToRead < 0) { - // we have a variable number of values to expect -> treat "unknown argument" as value - goto readValue; - } else { - // we have no more values to expect so we need to complain about the unknown argument - goto invalidArg; + for(Argument *arg : args) { + if(arg->name() && !strncmp(arg->name(), argDenotation, argDenLen)) { + matchingArg = arg; + break; + } } } + + if(matchingArg) { + // an argument matched the specified denotation + matchingArg->m_indices.push_back(index); + + // prepare reading parameter values + matchingArg->m_values.emplace_back(); + values = &matchingArg->m_values.back(); + if(equationPos) { + values->push_back(equationPos + 1); + } + + // 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); + break; + } // else: another abbreviated argument follows + } else { + break; + } + } + } + + if(!matchingArg) { + if(lastArg && values->size() < lastArg->requiredValueCount()) { + // treat unknown argument as parameter of last argument + values->emplace_back(abbreviationFound ? argDenotation : *argv); + ++index, ++argv; + continue; + } else { + // first value might denote "operation" + if(isTopLevel) { + for(Argument *arg : args) { + if(arg->denotesOperation() && arg->name() && !strcmp(arg->name(), *argv)) { + (matchingArg = arg)->m_indices.push_back(index); + ++index, ++argv; + break; + } + } + } + + if(!matchingArg) { + // use the first default argument + for(Argument *arg : args) { + if(arg->isDefault()) { + (matchingArg = arg)->m_indices.push_back(index); + break; + } + } + } + if(matchingArg) { + // an argument matched the specified denotation + if(lastArg == matchingArg) { + break; + } + + // prepare reading parameter values + matchingArg->m_values.emplace_back(); + values = &matchingArg->m_values.back(); + + // read sub arguments if no abbreviated argument follows + ++m_actualArgc, lastArg = matchingArg; + readSpecifiedArgs(matchingArg->m_subArgs, index, argv, end); + continue; + } + } + if(isTopLevel) { + if(m_ignoreUnknownArgs) { + cerr << "The specified argument \"" << *argv << "\" is unknown and will be ignored." << endl; + ++index, ++argv; + } else { + throw Failure("The specified argument \"" + string(*argv) + "\" is unknown and will be ignored."); + } } else { - readValue: - if(!currentArg) { - // we have not parsed an argument before - // -> check if an argument which denotes the operation is specified - if(i == argv + 1) { - for(Argument *arg : m_mainArgs) { - if(!arg->isPresent() && arg->denotesOperation() - && ((arg->name() && arg->name() == givenArg) || (arg->abbreviation() && arg->abbreviation() == givenArg))) { - currentArg = arg; - break; - } - } - if(currentArg) { - currentArg->m_present = true; - ++actualArgc; // we actually found an argument - // now we might need to read values tied to that argument - valuesToRead = currentArg->requiredValueCount(); - continue; - } - } - // -> check if there's an implicit argument definition - try { - foreachArg(nullptr, m_mainArgs, [&actualArgc, &valuesToRead, ¤tArg, this] (Argument *, Argument *arg) { - if(!arg->isPresent() && arg->isImplicit()) { - throw arg; - } - }); - } catch(Argument *arg) { - // set present flag of argument - arg->m_present = true; - ++actualArgc; // we actually found an argument - // now we might need to read values tied to that argument - valuesToRead = arg->requiredValueCount(); - currentArg = arg; - } - } - if(currentArg) { - // check if the given value is tied to the most recently parsed argument - if(valuesToRead == 0) { - throw Failure("Invalid extra information \"" + givenArg + "\" for the argument \"" + currentArg->name() + "\" given."); - } else if(valuesToRead < 0) { - currentArg->m_values.emplace_back(givenArg); - } else { - currentArg->m_values.emplace_back(givenArg); - --valuesToRead; // one value less to be read - } - } else { - // there is no implicit argument definition -> the "value" has to be an argument - // but does not start with '-' or '--' - invalidArg: - string msg("The given argument \"" + givenArg + "\" is unknown."); - if(m_ignoreUnknownArgs) { - cout << msg << " It will be ignored." << endl; - } else { - throw Failure(msg); - } - } + return; // unknown argument name or abbreviation found -> continue with parent level } } } } - // iterate actually through all arguments using previously defined function to check gathered arguments - foreachArg(nullptr, m_mainArgs, [this] (Argument *parent, Argument *arg) { - if(!arg->isPresent()) { - // the argument might be flagged as present if its a default argument - if(arg->isDefault() && (arg->isMainArgument() || (parent && parent->isPresent()))) { - arg->m_present = true; - if(firstPresentUncombinableArg(arg->isMainArgument() ? m_mainArgs : parent->subArguments(), arg)) { - arg->m_present = false; - } - } - // throw an error if mandatory argument is not present - if(!arg->isPresent() && (arg->isRequired() && (arg->isMainArgument() || (parent && parent->isPresent())))) { - throw Failure("The argument \"" + string(arg->name()) + "\" is required but not given."); - } +} + +/*! + * \brief Checks the constrains of the specified \a args. + * \remarks Checks the contraints of sub arguments, too. + */ +void ArgumentParser::checkConstraints(const ArgumentVector &args) +{ + for(const Argument *arg : args) { + const auto occurrences = arg->occurrences(); + if(arg->isParentPresent() && occurrences > arg->maxOccurrences()) { + throw Failure("The argument \"" + string(arg->name()) + "\" mustn't be specified more than " + numberToString(arg->maxOccurrences()) + (arg->maxOccurrences() == 1 ? " time." : " times.")); } - }); - foreachArg(nullptr, m_mainArgs, [this] (Argument *, Argument *arg) { - if(arg->isPresent()) { - if(!arg->isMainArgument() && arg->parents().size() && !arg->isParentPresent()) { - if(arg->parents().size() > 1) { - throw Failure("The argument \"" + string(arg->name()) + "\" needs to be used together with one the following arguments: " + arg->parentNames()); - } else { - throw Failure("The argument \"" + string(arg->name()) + "\" needs to be used together with the argument \"" + arg->parents().front()->name() + "\"."); - } + if(arg->isParentPresent() && occurrences < arg->minOccurrences()) { + throw Failure("The argument \"" + string(arg->name()) + "\" must be specified at least " + numberToString(arg->minOccurrences()) + (arg->minOccurrences() == 1 ? " time." : " times.")); + } + Argument *conflictingArgument = nullptr; + if(arg->isMainArgument()) { + if(!arg->isCombinable() && arg->isPresent()) { + conflictingArgument = firstPresentUncombinableArg(m_mainArgs, arg); } - Argument *conflictingArgument = nullptr; - if(arg->isMainArgument()) { - if(!arg->isCombinable() && arg->isPresent()) { - conflictingArgument = firstPresentUncombinableArg(m_mainArgs, arg); - } - } else { - conflictingArgument = arg->conflictsWithArgument(); - } - if(conflictingArgument) { - throw Failure("The argument \"" + string(conflictingArgument->name()) + "\" can not be combined with \"" + arg->name() + "\"."); - } - if(!arg->allRequiredValuesPresent()) { + } else { + conflictingArgument = arg->conflictsWithArgument(); + } + if(conflictingArgument) { + 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)) { stringstream ss(stringstream::in | stringstream::out); - ss << "Not all required information for the given argument \"" << string(arg->name()) << "\" provided. You have to give the following information:"; - int valueNamesPrint = 0; + ss << "Not all parameter for argument \"" << arg->name() << "\" "; + if(i) { + ss << " (" << (i + 1) << " occurrence) "; + } + ss << "provided. You have to provide the following parameter:"; + size_t valueNamesPrint = 0; for(const auto &name : arg->m_valueNames) { ss << "\n" << name; ++valueNamesPrint; } - while(valueNamesPrint < arg->m_requiredValueCount) { - ss << "\nvalue " << (++valueNamesPrint); + if(arg->m_requiredValueCount != static_cast(-1)) { + while(valueNamesPrint < arg->m_requiredValueCount) { + ss << "\nvalue " << (++valueNamesPrint); + } } throw Failure(ss.str()); } } - }); - // set actual argument count - m_actualArgc = actualArgc; - // interate through all arguments again to invoke callback functions - foreachArg(nullptr, m_mainArgs, [] (Argument *parent, Argument *arg) { + + // check contraints of sub arguments recursively + checkConstraints(arg->m_subArgs); + } +} + +/*! + * \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 + * the argument. + */ +void ArgumentParser::invokeCallbacks(const ArgumentVector &args) +{ + for(const Argument *arg : args) { + // invoke the callback for each occurance of the argument if(arg->m_callbackFunction) { - if(arg->isMainArgument() || (parent && parent->isPresent())) { - // only invoke if its a main argument or the parent is present - if(arg->isPresent()) { - if(arg->isDefault() && !arg->values().size()) { - vector defaultValues(arg->defaultValues().cbegin(), arg->defaultValues().cend()); - arg->m_callbackFunction(defaultValues); - } else { - arg->m_callbackFunction(arg->values()); - } + for(const auto &valuesOfOccurance : arg->m_values) { + if(arg->isDefault() && valuesOfOccurance.empty()) { + arg->m_callbackFunction(arg->defaultValues()); + } else { + arg->m_callbackFunction(valuesOfOccurance); } } } - }); + // invoke the callbacks for sub arguments recursively + invokeCallbacks(arg->m_subArgs); + } } /*! @@ -637,9 +618,9 @@ void ArgumentParser::parseArgs(int argc, char *argv[]) * \brief Constructs a new help argument for the specified parser. */ HelpArgument::HelpArgument(ArgumentParser &parser) : - Argument("help", "h", "shows this information") + Argument("help", 'h', "shows this information") { - setCallback([&parser] (const std::vector &) { + setCallback([&parser] (const std::vector &) { CMD_UTILS_START_CONSOLE; parser.printHelp(cout); }); diff --git a/application/argumentparser.h b/application/argumentparser.h index 250e2b6..0eda0a2 100644 --- a/application/argumentparser.h +++ b/application/argumentparser.h @@ -3,12 +3,12 @@ #include "./global.h" -#include #include -#include #include #include -#include +#ifdef DEBUG_BUILD +# include +#endif namespace ApplicationUtilities { @@ -37,35 +37,38 @@ class LIB_EXPORT Argument friend class ArgumentParser; public: - typedef std::function &)> CallbackFunction; + typedef std::function &)> CallbackFunction; - Argument(const char *name, const char *abbreviation = nullptr, const char *description = nullptr, const char *example = nullptr); + Argument(const char *name, char abbreviation = '\0', const char *description = nullptr, const char *example = nullptr); ~Argument(); const char *name() const; void setName(const char *name); - const char *abbreviation() const; - void setAbbreviation(const char *abbreviation); + char abbreviation() const; + void setAbbreviation(char abbreviation); const char *description() const; void setDescription(const char *description); const char *example() const; void setExample(const char *example); - const std::vector &values() const; - const std::string &value(std::size_t index) const; - std::size_t valueCount() const; - int requiredValueCount() const; - void setRequiredValueCount(int requiredValueCount); - const std::list &valueNames() const; + const std::vector &values(std::size_t occurrance = 0) 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() const; + bool allRequiredValuesPresent(std::size_t occurrance = 0) const; bool isDefault() const; - void setDefault(bool value); - const std::list &defaultValues() const; - void setDefaultValues(const std::list &defaultValues); + void setDefault(bool isDefault); + const std::vector &defaultValues() const; + void setDefaultValues(const std::initializer_list &defaultValues); bool isPresent() const; + std::size_t occurrences() const; + const std::vector &indices() const; + std::size_t minOccurrences() const; + std::size_t maxOccurrences() const; + void setConstraints(std::size_t minOccurrences, std::size_t maxOccurrences); bool isRequired() const; - void setRequired(bool value); + void setRequired(bool required); bool isCombinable() const; void setCombinable(bool value); bool isImplicit() const; @@ -80,25 +83,25 @@ public: bool hasSubArguments() const; const ArgumentVector parents() const; bool isMainArgument() const; - std::string parentNames() const; bool isParentPresent() const; Argument *conflictsWithArgument() const; + void reset(); private: const char *m_name; - const char *m_abbreviation; + char m_abbreviation; const char *m_description; const char *m_example; - bool m_required; + std::size_t m_minOccurrences; + std::size_t m_maxOccurrences; bool m_combinable; - bool m_implicit; bool m_denotesOperation; - int m_requiredValueCount; - std::list m_valueNames; + std::size_t m_requiredValueCount; + std::vector m_valueNames; bool m_default; - std::list m_defaultValues; - bool m_present; - std::vector m_values; + std::vector m_defaultValues; + std::vector m_indices; + std::vector > m_values; ArgumentVector m_subArgs; CallbackFunction m_callbackFunction; ArgumentVector m_parents; @@ -129,12 +132,7 @@ inline void Argument::setName(const char *name) #ifdef DEBUG_BUILD if(name && *name) { for(const char *c = name; *c; ++c) { - switch(*c) { - case ' ': case '=': - throw std::invalid_argument("name mustn't contain white spaces or equation chars"); - default: - ; - } + assert(*c != ' ' && *c != '='); } } #endif @@ -147,7 +145,7 @@ inline void Argument::setName(const char *name) * The parser compares the abbreviation with the characters following a "-" prefix to * identify arguments. */ -inline const char *Argument::abbreviation() const +inline char Argument::abbreviation() const { return m_abbreviation; } @@ -161,20 +159,9 @@ inline const char *Argument::abbreviation() const * The parser compares the abbreviation with the characters following a "-" prefix to * identify arguments. */ -inline void Argument::setAbbreviation(const char *abbreviation) +inline void Argument::setAbbreviation(char abbreviation) { -#ifdef DEBUG_BUILD - if(abbreviation && *abbreviation) { - for(const char *c = abbreviation; *c; ++c) { - switch(*c) { - case ' ': case '=': - throw std::invalid_argument("abbreviation mustn't contain white spaces or equation chars"); - default: - ; - } - } - } -#endif + IF_DEBUG_BUILD(assert(abbreviation != ' ' && abbreviation != '=')); m_abbreviation = abbreviation; } @@ -223,28 +210,9 @@ inline void Argument::setExample(const char *example) * * These values set by the parser when parsing the command line arguments. */ -inline const std::vector &Argument::values() const +inline const std::vector &Argument::values(std::size_t occurrance) const { - return m_values; -} - -/*! - * \brief Returns the value with the give \a index. - * - * These values set by the parser when parsing the command line arguments. - */ -inline const std::string &Argument::value(std::size_t index) const -{ - return m_values[index]; -} - -/*! - * Returns the number of values which could be found when parsing - * the command line arguments. - */ -inline std::size_t Argument::valueCount() const -{ - return m_values.size(); + return m_values[occurrance]; } /*! @@ -260,7 +228,7 @@ inline std::size_t Argument::valueCount() const * \sa valueNames() * \sa setValueNames() */ -inline int Argument::requiredValueCount() const +inline std::size_t Argument::requiredValueCount() const { return m_requiredValueCount; } @@ -276,7 +244,7 @@ inline int Argument::requiredValueCount() const * \sa valueNames() * \sa setValueNames() */ -inline void Argument::setRequiredValueCount(int requiredValueCount) +inline void Argument::setRequiredValueCount(std::size_t requiredValueCount) { m_requiredValueCount = requiredValueCount; } @@ -289,7 +257,7 @@ inline void Argument::setRequiredValueCount(int requiredValueCount) * \sa setValueNames() * \sa appendValueNames() */ -inline const std::list &Argument::valueNames() const +inline const std::vector &Argument::valueNames() const { return m_valueNames; } @@ -326,14 +294,11 @@ inline void Argument::appendValueName(const char *valueName) /*! * \brief Returns an indication whether all required values are present. */ -inline bool Argument::allRequiredValuesPresent() const +inline bool Argument::allRequiredValuesPresent(std::size_t occurrance) const { - if(m_requiredValueCount > 0) { - return (m_values.size() >= static_cast(m_requiredValueCount)) - || (m_default && m_defaultValues.size() >= static_cast(m_requiredValueCount)); - } else { - return true; - } + return m_requiredValueCount == static_cast(-1) + || (m_values[occurrance].size() >= static_cast(m_requiredValueCount)) + || (m_default && m_defaultValues.size() >= static_cast(m_requiredValueCount)); } /*! @@ -359,9 +324,9 @@ inline bool Argument::isDefault() const * \brief Sets whether the argument is a default argument. * \sa isDefault() */ -inline void Argument::setDefault(bool value) +inline void Argument::setDefault(bool isDefault) { - m_default = value; + m_default = isDefault; } /*! @@ -370,7 +335,7 @@ inline void Argument::setDefault(bool value) * \sa setDefault() * \sa setDefaultValues() */ -inline const std::list &Argument::defaultValues() const +inline const std::vector &Argument::defaultValues() const { return m_defaultValues; } @@ -385,18 +350,64 @@ inline const std::list &Argument::defaultValues() const * \sa setDefault() * \sa defaultValues() */ -inline void Argument::setDefaultValues(const std::list &defaultValues) +inline void Argument::setDefaultValues(const std::initializer_list &defaultValues) { m_defaultValues = defaultValues; } /*! - * \brief Returns an indication whether the argument could be detected - * when parsing. + * \brief Returns an indication whether the argument could be detected when parsing. */ inline bool Argument::isPresent() const { - return m_present; + return !m_indices.empty(); +} + +/*! + * \brief Returns how often the argument could be detected when parsing. + */ +inline std::size_t Argument::occurrences() const +{ + return m_indices.size(); +} + +/*! + * \brief Returns the indices of the argument's occurences which could be detected when parsing. + */ +inline const std::vector &Argument::indices() const +{ + return m_indices; +} + +/*! + * \brief Returns the minimum number of occurrences. + * + * If the argument occurs not that many times, the parser will complain. + */ +inline std::size_t Argument::minOccurrences() const +{ + return m_minOccurrences; +} + +/*! + * \brief Returns the maximum number of occurrences. + * + * If the argument occurs more often, the parser will complain. + */ +inline std::size_t Argument::maxOccurrences() const +{ + return m_maxOccurrences; +} + +/*! + * \brief Sets the allowed number of occurrences. + * \sa minOccurrences() + * \sa maxOccurrences() + */ +inline void Argument::setConstraints(std::size_t minOccurrences, std::size_t maxOccurrences) +{ + m_minOccurrences = minOccurrences; + m_maxOccurrences = maxOccurrences; } /*! @@ -410,19 +421,25 @@ inline bool Argument::isPresent() const */ inline bool Argument::isRequired() const { - return m_required; + return m_minOccurrences; } /*! - * \brief Sets if this argument is mandatory or not. + * \brief Sets whether this argument is mandatory or not. * * The parser will complain if a mandatory argument is not present. * * * \sa isRequired() */ -inline void Argument::setRequired(bool value) +inline void Argument::setRequired(bool required) { - m_required = value; + if(required) { + if(!m_minOccurrences) { + m_minOccurrences = 1; + } + } else { + m_minOccurrences = 0; + } } /*! @@ -451,30 +468,6 @@ inline void Argument::setCombinable(bool value) m_combinable = value; } - -/*! - * \brief Returns an indication whether the argument can be specified implicitely. - * - * An implicit argument is assumed to be present even if only its value is present. - * Only one argument can be implicit. - * - * \sa setImplicit() - */ -inline bool Argument::isImplicit() const -{ - return m_implicit; -} - -/*! - * \brief Sets if this argument can be specified implicitely. - * - * \sa isImplicit() - */ -inline void Argument::setImplicit(bool value) -{ - m_implicit = value; -} - /*! * \brief Returns whether the argument denotes the operation. * @@ -502,6 +495,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. */ inline void Argument::setCallback(Argument::CallbackFunction callback) { @@ -567,19 +561,22 @@ public: void printHelp(std::ostream &os) const; Argument *findArg(const ArgumentPredicate &predicate) const; static Argument *findArg(const ArgumentVector &arguments, const ArgumentPredicate &predicate); -#ifdef DEBUG_BUILD - void verifySetup() const; -#endif void parseArgs(int argc, char *argv[]); + void parseArgs(int argc, const char *argv[]); unsigned int actualArgumentCount() const; - const std::string ¤tDirectory() const; + const char *currentDirectory() const; bool areUnknownArgumentsIgnored() const; void setIgnoreUnknownArguments(bool ignore); private: + IF_DEBUG_BUILD(void verifyArgs(const ArgumentVector &args);) + void readSpecifiedArgs(ArgumentVector &args, std::size_t &index, const char **&argv, const char **end); + void checkConstraints(const ArgumentVector &args); + void invokeCallbacks(const ArgumentVector &args); + ArgumentVector m_mainArgs; unsigned int m_actualArgc; - std::string m_currentDirectory; + const char *m_currentDirectory; bool m_ignoreUnknownArgs; }; @@ -592,6 +589,14 @@ inline const ArgumentVector &ArgumentParser::mainArguments() const return m_mainArgs; } +/*! + * \brief Parses the specified command line arguments. + */ +inline void ArgumentParser::parseArgs(int argc, char *argv[]) +{ + parseArgs(argc, const_cast(argv)); +} + /*! * \brief Returns the actual number of arguments that could be found when parsing. */ @@ -603,7 +608,7 @@ inline unsigned int ArgumentParser::actualArgumentCount() const /*! * \brief Returns the current directory. */ -inline const std::string &ArgumentParser::currentDirectory() const +inline const char *ArgumentParser::currentDirectory() const { return m_currentDirectory; } diff --git a/application/fakeqtconfigarguments.cpp b/application/fakeqtconfigarguments.cpp index cc81c3d..6bb84e9 100644 --- a/application/fakeqtconfigarguments.cpp +++ b/application/fakeqtconfigarguments.cpp @@ -12,8 +12,8 @@ namespace ApplicationUtilities { * \brief Constructs new fake Qt-config arguments. */ FakeQtConfigArguments::FakeQtConfigArguments() : - m_qtWidgetsGuiArg("qt-widgets-gui", "g", "shows a Qt widgets based graphical user interface (the application has not been built with Qt widgets support)"), - m_qtQuickGuiArg("qt-quick-gui", "q", "shows a Qt quick based graphical user interface (the application has not been built with Qt quick support)") + m_qtWidgetsGuiArg("qt-widgets-gui", 'g', "shows a Qt widgets based graphical user interface (the application has not been built with Qt widgets support)"), + m_qtQuickGuiArg("qt-quick-gui", 'q', "shows a Qt quick based graphical user interface (the application has not been built with Qt quick support)") {} } // namespace ApplicationUtilities diff --git a/conversion/stringconversion.h b/conversion/stringconversion.h index bc1753c..2667a15 100644 --- a/conversion/stringconversion.h +++ b/conversion/stringconversion.h @@ -178,6 +178,25 @@ template LIB_EXPORT NumberType string } } +/*! + * \brief Converts the given \a string to a numeric value using the specified \a base. + * \tparam NumberType The data type used to store the converted value. + * \tparam StringType The string type (should be an instantiation of the basic_string class template). + * \throws A ConversionException will be thrown if the provided string is not a valid number. + * \sa numberToString() + */ +template LIB_EXPORT NumberType stringToNumber(const CharType *string, int base = 10) +{ + std::basic_stringstream ss; + ss << std::setbase(base) << string; + NumberType result; + if((ss >> result) && ss.eof()) { + return result; + } else { + throw ConversionException("The specified string is no valid number."); + } +} + /*! * \brief Interprets the given \a integer at the specified position as std::string using the specified byte order. * diff --git a/tests/argumentparsertests.cpp b/tests/argumentparsertests.cpp new file mode 100644 index 0000000..fc7e938 --- /dev/null +++ b/tests/argumentparsertests.cpp @@ -0,0 +1,253 @@ +#include "./testutils.h" + +#include "../application/argumentparser.h" +#include "../application/failure.h" +#include "../application/fakeqtconfigarguments.h" + +#include "resources/config.h" + +#include +#include + +#include + +using namespace std; +using namespace ApplicationUtilities; + +using namespace CPPUNIT_NS; + +/*! + * \brief The ArgumentParserTests class tests the ArgumentParser and Argument classes. + */ +class ArgumentParserTests : public TestFixture +{ + CPPUNIT_TEST_SUITE(ArgumentParserTests); + CPPUNIT_TEST(testArgument); + CPPUNIT_TEST(testParsing); + CPPUNIT_TEST(testCallbacks); + CPPUNIT_TEST_SUITE_END(); + +public: + void setUp(); + void tearDown(); + + void testArgument(); + void testParsing(); + void testCallbacks(); + +private: + void callback(); +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(ArgumentParserTests); + +void ArgumentParserTests::setUp() +{} + +void ArgumentParserTests::tearDown() +{} + +/*! + * \brief Tests the behaviour of the argument class. + */ +void ArgumentParserTests::testArgument() +{ + Argument argument("test", 't', "some description"); + CPPUNIT_ASSERT_EQUAL(argument.isRequired(), false); + argument.setConstraints(1, 10); + CPPUNIT_ASSERT_EQUAL(argument.isRequired(), true); + Argument subArg("sub", 's', "sub arg"); + argument.addSubArgument(&subArg); + CPPUNIT_ASSERT_EQUAL(subArg.parents().at(0), &argument); + CPPUNIT_ASSERT(!subArg.conflictsWithArgument()); + +} + +/*! + * \brief Tests parsing command line arguments. + */ +void ArgumentParserTests::testParsing() +{ + ArgumentParser parser; + + // add some test argument definitions + SET_APPLICATION_INFO; + QT_CONFIG_ARGUMENTS qtConfigArgs; + HelpArgument helpArg(parser); + Argument verboseArg("verbose", 'v', "be verbose"); + verboseArg.setCombinable(true); + Argument fileArg("file", 'f', "specifies the path of the file to be opened"); + fileArg.setValueNames({"path"}); + fileArg.setRequiredValueCount(1); + Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened"); + filesArg.setValueNames({"path 1", "path 2"}); + filesArg.setRequiredValueCount(-1); + Argument outputFileArg("output-file", 'o', "specifies the path of the output file"); + outputFileArg.setValueNames({"path"}); + outputFileArg.setRequiredValueCount(1); + outputFileArg.setRequired(true); + outputFileArg.setCombinable(true); + Argument printFieldNamesArg("print-field-names", '\0', "prints available field names"); + Argument displayFileInfoArg("display-file-info", 'i', "displays general file information"); + displayFileInfoArg.setDenotesOperation(true); + displayFileInfoArg.setSubArguments({&fileArg, &verboseArg}); + Argument fieldsArg("fields", '\0', "specifies the fields"); + fieldsArg.setRequiredValueCount(-1); + fieldsArg.setValueNames({"title", "album", "artist", "trackpos"}); + fieldsArg.setDefault(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}); + parser.setMainArguments({&qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayTagInfoArg, &displayFileInfoArg, &helpArg}); + + // define some argument values + const char *argv[] = {"tageditor", "get", "album", "title", "diskpos", "-f", "somefile"}; + // try to parse, this should fail + try { + parser.parseArgs(7, argv); + CPPUNIT_FAIL("Exception expected."); + } catch(const Failure &e) { + CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"files\" can not be combined with \"fields\".")); + } + + // try to parse again, but adjust the configuration for a successful parse + displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); + filesArg.setCombinable(true); + parser.parseArgs(7, argv); + // check results + CPPUNIT_ASSERT(!displayFileInfoArg.isPresent()); + CPPUNIT_ASSERT(!strcmp(parser.currentDirectory(), "tageditor")); + 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); + + // define the same arguments in a different way + const char *argv2[] = {"tageditor", "-p", "album", "title", "diskpos", "--file", "somefile"}; + // reparse the args + displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); + parser.parseArgs(7, argv2); + // check results again + 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")); + + // forget "get"/"-p" + const char *argv3[] = {"tageditor", "album", "title", "diskpos", "--file", "somefile"}; + displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); + + // a parsing error should occur because the argument "album" is not defined + try { + parser.parseArgs(6, argv3); + CPPUNIT_FAIL("Exception expected."); + } catch(const Failure &e) { + 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 + displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); + parser.setIgnoreUnknownArguments(true); + parser.parseArgs(6, argv3); + // none of the arguments should be present now + CPPUNIT_ASSERT(!displayFileInfoArg.isPresent()); + CPPUNIT_ASSERT(!displayTagInfoArg.isPresent()); + CPPUNIT_ASSERT(!fieldsArg.isPresent()); + CPPUNIT_ASSERT(!filesArg.isPresent()); + + // test abbreviations like "-vf" + const char *argv4[] = {"tageditor", "-i", "-vf", "test"}; + displayTagInfoArg.reset(), fieldsArg.reset(), filesArg.reset(); + parser.setIgnoreUnknownArguments(false); + parser.parseArgs(4, argv4); + CPPUNIT_ASSERT(displayFileInfoArg.isPresent()); + CPPUNIT_ASSERT(verboseArg.isPresent()); + CPPUNIT_ASSERT(!displayTagInfoArg.isPresent()); + CPPUNIT_ASSERT(!filesArg.isPresent()); + CPPUNIT_ASSERT(fileArg.isPresent()); + CPPUNIT_ASSERT(!strcmp(fileArg.values().at(0), "test")); + CPPUNIT_ASSERT_THROW(fileArg.values().at(1), out_of_range); + + // don't reset verbose argument to test constraint checking + displayFileInfoArg.reset(), fileArg.reset(); + try { + parser.parseArgs(4, argv4); + CPPUNIT_FAIL("Exception expected."); + } catch(const Failure &e) { + CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" mustn't be specified more than 1 time.")); + } + + // relax constraint + displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); + verboseArg.setConstraints(0, -1); + parser.parseArgs(4, argv4); + + // make verbose mandatory + verboseArg.setRequired(true); + displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); + parser.parseArgs(4, argv4); + + // make it complain about missing argument + const char *argv5[] = {"tageditor", "-i", "-f", "test"}; + displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); + try { + parser.parseArgs(4, argv5); + CPPUNIT_FAIL("Exception expected."); + } catch(const Failure &e) { + CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" must be specified at least 1 time.")); + } + + // it should not complain if -i is not present + const char *argv6[] = {"tageditor", "-g"}; + displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); + parser.parseArgs(2, argv6); + + // it should not be possible to specify -f without -i or -p + const char *argv7[] = {"tageditor", "-f", "test"}; + displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset(); + try { + parser.parseArgs(3, argv7); + CPPUNIT_FAIL("Exception expected."); + } catch(const Failure &e) { + CPPUNIT_ASSERT(!strcmp(e.what(), "The specified argument \"-f\" is unknown and will be ignored.")); + } +} + +void ArgumentParserTests::testCallbacks() +{ + ArgumentParser parser; + Argument callbackArg("with-callback", 't', "callback test"); + callbackArg.setRequiredValueCount(2); + callbackArg.setCallback([] (const vector &values) { + CPPUNIT_ASSERT(values.size() == 2); + CPPUNIT_ASSERT(!strcmp(values[0], "val1")); + CPPUNIT_ASSERT(!strcmp(values[1], "val2")); + throw 42; + }); + Argument noCallbackArg("no-callback", 'l', "callback test"); + noCallbackArg.setRequiredValueCount(2); + parser.setMainArguments({&callbackArg, &noCallbackArg}); + + // test whether callback is invoked when argument with callback is specified + const char *argv[] = {"test", "-t", "val1", "val2"}; + try { + parser.parseArgs(4, argv); + } catch(int i) { + CPPUNIT_ASSERT_EQUAL(i, 42); + } + + // test whether callback is not invoked when argument with callback is not specified + callbackArg.reset(); + const char *argv2[] = {"test", "-l", "val1", "val2"}; + parser.parseArgs(4, argv2); + +} diff --git a/tests/cppunit.h b/tests/cppunit.h index 2aba57f..54e0be7 100644 --- a/tests/cppunit.h +++ b/tests/cppunit.h @@ -23,14 +23,13 @@ int main(int argc, char **argv) // run tests TextUi::TestRunner runner; TestFactoryRegistry ®istry = TestFactoryRegistry::getRegistry(); - const auto &units = testApp.units(); - if(units.empty()) { + if(!testApp.unitsSpecified() || testApp.units().empty()) { // no units specified -> test all runner.addTest(registry.makeTest()); } else { // pick specified units from overall test Test *overallTest = registry.makeTest(); - for(const string &unit : units) { + for(const char *unit : testApp.units()) { try { runner.addTest(overallTest->findTest(unit)); } catch(const invalid_argument &) { diff --git a/tests/testutils.cpp b/tests/testutils.cpp index 13f06b3..09f7d3a 100644 --- a/tests/testutils.cpp +++ b/tests/testutils.cpp @@ -33,9 +33,9 @@ TestApplication *TestApplication::m_instance = nullptr; */ TestApplication::TestApplication(int argc, char **argv) : m_helpArg(m_parser), - m_testFilesPathArg("test-files-path", "p", "specifies the path of the directory with test files"), - m_workingDirArg("working-dir", "w", "specifies the directory to store working copies of test files"), - m_unitsArg("units", "u", "specifies the units to test; omit to test all units") + m_testFilesPathArg("test-files-path", 'p', "specifies the path of the directory with test files"), + m_workingDirArg("working-dir", 'w', "specifies the directory to store working copies of test files"), + m_unitsArg("units", 'u', "specifies the units to test; omit to test all units") { // check whether there is already an instance if(m_instance) { @@ -69,8 +69,8 @@ TestApplication::TestApplication(int argc, char **argv) : m_parser.parseArgs(argc, argv); cerr << "Directories used to search for testfiles:" << endl; if(m_testFilesPathArg.isPresent()) { - if(!m_testFilesPathArg.values().front().empty()) { - cerr << (m_testFilesPathArgValue = m_testFilesPathArg.values().front() + '/') << endl; + if(*m_testFilesPathArg.values().front()) { + cerr << ((m_testFilesPathArgValue = m_testFilesPathArg.values().front()) += '/') << endl; } else { cerr << (m_testFilesPathArgValue = "./") << endl; } @@ -81,8 +81,8 @@ TestApplication::TestApplication(int argc, char **argv) : cerr << "./testfiles/" << endl << endl; cerr << "Directory used to store working copies:" << endl; if(m_workingDirArg.isPresent()) { - if(!m_workingDirArg.values().front().empty()) { - m_workingDir = m_workingDirArg.values().front() + '/'; + if(*m_workingDirArg.values().front()) { + (m_workingDir = m_workingDirArg.values().front()) += '/'; } else { m_workingDir = "./"; } diff --git a/tests/testutils.h b/tests/testutils.h index fe2f0c4..fa0da66 100644 --- a/tests/testutils.h +++ b/tests/testutils.h @@ -18,7 +18,8 @@ public: #ifdef PLATFORM_UNIX std::string workingCopyPath(const std::string &name) const; #endif - const std::vector &units() const; + bool unitsSpecified() const; + const std::vector &units() const; static const TestApplication *instance(); private: @@ -54,9 +55,18 @@ inline const TestApplication *TestApplication::instance() } /*! - * \brief Returns the specified test units. + * \brief Returns whether particular units have been specified. */ -inline const std::vector &TestApplication::units() const +inline bool TestApplication::unitsSpecified() const +{ + return m_unitsArg.isPresent(); +} + +/*! + * \brief Returns the specified test units. + * \remarks The units argument must be present. + */ +inline const std::vector &TestApplication::units() const { return m_unitsArg.values(); }