diff --git a/include/clara.hpp b/include/clara.hpp index 13a7981..bed3186 100644 --- a/include/clara.hpp +++ b/include/clara.hpp @@ -458,9 +458,25 @@ namespace detail { class ParserBase { public: virtual ~ParserBase() = default; - virtual auto validate() const -> Result { return Result::ok(); } - virtual auto parse( std::string const& exeName, TokenStream const &tokens) const -> InternalParseResult = 0; - virtual auto cardinality() const -> size_t { return 1; } + virtual auto validateSettings() const -> Result { return Result::ok(); } + virtual auto validateFinal() const -> Result { return Result::ok(); } + virtual auto canParse() const -> bool { return false; } + virtual auto internalParse( std::string const& exeName, TokenStream const &tokens ) const->InternalParseResult = 0; + + auto parse( std::string const& exeName, TokenStream const &tokens ) const -> InternalParseResult { + auto validationResult = validateSettings(); + if( !validationResult ) + return InternalParseResult( validationResult ); + + auto result = internalParse( exeName, tokens ); + + // Call this even if parsing failed in order to perform cleanup + validationResult = validateFinal(); + if( result && result.value().type() != ParseResultType::ShortCircuitAll && !validationResult ) + return InternalParseResult( validationResult ); + + return result; + } auto parse( Args const &args ) const -> InternalParseResult { return parse( args.exeName(), TokenStream( args ) ); @@ -482,20 +498,31 @@ namespace detail { std::shared_ptr m_ref; std::string m_hint; std::string m_description; - - explicit ParserRefImpl( std::shared_ptr const &ref ) : m_ref( ref ) {} + mutable std::size_t m_count; + std::string m_default; + bool m_hasDefault; + + explicit ParserRefImpl( std::shared_ptr const &ref ) + : m_ref( ref ), + m_count( 0 ), + m_hasDefault( false ) + {} public: template ParserRefImpl( T &ref, std::string const &hint ) : m_ref( std::make_shared>( ref ) ), - m_hint( hint ) + m_hint( hint ), + m_count( 0 ), + m_hasDefault( false ) {} template ParserRefImpl( LambdaT const &ref, std::string const &hint ) : m_ref( std::make_shared>( ref ) ), - m_hint(hint) + m_hint( hint ), + m_count( 0 ), + m_hasDefault( false ) {} auto operator()( std::string const &description ) -> DerivedT & { @@ -508,6 +535,13 @@ namespace detail { return static_cast( *this ); }; + auto optional( std::string def ) -> DerivedT & { + m_optionality = Optionality::Optional; + m_hasDefault = true; + m_default = std::move( def ); + return static_cast( *this ); + }; + auto required() -> DerivedT & { m_optionality = Optionality::Required; return static_cast( *this ); @@ -517,14 +551,42 @@ namespace detail { return m_optionality == Optionality::Optional; } - auto cardinality() const -> size_t override { + virtual auto cardinality() const -> size_t { if( m_ref->isContainer() ) return 0; else return 1; } + auto validateSettings() const -> Result override { + m_count = 0; + return ComposableParserImpl::validateSettings(); + } + + auto validateFinal() const -> Result override { + if( !isOptional() && count() < 1 ) + return Result::runtimeError( "Missing token: " + hint() ); + if( count() == 0 && m_hasDefault ) + m_ref->setValue( m_default ); + return ComposableParserImpl::validateFinal(); + } + + auto canParse() const -> bool override { + return (cardinality() == 0 || count() < cardinality()); + } + auto hint() const -> std::string { return m_hint; } + + auto description() const -> std::string { + std::string desc = m_description; + if( m_hasDefault ) { + desc += " Default: "; + desc += m_default; + } + return desc; + } + + auto count() const -> std::size_t { return m_count; } }; class ExeName : public ComposableParserImpl { @@ -549,7 +611,7 @@ namespace detail { } // The exe name is not parsed out of the normal tokens, but is handled specially - auto parse( std::string const&, TokenStream const &tokens ) const -> InternalParseResult override { + auto internalParse( std::string const&, TokenStream const &tokens ) const -> InternalParseResult override { return InternalParseResult::ok( ParseState( ParseResultType::NoMatch, tokens ) ); } @@ -573,11 +635,7 @@ namespace detail { public: using ParserRefImpl::ParserRefImpl; - auto parse( std::string const &, TokenStream const &tokens ) const -> InternalParseResult override { - auto validationResult = validate(); - if( !validationResult ) - return InternalParseResult( validationResult ); - + auto internalParse( std::string const &, TokenStream const &tokens ) const -> InternalParseResult override { auto remainingTokens = tokens; auto const &token = *remainingTokens; if( token.type != TokenType::Argument ) @@ -586,8 +644,10 @@ namespace detail { auto result = m_ref->setValue( remainingTokens->token ); if( !result ) return InternalParseResult( result ); - else + else { + ++m_count; return InternalParseResult::ok( ParseState( ParseResultType::Matched, ++remainingTokens ) ); + } } }; @@ -631,9 +691,9 @@ namespace detail { oss << ", "; oss << opt; } - if( !m_hint.empty() ) - oss << " <" << m_hint << ">"; - return { { oss.str(), m_description } }; + if( !hint().empty() ) + oss << " <" << hint() << ">"; + return { { oss.str(), description() } }; } auto isMatch( std::string const &optToken ) const -> bool { @@ -647,22 +707,19 @@ namespace detail { using ParserBase::parse; - auto parse( std::string const&, TokenStream const &tokens ) const -> InternalParseResult override { - auto validationResult = validate(); - if( !validationResult ) - return InternalParseResult( validationResult ); - + auto internalParse( std::string const &, TokenStream const &tokens ) const -> InternalParseResult override { auto remainingTokens = tokens; if( remainingTokens && remainingTokens->type == TokenType::Option ) { auto const &token = *remainingTokens; - if( isMatch(token.token ) ) { + if( isMatch( token.token ) ) { if( m_ref->isFlag() ) { auto result = m_ref->setFlag( true ); if( !result ) return InternalParseResult( result ); if( result.value() == ParseResultType::ShortCircuitAll ) return InternalParseResult::ok( ParseState( result.value(), remainingTokens ) ); - } else { + } + else { ++remainingTokens; if( !remainingTokens ) return InternalParseResult::runtimeError( "Expected argument following " + token.token ); @@ -675,13 +732,14 @@ namespace detail { if( result.value() == ParseResultType::ShortCircuitAll ) return InternalParseResult::ok( ParseState( result.value(), remainingTokens ) ); } + ++m_count; return InternalParseResult::ok( ParseState( ParseResultType::Matched, ++remainingTokens ) ); } } return InternalParseResult::ok( ParseState( ParseResultType::NoMatch, remainingTokens ) ); } - auto validate() const -> Result override { + auto validateSettings() const -> Result override { if( m_optNames.empty() ) return Result::logicError( "No options supplied to Opt" ); for( auto const &name : m_optNames ) { @@ -695,7 +753,7 @@ namespace detail { return Result::logicError( "Option name must begin with '-'" ); #endif } - return ParserRefImpl::validate(); + return ParserRefImpl::validateSettings(); } }; @@ -801,37 +859,46 @@ namespace detail { return os; } - auto validate() const -> Result override { + auto validateSettings() const -> Result override { for( auto const &opt : m_options ) { - auto result = opt.validate(); + auto result = opt.validateSettings(); if( !result ) return result; } for( auto const &arg : m_args ) { - auto result = arg.validate(); + auto result = arg.validateSettings(); if( !result ) return result; } return Result::ok(); } - using ParserBase::parse; + auto validateFinal() const -> Result override { + for( auto const &opt : m_options ) { + auto result = opt.validateFinal(); + if( !result ) + return result; + } + for( auto const &arg : m_args ) { + auto result = arg.validateFinal(); + if( !result ) + return result; + } + return Result::ok(); + } - auto parse( std::string const& exeName, TokenStream const &tokens ) const -> InternalParseResult override { + using ParserBase::parse; - struct ParserInfo { - ParserBase const* parser = nullptr; - size_t count = 0; - }; + auto internalParse( std::string const& exeName, TokenStream const &tokens ) const -> InternalParseResult override { const size_t totalParsers = m_options.size() + m_args.size(); assert( totalParsers < 512 ); // ParserInfo parseInfos[totalParsers]; // <-- this is what we really want to do - ParserInfo parseInfos[512]; + ParserBase const* parsers[512]; { size_t i = 0; - for (auto const &opt : m_options) parseInfos[i++].parser = &opt; - for (auto const &arg : m_args) parseInfos[i++].parser = &arg; + for (auto const &opt : m_options) parsers[i++] = &opt; + for (auto const &arg : m_args) parsers[i++] = &arg; } m_exeName.set( exeName ); @@ -841,14 +908,13 @@ namespace detail { bool tokenParsed = false; for( size_t i = 0; i < totalParsers; ++i ) { - auto& parseInfo = parseInfos[i]; - if( parseInfo.parser->cardinality() == 0 || parseInfo.count < parseInfo.parser->cardinality() ) { - result = parseInfo.parser->parse(exeName, result.value().remainingTokens()); + auto& parser = parsers[i]; + if( parser->canParse() ) { + result = parser->internalParse(exeName, result.value().remainingTokens()); if (!result) return result; if (result.value().type() != ParseResultType::NoMatch) { tokenParsed = true; - ++parseInfo.count; break; } } @@ -859,7 +925,6 @@ namespace detail { if( !tokenParsed ) return InternalParseResult::runtimeError( "Unrecognised token: " + result.value().remainingTokens()->token ); } - // !TBD Check missing required options return result; } }; diff --git a/single_include/clara.hpp b/single_include/clara.hpp index aa429e7..869eb92 100644 --- a/single_include/clara.hpp +++ b/single_include/clara.hpp @@ -789,9 +789,25 @@ namespace detail { class ParserBase { public: virtual ~ParserBase() = default; - virtual auto validate() const -> Result { return Result::ok(); } - virtual auto parse( std::string const& exeName, TokenStream const &tokens) const -> InternalParseResult = 0; - virtual auto cardinality() const -> size_t { return 1; } + virtual auto validateSettings() const -> Result { return Result::ok(); } + virtual auto validateFinal() const -> Result { return Result::ok(); } + virtual auto canParse() const -> bool { return false; } + virtual auto internalParse( std::string const& exeName, TokenStream const &tokens ) const->InternalParseResult = 0; + + auto parse( std::string const& exeName, TokenStream const &tokens ) const -> InternalParseResult { + auto validationResult = validateSettings(); + if( !validationResult ) + return InternalParseResult( validationResult ); + + auto result = internalParse( exeName, tokens ); + + // Call this even if parsing failed in order to perform cleanup + validationResult = validateFinal(); + if( result && result.value().type() != ParseResultType::ShortCircuitAll && !validationResult ) + return InternalParseResult( validationResult ); + + return result; + } auto parse( Args const &args ) const -> InternalParseResult { return parse( args.exeName(), TokenStream( args ) ); @@ -813,20 +829,26 @@ namespace detail { std::shared_ptr m_ref; std::string m_hint; std::string m_description; + mutable std::size_t m_count; - explicit ParserRefImpl( std::shared_ptr const &ref ) : m_ref( ref ) {} + explicit ParserRefImpl( std::shared_ptr const &ref ) + : m_ref( ref ), + m_count( 0 ) + {} public: template ParserRefImpl( T &ref, std::string const &hint ) : m_ref( std::make_shared>( ref ) ), - m_hint( hint ) + m_hint( hint ), + m_count( 0 ) {} template ParserRefImpl( LambdaT const &ref, std::string const &hint ) : m_ref( std::make_shared>( ref ) ), - m_hint(hint) + m_hint( hint ), + m_count( 0 ) {} auto operator()( std::string const &description ) -> DerivedT & { @@ -848,14 +870,27 @@ namespace detail { return m_optionality == Optionality::Optional; } - auto cardinality() const -> size_t override { + virtual auto cardinality() const -> size_t { if( m_ref->isContainer() ) return 0; else return 1; } + auto validateFinal() const -> Result override { + if( !isOptional() && count() < 1 ) + return Result::runtimeError( "Missing token: " + hint() ); + m_count = 0; + return ComposableParserImpl::validateFinal(); + } + + auto canParse() const -> bool override { + return (cardinality() == 0 || count() < cardinality()); + } + auto hint() const -> std::string { return m_hint; } + + auto count() const -> std::size_t { return m_count; } }; class ExeName : public ComposableParserImpl { @@ -880,7 +915,7 @@ namespace detail { } // The exe name is not parsed out of the normal tokens, but is handled specially - auto parse( std::string const&, TokenStream const &tokens ) const -> InternalParseResult override { + auto internalParse( std::string const&, TokenStream const &tokens ) const -> InternalParseResult override { return InternalParseResult::ok( ParseState( ParseResultType::NoMatch, tokens ) ); } @@ -904,11 +939,7 @@ namespace detail { public: using ParserRefImpl::ParserRefImpl; - auto parse( std::string const &, TokenStream const &tokens ) const -> InternalParseResult override { - auto validationResult = validate(); - if( !validationResult ) - return InternalParseResult( validationResult ); - + auto internalParse( std::string const &, TokenStream const &tokens ) const -> InternalParseResult override { auto remainingTokens = tokens; auto const &token = *remainingTokens; if( token.type != TokenType::Argument ) @@ -917,8 +948,10 @@ namespace detail { auto result = m_ref->setValue( remainingTokens->token ); if( !result ) return InternalParseResult( result ); - else + else { + ++m_count; return InternalParseResult::ok( ParseState( ParseResultType::Matched, ++remainingTokens ) ); + } } }; @@ -978,22 +1011,19 @@ namespace detail { using ParserBase::parse; - auto parse( std::string const&, TokenStream const &tokens ) const -> InternalParseResult override { - auto validationResult = validate(); - if( !validationResult ) - return InternalParseResult( validationResult ); - + auto internalParse( std::string const &, TokenStream const &tokens ) const -> InternalParseResult override { auto remainingTokens = tokens; if( remainingTokens && remainingTokens->type == TokenType::Option ) { auto const &token = *remainingTokens; - if( isMatch(token.token ) ) { + if( isMatch( token.token ) ) { if( m_ref->isFlag() ) { auto result = m_ref->setFlag( true ); if( !result ) return InternalParseResult( result ); if( result.value() == ParseResultType::ShortCircuitAll ) return InternalParseResult::ok( ParseState( result.value(), remainingTokens ) ); - } else { + } + else { ++remainingTokens; if( !remainingTokens ) return InternalParseResult::runtimeError( "Expected argument following " + token.token ); @@ -1006,13 +1036,14 @@ namespace detail { if( result.value() == ParseResultType::ShortCircuitAll ) return InternalParseResult::ok( ParseState( result.value(), remainingTokens ) ); } + ++m_count; return InternalParseResult::ok( ParseState( ParseResultType::Matched, ++remainingTokens ) ); } } return InternalParseResult::ok( ParseState( ParseResultType::NoMatch, remainingTokens ) ); } - auto validate() const -> Result override { + auto validateSettings() const -> Result override { if( m_optNames.empty() ) return Result::logicError( "No options supplied to Opt" ); for( auto const &name : m_optNames ) { @@ -1026,7 +1057,7 @@ namespace detail { return Result::logicError( "Option name must begin with '-'" ); #endif } - return ParserRefImpl::validate(); + return ParserRefImpl::validateSettings(); } }; @@ -1130,37 +1161,46 @@ namespace detail { return os; } - auto validate() const -> Result override { + auto validateSettings() const -> Result override { for( auto const &opt : m_options ) { - auto result = opt.validate(); + auto result = opt.validateSettings(); if( !result ) return result; } for( auto const &arg : m_args ) { - auto result = arg.validate(); + auto result = arg.validateSettings(); if( !result ) return result; } return Result::ok(); } - using ParserBase::parse; + auto validateFinal() const -> Result override { + for( auto const &opt : m_options ) { + auto result = opt.validateFinal(); + if( !result ) + return result; + } + for( auto const &arg : m_args ) { + auto result = arg.validateFinal(); + if( !result ) + return result; + } + return Result::ok(); + } - auto parse( std::string const& exeName, TokenStream const &tokens ) const -> InternalParseResult override { + using ParserBase::parse; - struct ParserInfo { - ParserBase const* parser = nullptr; - size_t count = 0; - }; + auto internalParse( std::string const& exeName, TokenStream const &tokens ) const -> InternalParseResult override { const size_t totalParsers = m_options.size() + m_args.size(); assert( totalParsers < 512 ); // ParserInfo parseInfos[totalParsers]; // <-- this is what we really want to do - ParserInfo parseInfos[512]; + ParserBase const* parsers[512]; { size_t i = 0; - for (auto const &opt : m_options) parseInfos[i++].parser = &opt; - for (auto const &arg : m_args) parseInfos[i++].parser = &arg; + for (auto const &opt : m_options) parsers[i++] = &opt; + for (auto const &arg : m_args) parsers[i++] = &arg; } m_exeName.set( exeName ); @@ -1170,14 +1210,13 @@ namespace detail { bool tokenParsed = false; for( size_t i = 0; i < totalParsers; ++i ) { - auto& parseInfo = parseInfos[i]; - if( parseInfo.parser->cardinality() == 0 || parseInfo.count < parseInfo.parser->cardinality() ) { - result = parseInfo.parser->parse(exeName, result.value().remainingTokens()); + auto& parser = parsers[i]; + if( parser->canParse() ) { + result = parser->internalParse(exeName, result.value().remainingTokens()); if (!result) return result; if (result.value().type() != ParseResultType::NoMatch) { tokenParsed = true; - ++parseInfo.count; break; } } @@ -1188,7 +1227,6 @@ namespace detail { if( !tokenParsed ) return InternalParseResult::runtimeError( "Unrecognised token: " + result.value().remainingTokens()->token ); } - // !TBD Check missing required options return result; } }; diff --git a/src/ClaraTests.cpp b/src/ClaraTests.cpp index f0f1af7..daaa73b 100644 --- a/src/ClaraTests.cpp +++ b/src/ClaraTests.cpp @@ -49,27 +49,37 @@ TEST_CASE( "single parsers" ) { SECTION( "-n" ) { p.parse( Args{ "TestApp", "-n", "Vader" } ); - REQUIRE( name == "Vader"); + REQUIRE( name == "Vader" ); } SECTION( "--name" ) { p.parse( Args{ "TestApp", "--name", "Vader" } ); - REQUIRE( name == "Vader"); + REQUIRE( name == "Vader" ); } SECTION( "-n:" ) { p.parse( Args{ "TestApp", "-n:Vader" } ); - REQUIRE( name == "Vader"); + REQUIRE( name == "Vader" ); } SECTION( "-n=" ) { p.parse( Args{ "TestApp", "-n=Vader" } ); - REQUIRE( name == "Vader"); + REQUIRE( name == "Vader" ); } SECTION( "no args" ) { p.parse( Args{ "TestApp" } ); - REQUIRE( name == ""); + REQUIRE( name == "" ); } SECTION( "different args" ) { p.parse( Args{ "TestApp", "-f" } ); - REQUIRE( name == ""); + REQUIRE( name == "" ); + } + SECTION( "default value" ) { + p.optional( "Vader" ); + p.parse( Args{ "TestApp" } ); + REQUIRE( name == "Vader" ); + } + SECTION( "set and default value" ) { + p.optional( "Darth" ); + p.parse( Args{ "TestApp", "-n", "Vader" } ); + REQUIRE( name == "Vader" ); } } @@ -100,6 +110,7 @@ TEST_CASE( "Combined parser" ) { | Opt( [&]( double value ){ config.m_value = value; }, "number" ) ["-d"]["--double"] ( "just some number" ) + .optional( "1123.58" ) | Arg( config.m_tests, "test name|tags|pattern" ) ( "which test or tests to use" ); @@ -116,11 +127,11 @@ TEST_CASE( "Combined parser" ) { " --rng-seed, -r set a specific seed for random numbers\n" " -n, --name the name to use\n" " -f, --flag a flag to set\n" - " -d, --double just some number\n" + " -d, --double just some number Default: 1123.58\n" ); } SECTION( "some args" ) { - auto result = parser.parse( Args{ "TestApp", "-n", "Bill", "-d:123.45", "-f", "test1", "test2" } ); + auto result = parser.parse( Args{ "TestApp", "-r", "42", "-n", "Bill", "-d:123.45", "-f", "test1", "test2" } ); CHECK( result ); CHECK( result.value().type() == ParseResultType::Matched ); @@ -129,6 +140,13 @@ TEST_CASE( "Combined parser" ) { REQUIRE( config.m_tests == std::vector { "test1", "test2" } ); CHECK( showHelp == false ); } + SECTION( "missing required" ) { + using namespace Catch::Matchers; + + auto result = parser.parse( Args{ "TestApp", "-n", "Bill", "-d:123.45", "-f", "test1", "test2" } ); + CHECK( !result ); + CHECK_THAT( result.errorMessage(), Contains( "Missing token" ) && Contains( "time|value" ) ); + } SECTION( "help" ) { auto result = parser.parse( Args{ "TestApp", "-?", "-n:NotSet" } ); CHECK( result );