From cf665adc56e9185553960132de1b4115a3216ded Mon Sep 17 00:00:00 2001 From: Pavel Belikov Date: Thu, 9 Nov 2017 20:05:40 +0300 Subject: add subparsers validation --- args.hxx | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++---- test.cxx | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 5 deletions(-) diff --git a/args.hxx b/args.hxx index c290d6b..00b5706 100644 --- a/args.hxx +++ b/args.hxx @@ -160,7 +160,8 @@ namespace args Required, Map, Extra, - Help + Help, + Subparser, }; #else /** Base error class @@ -234,6 +235,15 @@ namespace args Help(const std::string &flag) : Error(flag) {} virtual ~Help() {}; }; + + /** (INTERNAL) An exception that emulates coroutine-like control flow for subparsers. + */ + class SubparserError : public Error + { + public: + SubparserError() : Error("") {} + virtual ~SubparserError() {}; + }; #endif /** A simple unified option type for unified initializer lists for the Matcher class. @@ -1222,6 +1232,9 @@ namespace args mutable bool subparserHasFlag = false; mutable bool subparserHasPositional = false; mutable bool subparserHasCommand = false; +#ifdef ARGS_NOEXCEPT + mutable Error subparserError = Error::None; +#endif mutable Subparser *subparser = nullptr; protected: @@ -1508,7 +1521,7 @@ namespace args { parserCoroutine(coro.Parser()); } - catch (args::Help) + catch (args::SubparserError) { } #else @@ -1641,7 +1654,33 @@ namespace args subparserHasFlag = false; subparserHasPositional = false; subparserHasCommand = false; +#ifdef ARGS_NOEXCEPT + subparserError = Error::None; +#endif + } + +#ifdef ARGS_NOEXCEPT + /// Only for ARGS_NOEXCEPT + virtual Error GetError() const override + { + if (!Matched()) + { + return Error::None; + } + + if (error != Error::None) + { + return error; + } + + if (subparserError != Error::None) + { + return subparserError; + } + + return Group::GetError(); } +#endif }; /** The main user facing command line argument parser class @@ -1914,10 +1953,22 @@ namespace args RaiiSubparser coro(*this, std::vector(it, end)); coroutine(coro.Parser()); #ifdef ARGS_NOEXCEPT - if (GetError() != Error::None) + error = GetError(); + if (error != Error::None) + { + return end; + } + + if (!coro.Parser().IsParsed()) { + error = Error::Usage; return end; } +#else + if (!coro.Parser().IsParsed()) + { + throw UsageError("Subparser::Parse was not called"); + } #endif break; @@ -2196,6 +2247,13 @@ namespace args { // Reset all Matched statuses and errors Reset(); +#ifdef ARGS_NOEXCEPT + error = GetError(); + if (error != Error::None) + { + return end; + } +#endif return Parse(begin, end); } @@ -2241,6 +2299,7 @@ namespace args void Subparser::Parse() { isParsed = true; + Reset(); command.subparserDescription = GetDescription(helpParams, 0); command.subparserHasFlag = HasFlag(); command.subparserHasPositional = HasPositional(); @@ -2249,9 +2308,9 @@ namespace args if (parser == nullptr) { #ifndef ARGS_NOEXCEPT - throw args::Help(""); + throw args::SubparserError(); #else - error = Error::Help; + error = Error::Subparser; return; #endif } @@ -2259,6 +2318,10 @@ namespace args auto it = parser->Parse(args.begin(), args.end()); command.Validate(parser->ShortPrefix(), parser->LongPrefix()); kicked.assign(it, args.end()); + +#ifdef ARGS_NOEXCEPT + command.subparserError = GetError(); +#endif } inline std::ostream &operator<<(std::ostream &os, const ArgumentParser &parser) diff --git a/test.cxx b/test.cxx index 7f92f26..0bb58d2 100644 --- a/test.cxx +++ b/test.cxx @@ -635,8 +635,11 @@ TEST_CASE("Nargs work as expected", "[args]") args::NargsValueFlag a(parser, "", "", {'a'}, 2); args::NargsValueFlag b(parser, "", "", {'b'}, {2, 3}); args::NargsValueFlag c(parser, "", "", {'c'}, {0, 2}); + args::NargsValueFlag d(parser, "", "", {'d'}, {1, 3}); args::Flag f(parser, "", "", {'f'}); + REQUIRE_THROWS_AS(args::Nargs(3, 2), args::UsageError); + REQUIRE_NOTHROW(parser.ParseArgs(std::vector{"-a", "1", "2"})); REQUIRE((args::get(a) == std::vector{1, 2})); @@ -676,6 +679,9 @@ TEST_CASE("Nargs work as expected", "[args]") REQUIRE_NOTHROW(parser.ParseArgs(std::vector{"-cf"})); REQUIRE((args::get(c) == std::vector{"f"})); REQUIRE(args::get(f) == false); + + REQUIRE_THROWS_AS(parser.ParseArgs(std::vector{"-d"}), args::ParseError); + REQUIRE_THROWS_AS(parser.ParseArgs(std::vector{"-b"}), args::ParseError); } TEST_CASE("Simple commands work as expected", "[args]") @@ -869,6 +875,8 @@ TEST_CASE("Subparser validation works as expected", "[args]") args::Command b(p, "b", "command b"); args::ValueFlag f(b, "", "", {'f'}, args::Options::Required); + args::Command c(p, "c", "command c", [](args::Subparser&){}); + REQUIRE_NOTHROW(p.ParseArgs(std::vector{})); REQUIRE_THROWS_AS(p.ParseArgs(std::vector{"a"}), args::RequiredError); REQUIRE_NOTHROW(p.ParseArgs(std::vector{"a", "-f", "F"})); @@ -880,6 +888,10 @@ TEST_CASE("Subparser validation works as expected", "[args]") p.RequireCommand(false); REQUIRE_NOTHROW(p.ParseArgs(std::vector{})); + + REQUIRE_THROWS_AS(p.ParseArgs(std::vector{"c"}), args::UsageError); + + REQUIRE_THROWS_AS(p.ParseArgs(std::vector{"unknown-command"}), args::ParseError); } TEST_CASE("Global options work as expected", "[args]") @@ -960,3 +972,77 @@ TEST_CASE("Noexcept mode works as expected", "[args]") parser.ParseArgs(std::vector{"--mf", "yellow"}); REQUIRE(parser.GetError() == argstest::Error::None); } + +TEST_CASE("Required flags work as expected in noexcept mode", "[args]") +{ + argstest::ArgumentParser parser1("Test command"); + argstest::ValueFlag foo(parser1, "foo", "foo", {'f', "foo"}, argstest::Options::Required); + argstest::ValueFlag bar(parser1, "bar", "bar", {'b', "bar"}); + + parser1.ParseArgs(std::vector{"-f", "42"}); + REQUIRE(foo.Get() == 42); + REQUIRE(parser1.GetError() == argstest::Error::None); + + parser1.ParseArgs(std::vector{"-b4"}); + REQUIRE(parser1.GetError() == argstest::Error::Required); + + argstest::ArgumentParser parser2("Test command"); + argstest::Positional pos1(parser2, "a", "a"); + parser2.ParseArgs(std::vector{}); + REQUIRE(parser2.GetError() == argstest::Error::None); + + argstest::ArgumentParser parser3("Test command"); + argstest::Positional pos2(parser3, "a", "a", argstest::Options::Required); + parser3.ParseArgs(std::vector{}); + REQUIRE(parser3.GetError() == argstest::Error::Required); +} + +TEST_CASE("Subparser validation works as expected in noexcept mode", "[args]") +{ + argstest::ArgumentParser p("parser"); + argstest::Command a(p, "a", "command a", [](argstest::Subparser &s) + { + argstest::ValueFlag f(s, "", "", {'f'}, argstest::Options::Required); + s.Parse(); + }); + + argstest::Command b(p, "b", "command b"); + argstest::ValueFlag f(b, "", "", {'f'}, argstest::Options::Required); + + argstest::Command c(p, "c", "command c", [](argstest::Subparser&){}); + + p.ParseArgs(std::vector{}); + REQUIRE(p.GetError() == argstest::Error::None); + + p.ParseArgs(std::vector{"a"}); + REQUIRE((size_t)p.GetError() == (size_t)argstest::Error::Required); + + p.ParseArgs(std::vector{"a", "-f", "F"}); + REQUIRE(p.GetError() == argstest::Error::None); + + p.ParseArgs(std::vector{"b"}); + REQUIRE(p.GetError() == argstest::Error::Required); + + p.ParseArgs(std::vector{"b", "-f", "F"}); + REQUIRE(p.GetError() == argstest::Error::None); + + p.RequireCommand(true); + p.ParseArgs(std::vector{}); + REQUIRE(p.GetError() == argstest::Error::Validation); + + p.RequireCommand(false); + p.ParseArgs(std::vector{}); + REQUIRE(p.GetError() == argstest::Error::None); + + p.ParseArgs(std::vector{"c"}); + REQUIRE(p.GetError() == argstest::Error::Usage); +} + +TEST_CASE("Nargs work as expected in noexcept mode", "[args]") +{ + argstest::ArgumentParser parser("Test command"); + argstest::NargsValueFlag a(parser, "", "", {'a'}, {3, 2}); + + parser.ParseArgs(std::vector{"-a", "1", "2"}); + REQUIRE(parser.GetError() == argstest::Error::Usage); +} -- cgit v1.2.1