diff --git a/application/argumentparser.cpp b/application/argumentparser.cpp index 8f8d150..d3172cf 100644 --- a/application/argumentparser.cpp +++ b/application/argumentparser.cpp @@ -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 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 *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(-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 diff --git a/application/argumentparser.h b/application/argumentparser.h index 0eda0a2..03b3caf 100644 --- a/application/argumentparser.h +++ b/application/argumentparser.h @@ -57,10 +57,6 @@ public: void setValueNames(std::initializer_list valueNames); void appendValueName(const char *valueName); bool allRequiredValuesPresent(std::size_t occurrance = 0) const; - bool isDefault() const; - 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; @@ -98,8 +94,7 @@ private: bool m_denotesOperation; std::size_t m_requiredValueCount; std::vector m_valueNames; - bool m_default; - std::vector m_defaultValues; + bool m_implicit; std::vector m_indices; std::vector > 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(-1) - || (m_values[occurrance].size() >= static_cast(m_requiredValueCount)) - || (m_default && m_defaultValues.size() >= static_cast(m_requiredValueCount)); + || (m_values[occurrance].size() >= static_cast(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 &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 &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: diff --git a/tests/argumentparsertests.cpp b/tests/argumentparsertests.cpp index fc7e938..bb36df2 100644 --- a/tests/argumentparsertests.cpp +++ b/tests/argumentparsertests.cpp @@ -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); - }