diff options
-rw-r--r-- | args.hxx | 595 | ||||
-rw-r--r-- | test.cxx | 65 |
2 files changed, 485 insertions, 175 deletions
@@ -88,7 +88,7 @@ namespace args * different width for the first line * * \param width The width of the body - * \param the widtho f the first line, defaults to the width of the body + * \param the width of the first line, defaults to the width of the body * \return the vector of lines */ inline std::vector<std::string> Wrap(const std::string &in, const std::string::size_type width, std::string::size_type firstlinewidth = 0) @@ -510,6 +510,26 @@ namespace args } }; + struct Nargs + { + const size_t min; + const size_t max; + + Nargs(size_t min_, size_t max_) : min(min_), max(max_) + { +#ifndef ARGS_NOEXCEPT + if (max < min) + { + throw std::invalid_argument("Nargs: max > min"); + } +#endif + } + + Nargs(size_t num_) : min(num_), max(num_) + { + } + }; + /** Base class for all flag options */ class FlagBase : public NamedBase @@ -595,6 +615,18 @@ namespace args std::get<1>(description) = help; return description; } + + /** Defines how many values can be consumed by this option. + * + * \return closed interval [min, max] + */ + virtual Nargs NumberOfArguments() const noexcept = 0; + + /** Parse values of this option. + * + * \param value Vector of values. It's size must be in NumberOfArguments() interval. + */ + virtual void ParseValue(const std::vector<std::string> &value) = 0; }; /** Base class for value-accepting flag options @@ -605,7 +637,6 @@ namespace args ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : FlagBase(name_, help_, std::move(matcher_), extraError_) {} ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : FlagBase(name_, help_, std::move(matcher_), options_) {} virtual ~ValueFlagBase() {} - virtual void ParseValue(const std::string &value) = 0; virtual std::tuple<std::string, std::string> GetDescription(const std::string &shortPrefix, const std::string &longPrefix, const std::string &shortSeparator, const std::string &longSeparator) const override { @@ -624,6 +655,11 @@ namespace args std::get<1>(description) = help; return description; } + + virtual Nargs NumberOfArguments() const noexcept override + { + return 1; + } }; /** Base class for positional options @@ -964,6 +1000,197 @@ namespace args bool allowSeparateShortValue; bool allowSeparateLongValue; + protected: + bool RaiseParseError(const std::string &message) + { +#ifdef ARGS_NOEXCEPT + (void)message; + error = Error::Parse; + return false; +#else + throw ParseError(message); +#endif + } + + enum class OptionType + { + LongFlag, + ShortFlag, + Positional + }; + + OptionType ParseOption(const std::string &s) + { + if (s.find(longprefix) == 0 && s.length() > longprefix.length()) + { + return OptionType::LongFlag; + } + + if (s.find(shortprefix) == 0 && s.length() > shortprefix.length()) + { + return OptionType::ShortFlag; + } + + return OptionType::Positional; + } + + /** (INTERNAL) Parse flag's values + * + * \param arg The string to display in error message as a flag name + * \param[in, out] it The iterator to first value. It will point to the last value + * \param end The end iterator + * \param joinedArg Joined value (e.g. bar in --foo=bar) + * \param canDiscardJoined If true joined value can be parsed as flag not as a value (as in -abcd) + * \param[out] values The vector to store parsed arg's values + */ + template <typename It> + bool ParseArgsValues(FlagBase &flag, const std::string &arg, It &it, It end, + const bool allowSeparate, const bool allowJoined, + const bool hasJoined, const std::string &joinedArg, + const bool canDiscardJoined, std::vector<std::string> &values) + { + values.clear(); + + Nargs nargs = flag.NumberOfArguments(); + + if (hasJoined && !allowJoined && nargs.min != 0) + { + return RaiseParseError("Flag '" + arg + "' was passed a joined argument, but these are disallowed"); + } + + if (hasJoined) + { + if (!canDiscardJoined || nargs.max != 0) + { + values.push_back(joinedArg); + } + } else if (!allowSeparate) + { + if (nargs.min != 0) + { + return RaiseParseError("Flag '" + arg + "' was passed a separate argument, but these are disallowed"); + } + } else + { + auto valueIt = it; + ++valueIt; + + while (valueIt != end && + values.size() < nargs.max && + (nargs.min == nargs.max || ParseOption(*valueIt) == OptionType::Positional)) + { + + values.push_back(*valueIt); + ++it; + ++valueIt; + } + } + + if (values.size() > nargs.max) + { + return RaiseParseError("Passed an argument into a non-argument flag: " + arg); + } else if (values.size() < nargs.min) + { + if (nargs.min == 1 && nargs.max == 1) + { + return RaiseParseError("Flag '" + arg + "' requires an argument but received none"); + } else if (nargs.min == 1) + { + return RaiseParseError("Flag '" + arg + "' requires at least one argument but received none"); + } else if (nargs.min != nargs.max) + { + return RaiseParseError("Flag '" + arg + "' requires at least " + std::to_string(nargs.min) + + " arguments but received " + std::to_string(values.size())); + } else + { + return RaiseParseError("Flag '" + arg + "' requires " + std::to_string(nargs.min) + + " arguments but received " + std::to_string(values.size())); + } + } + + return true; + } + + template <typename It> + bool ParseLong(It &it, It end) + { + const auto &chunk = *it; + const auto argchunk = chunk.substr(longprefix.size()); + // Try to separate it, in case of a separator: + const auto separator = longseparator.empty() ? argchunk.npos : argchunk.find(longseparator); + // If the separator is in the argument, separate it. + const auto arg = (separator != argchunk.npos ? + std::string(argchunk, 0, separator) + : argchunk); + const auto joined = (separator != argchunk.npos ? + argchunk.substr(separator + longseparator.size()) + : std::string()); + + if (auto flag = Match(arg)) + { + std::vector<std::string> values; + if (!ParseArgsValues(*flag, arg, it, end, allowSeparateLongValue, allowJoinedLongValue, + separator != argchunk.npos, joined, false, values)) + { + return false; + } + + flag->ParseValue(values); + + if (flag->KickOut()) + { + ++it; + return false; + } + } else + { + return RaiseParseError("Flag could not be matched: " + arg); + } + + return true; + } + + template <typename It> + bool ParseShort(It &it, It end) + { + const auto &chunk = *it; + const auto argchunk = chunk.substr(shortprefix.size()); + for (auto argit = std::begin(argchunk); argit != std::end(argchunk); ++argit) + { + const auto arg = *argit; + + if (auto flag = Match(arg)) + { + const std::string value(argit + 1, std::end(argchunk)); + std::vector<std::string> values; + if (!ParseArgsValues(*flag, std::string(1, arg), it, end, + allowSeparateShortValue, allowJoinedShortValue, + !value.empty(), value, !value.empty(), values)) + { + return false; + } + + flag->ParseValue(values); + + if (flag->KickOut()) + { + ++it; + return false; + } + + if (!values.empty()) + { + break; + } + } else + { + return RaiseParseError("Flag could not be matched: '" + std::string(1, arg) + "'"); + } + } + + return true; + } + public: /** A simple structure of parameters for easy user-modifyable help menus */ @@ -1270,172 +1497,17 @@ namespace args if (!terminated && chunk == terminator) { terminated = true; - // If a long arg was found - } else if (!terminated && chunk.find(longprefix) == 0 && chunk.size() > longprefix.size()) + } else if (!terminated && ParseOption(chunk) == OptionType::LongFlag) { - const auto argchunk = chunk.substr(longprefix.size()); - // Try to separate it, in case of a separator: - const auto separator = longseparator.empty() ? argchunk.npos : argchunk.find(longseparator); - // If the separator is in the argument, separate it. - const auto arg = (separator != argchunk.npos ? - std::string(argchunk, 0, separator) - : argchunk); - - if (auto base = Match(arg)) + if (!ParseLong(it, end)) { - if (auto argbase = dynamic_cast<ValueFlagBase *>(base)) - { - if (separator != argchunk.npos) - { - if (allowJoinedLongValue) - { - argbase->ParseValue(argchunk.substr(separator + longseparator.size())); - } else - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; - return it; -#else - std::ostringstream problem; - problem << "Flag '" << arg << "' was passed a joined argument, but these are disallowed"; - throw ParseError(problem.str()); -#endif - } - } else - { - ++it; - if (it == end) - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; - return it; -#else - std::ostringstream problem; - problem << "Flag '" << arg << "' requires an argument but received none"; - throw ParseError(problem.str()); -#endif - } - - if (allowSeparateLongValue) - { - argbase->ParseValue(*it); - } else - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; - return it; -#else - std::ostringstream problem; - problem << "Flag '" << arg << "' was passed a separate argument, but these are disallowed"; - throw ParseError(problem.str()); -#endif - } - } - } else if (separator != argchunk.npos) - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; - return it; -#else - std::ostringstream problem; - problem << "Passed an argument into a non-argument flag: " << chunk; - throw ParseError(problem.str()); -#endif - } - - if (base->KickOut()) - { - return ++it; - } - } else - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; return it; -#else - std::ostringstream problem; - problem << "Flag could not be matched: " << arg; - throw ParseError(problem.str()); -#endif } - // Check short args - } else if (!terminated && chunk.find(shortprefix) == 0 && chunk.size() > shortprefix.size()) + } else if (!terminated && ParseOption(chunk) == OptionType::ShortFlag) { - const auto argchunk = chunk.substr(shortprefix.size()); - for (auto argit = std::begin(argchunk); argit != std::end(argchunk); ++argit) + if (!ParseShort(it, end)) { - const auto arg = *argit; - - if (auto base = Match(arg)) - { - if (auto argbase = dynamic_cast<ValueFlagBase *>(base)) - { - const std::string value(++argit, std::end(argchunk)); - if (!value.empty()) - { - if (allowJoinedShortValue) - { - argbase->ParseValue(value); - } else - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; - return it; -#else - std::ostringstream problem; - problem << "Flag '" << arg << "' was passed a joined argument, but these are disallowed"; - throw ParseError(problem.str()); -#endif - } - } else - { - ++it; - if (it == end) - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; - return it; -#else - std::ostringstream problem; - problem << "Flag '" << arg << "' requires an argument but received none"; - throw ParseError(problem.str()); -#endif - } - - if (allowSeparateShortValue) - { - argbase->ParseValue(*it); - } else - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; - return it; -#else - std::ostringstream problem; - problem << "Flag '" << arg << "' was passed a separate argument, but these are disallowed"; - throw ParseError(problem.str()); -#endif - } - } - // Because this argchunk is done regardless - break; - } - - if (base->KickOut()) - { - return ++it; - } - } else - { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; - return it; -#else - std::ostringstream problem; - problem << "Flag could not be matched: '" << arg << "'"; - throw ParseError(problem.str()); -#endif - } + return it; } } else { @@ -1450,14 +1522,8 @@ namespace args } } else { -#ifdef ARGS_NOEXCEPT - error = Error::Parse; + RaiseParseError("Passed in argument, but no positional arguments were ready to receive it: " + chunk); return it; -#else - std::ostringstream problem; - problem << "Passed in argument, but no positional arguments were ready to receive it: " << chunk; - throw ParseError(problem.str()); -#endif } } } @@ -1477,6 +1543,7 @@ namespace args throw ValidationError(problem.str()); #endif } + return end; } @@ -1536,6 +1603,15 @@ namespace args { return Matched(); } + + virtual Nargs NumberOfArguments() const noexcept override + { + return 0; + } + + virtual void ParseValue(const std::vector<std::string>&) override + { + } }; /** Help flag class @@ -1684,8 +1760,10 @@ namespace args typename Reader = ValueReader<T>> class ValueFlag : public ValueFlagBase { - private: + protected: T value; + + private: Reader reader; public: @@ -1705,8 +1783,10 @@ namespace args virtual ~ValueFlag() {} - virtual void ParseValue(const std::string &value_) override + virtual void ParseValue(const std::vector<std::string> &values_) override { + const std::string &value_ = values_.at(0); + #ifdef ARGS_NOEXCEPT if (!reader(name, value_, this->value)) { @@ -1725,6 +1805,165 @@ namespace args } }; + /** An optional argument-accepting flag class + * + * \tparam T the type to extract the argument as + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + */ + template < + typename T, + typename Reader = ValueReader<T>> + class ImplicitValueFlag : public ValueFlag<T, Reader> + { + protected: + + T implicitValue; + T defaultValue; + + public: + + ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &implicitValue_, const T &defaultValue_ = T(), Options options_ = {}) + : ValueFlag<T, Reader>(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(implicitValue_), defaultValue(defaultValue_) + { + } + + ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), Options options_ = {}) + : ValueFlag<T, Reader>(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(defaultValue_), defaultValue(defaultValue_) + { + } + + ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) + : ValueFlag<T, Reader>(group_, name_, help_, std::move(matcher_), {}, options_), implicitValue(), defaultValue() + { + } + + virtual ~ImplicitValueFlag() {} + + virtual Nargs NumberOfArguments() const noexcept override + { + return {0, 1}; + } + + virtual void ParseValue(const std::vector<std::string> &value_) override + { + if (value_.empty()) + { + this->value = implicitValue; + } else + { + ValueFlag<T, Reader>::ParseValue(value_); + } + } + + virtual void Reset() noexcept override + { + this->value = defaultValue; + ValueFlag<T, Reader>::Reset(); + } + }; + + /** A variadic arguments accepting flag class + * + * \tparam T the type to extract the argument as + * \tparam List the list type that houses the values + * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) + */ + template < + typename T, + template <typename...> class List = std::vector, + typename Reader = ValueReader<T>> + class NargsValueFlag : public FlagBase + { + protected: + + List<T> values; + Nargs nargs; + Reader reader; + + public: + + typedef List<T> Container; + typedef T value_type; + typedef typename Container::allocator_type allocator_type; + typedef typename Container::pointer pointer; + typedef typename Container::const_pointer const_pointer; + typedef T& reference; + typedef const T& const_reference; + typedef typename Container::size_type size_type; + typedef typename Container::difference_type difference_type; + typedef typename Container::iterator iterator; + typedef typename Container::const_iterator const_iterator; + typedef std::reverse_iterator<iterator> reverse_iterator; + typedef std::reverse_iterator<const_iterator> const_reverse_iterator; + + NargsValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, const List<T> &defaultValues_ = {}, Options options_ = {}) + : FlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), nargs(nargs_) + { + group_.Add(*this); + } + + virtual ~NargsValueFlag() {} + + virtual Nargs NumberOfArguments() const noexcept override + { + return nargs; + } + + virtual void ParseValue(const std::vector<std::string> &values_) override + { + values.clear(); + + for (const std::string &value : values_) + { + T v; +#ifdef ARGS_NOEXCEPT + if (!reader(name, value, v)) + { + error = Error::Parse; + } +#else + reader(name, value, v); +#endif + values.insert(std::end(values), v); + } + } + + List<T> &Get() noexcept + { + return values; + } + + iterator begin() noexcept + { + return values.begin(); + } + + const_iterator begin() const noexcept + { + return values.begin(); + } + + const_iterator cbegin() const noexcept + { + return values.cbegin(); + } + + iterator end() noexcept + { + return values.end(); + } + + const_iterator end() const noexcept + { + return values.end(); + } + + const_iterator cend() const noexcept + { + return values.cend(); + } + }; + /** An argument-accepting flag class that pushes the found values into a list * * \tparam T the type to extract the argument as @@ -1741,7 +1980,7 @@ namespace args using Container = List<T>; Container values; Reader reader; - + public: typedef T value_type; @@ -1764,8 +2003,10 @@ namespace args virtual ~ValueFlagList() {} - virtual void ParseValue(const std::string &value_) override + virtual void ParseValue(const std::vector<std::string> &values_) override { + const std::string &value_ = values_.at(0); + T v; #ifdef ARGS_NOEXCEPT if (!reader(name, value_, v)) @@ -1863,8 +2104,10 @@ namespace args virtual ~MapFlag() {} - virtual void ParseValue(const std::string &value_) override + virtual void ParseValue(const std::vector<std::string> &values_) override { + const std::string &value_ = values_.at(0); + K key; #ifdef ARGS_NOEXCEPT if (!reader(name, value_, key)) @@ -1941,8 +2184,10 @@ namespace args virtual ~MapFlagList() {} - virtual void ParseValue(const std::string &value) override + virtual void ParseValue(const std::vector<std::string> &values_) override { + const std::string &value = values_.at(0); + K key; #ifdef ARGS_NOEXCEPT if (!reader(name, value, key)) @@ -606,6 +606,71 @@ TEST_CASE("Hidden options are excluded from help", "[args]") REQUIRE(std::get<0>(desc[2]) == "b[bar]"); } +TEST_CASE("Implicit values work as expected", "[args]") +{ + args::ArgumentParser parser("Test command"); + args::ImplicitValueFlag<int> j(parser, "parallel", "parallel", {'j', "parallel"}, 0, 1); + args::Flag foo(parser, "FOO", "test flag", {'f', "foo"}); + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-j"})); + REQUIRE(args::get(j) == 0); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-j4"})); + REQUIRE(args::get(j) == 4); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-j", "4"})); + REQUIRE(args::get(j) == 4); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-j", "-f"})); + REQUIRE(args::get(j) == 0); +} + +TEST_CASE("Nargs work as expected", "[args]") +{ + args::ArgumentParser parser("Test command"); + args::NargsValueFlag<int> a(parser, "", "", {'a'}, 2); + args::NargsValueFlag<int> b(parser, "", "", {'b'}, {2, 3}); + args::NargsValueFlag<std::string> c(parser, "", "", {'c'}, {0, 2}); + args::Flag f(parser, "", "", {'f'}); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-a", "1", "2"})); + REQUIRE((args::get(a) == std::vector<int>{1, 2})); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-a", "1", "2", "-f"})); + REQUIRE((args::get(a) == std::vector<int>{1, 2})); + REQUIRE(args::get(f) == true); + + REQUIRE_THROWS_AS(parser.ParseArgs(std::vector<std::string>{"-a", "1"}), args::ParseError); + REQUIRE_THROWS_AS(parser.ParseArgs(std::vector<std::string>{"-a1"}), args::ParseError); + REQUIRE_THROWS_AS(parser.ParseArgs(std::vector<std::string>{"-a1", "2"}), args::ParseError); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-b", "1", "2", "-f"})); + REQUIRE((args::get(b) == std::vector<int>{1, 2})); + REQUIRE(args::get(f) == true); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-b", "1", "2", "3"})); + REQUIRE((args::get(b) == std::vector<int>{1, 2, 3})); + REQUIRE(args::get(f) == false); + + std::vector<int> vec; + for (int c : b) + { + vec.push_back(c); + } + + REQUIRE((vec == std::vector<int>{1, 2, 3})); + + parser.SetArgumentSeparations(true, true, false, false); + REQUIRE_THROWS_AS(parser.ParseArgs(std::vector<std::string>{"-a", "1", "2"}), args::ParseError); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-c", "-f"})); + REQUIRE(args::get(c).empty()); + REQUIRE(args::get(f) == true); + + REQUIRE_NOTHROW(parser.ParseArgs(std::vector<std::string>{"-cf"})); + REQUIRE((args::get(c) == std::vector<std::string>{"f"})); + REQUIRE(args::get(f) == false); +} + #undef ARGS_HXX #define ARGS_TESTNAMESPACE #define ARGS_NOEXCEPT |