From f28af2623b22115f253784f0c178e46fa9aa1264 Mon Sep 17 00:00:00 2001 From: Hongyuan Liu Date: Sat, 1 Jun 2019 00:56:33 -0400 Subject: [PATCH] Issue #93: Fix required option When the option is not provided in the commmand line, it raises an error. The according tests are also added. --- include/clara.hpp | 43 +++++++++++++++++++++++++++++++++++++++++-- src/ClaraTests.cpp | 22 +++++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/include/clara.hpp b/include/clara.hpp index 51be3e3..571f921 100644 --- a/include/clara.hpp +++ b/include/clara.hpp @@ -37,6 +37,7 @@ #include #include + #if !defined(CLARA_PLATFORM_WINDOWS) && ( defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) ) #define CLARA_PLATFORM_WINDOWS #endif @@ -651,6 +652,14 @@ namespace detail { return { { oss.str(), m_description } }; } + auto getOptNames() const -> std::string { + std::string optNames = ""; + for (auto const &name : m_optNames) { + optNames.append(name + ";"); + } + return optNames; + } + auto isMatch( std::string const &optToken ) const -> bool { auto normalisedToken = normaliseOpt( optToken ); for( auto const &name : m_optNames ) { @@ -667,6 +676,20 @@ namespace detail { if( !validationResult ) return InternalParseResult( validationResult ); + if (!isOptional()) { + auto remainingTokens = tokens; + auto requiredOptionMatched = false; + while (remainingTokens) { + if (isMatch(remainingTokens->token)) { + requiredOptionMatched = true; + } + remainingTokens = ++remainingTokens; + } + if (!requiredOptionMatched) { + return InternalParseResult::runtimeError( "The required option " + getOptNames() + " is not provided. "); + } + } + auto remainingTokens = tokens; if( remainingTokens && remainingTokens->type == TokenType::Option ) { auto const &token = *remainingTokens; @@ -841,7 +864,6 @@ namespace detail { using ParserBase::parse; auto parse( std::string const& exeName, TokenStream const &tokens ) const -> InternalParseResult override { - struct ParserInfo { ParserBase const* parser = nullptr; size_t count = 0; @@ -859,6 +881,7 @@ namespace detail { m_exeName.set( exeName ); + std::set tokenList; auto result = InternalParseResult::ok( ParseState( ParseResultType::NoMatch, tokens ) ); while( result.value().remainingTokens() ) { bool tokenParsed = false; @@ -866,6 +889,7 @@ namespace detail { for( size_t i = 0; i < totalParsers; ++i ) { auto& parseInfo = parseInfos[i]; if( parseInfo.parser->cardinality() == 0 || parseInfo.count < parseInfo.parser->cardinality() ) { + tokenList.insert(result.value().remainingTokens()->token); result = parseInfo.parser->parse(exeName, result.value().remainingTokens()); if (!result) return result; @@ -882,7 +906,22 @@ namespace detail { if( !tokenParsed ) return InternalParseResult::runtimeError( "Unrecognised token: " + result.value().remainingTokens()->token ); } - // !TBD Check missing required options + + for (auto const &opt : m_options) { + if (!opt.isOptional()) { + auto matched = false; + for (auto const &token : tokenList) { + if (opt.isMatch(token)) { + matched = true; + break; + } + } + if (!matched) { + return InternalParseResult::runtimeError( "The required option " + opt.getOptNames() + " is not provided. "); + } + } + } + return result; } }; diff --git a/src/ClaraTests.cpp b/src/ClaraTests.cpp index 1251e9b..b3a8ca0 100644 --- a/src/ClaraTests.cpp +++ b/src/ClaraTests.cpp @@ -128,7 +128,7 @@ TEST_CASE( "Combined parser" ) { ); } SECTION( "some args" ) { - auto result = parser.parse( Args{ "TestApp", "-n", "Bill", "-d:123.45", "-f", "test1", "test2" } ); + auto result = parser.parse( Args{ "TestApp", "-n", "Bill", "-d:123.45", "-f", "test1", "test2", "-r", "42" } ); CHECK( result ); CHECK( result.value().type() == ParseResultType::Matched ); @@ -395,6 +395,26 @@ TEST_CASE( "Invalid parsers" ) CHECK( !result ); CHECK_THAT( result.errorMessage(), StartsWith( "Option name must begin with '-'" ) ); } + SECTION( "no required option 1" ) { + bool showHelp = false; + auto cli = Help( showHelp ) | Opt( config.number, "number" )["-n"]["--number"].required(); + auto result = cli.parse( { "TestApp" } ); + CHECK( !result ); + CHECK_THAT( result.errorMessage(), StartsWith("The required option") ); + } + SECTION( "no required option 2" ) { + auto cli = Opt( config.number, "number" )["-n"]["--number"].required(); + auto result = cli.parse( { "TestApp" } ); + CHECK( !result ); + CHECK_THAT( result.errorMessage(), StartsWith("The required option") ); + } + SECTION( "no required option 3" ) { + auto rseed = -1; + auto cli = Opt( config.number, "number" )["-n"]["--number"].required() | Opt( rseed, "rseed" )["-r"]["--rseed"].required(); + auto result = cli.parse( { "TestApp", "-n", "999"} ); + CHECK( !result ); + CHECK_THAT( result.errorMessage(), StartsWith("The required option") ); + } } TEST_CASE( "Multiple flags" ) {