From 26ae11774b66ec849b4f8ef8f34fcb4e9c56ca66 Mon Sep 17 00:00:00 2001 From: Phil Nash Date: Sat, 27 Apr 2013 12:26:13 +0100 Subject: [PATCH] First draft of future opt parser lib --- .gitignore | 1 + projects/SelfTest/CmdLineTests.cpp | 313 ++++++++++++++++++ .../CatchSelfTest.xcodeproj/project.pbxproj | 12 + 3 files changed, 326 insertions(+) create mode 100644 projects/SelfTest/CmdLineTests.cpp diff --git a/.gitignore b/.gitignore index 76fc19e1..986f75d1 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ projects/XCode4/iOSTest/Build/Products/Debug-iphonesimulator/iOSTest.app.dSYM/Co projects/XCode4/iOSTest/Build projects/XCode4/CatchSelfTest/DerivedData projects/XCode4/OCTest/DerivedData +*.pyc diff --git a/projects/SelfTest/CmdLineTests.cpp b/projects/SelfTest/CmdLineTests.cpp new file mode 100644 index 00000000..9952f36c --- /dev/null +++ b/projects/SelfTest/CmdLineTests.cpp @@ -0,0 +1,313 @@ +/* + * Created by Phil on 22/10/2010. + * Copyright 2010 Two Blue Cubes Ltd + * + * Distributed under the Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + */ +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wpadded" +#endif + +#include "catch.hpp" +//#include "catch_ptr.hpp" + +namespace Catch { + +class ArgData { +public: + ArgData( std::string const& _arg ) : m_weight( 2 ) + { + std::size_t first = _arg.find( '<' ); + std::size_t last = _arg.find_last_of( '>' ); + if( first == std::string::npos || last == std::string::npos || last <= first+1 ) + throw std::logic_error( "Argument must contain a name in angle brackets but it was: " + _arg ); + m_prefix = _arg.substr( 0, first ); + m_postfix = _arg.substr( last+1 ); + m_name = _arg.substr( first+1, last-first-1 ); + if( !m_prefix.empty() ) + --m_weight; + if( !m_postfix.empty() ) + --m_weight; + } + std::string const& name() const { return m_name; } + std::string const& prefix() const { return m_prefix; } + std::string const& postfix() const { return m_postfix; } + + bool isMatch( std::string const& _arg ) const { + return startsWith( _arg, m_prefix ) && endsWith( _arg, m_postfix ); + } + std::string strip( std::string const& _arg ) const { + return _arg.substr( m_prefix.size(), + _arg.size() - m_prefix.size() - m_postfix.size() ); + } + bool operator < ( ArgData const& _other ) const { + return m_weight < _other.m_weight; + } +protected: + std::string m_prefix; + std::string m_postfix; + std::string m_name; + int m_weight; +}; + +template +class Opt { +public: + Opt( std::string const& _synposis ) : m_synopsis( _synposis ) {} + Opt& shortOpt( std::string const& _value ) { m_shortOpt = _value; return *this; } + Opt& longOpt( std::string const& _value ) { m_longOpt = _value; return *this; } + + template + Opt& addArg( std::string const& _name, M const& _member ){ + m_args.push_back( Arg( _name, _member ) ); + return *this; + } + + std::string synopsis() const { return m_synopsis; } + std::string shortOpt() const { return m_shortOpt; } + std::string longOpt() const { return m_longOpt; } + + bool parseInto( std::string const& _arg, T& _config ) const { + ensureWeightedArgsAreSorted(); + typename std::vector::const_iterator + it = m_argsInWeightedOrder.begin(), + itEnd = m_argsInWeightedOrder.end(); + for( ; it != itEnd; ++it ) + if( (*it)->isMatch( _arg ) ) { + (*it)->parseInto( _arg, _config ); + return true; + } + return false; + } + +private: + + struct IField : SharedImpl<> { + virtual ~IField() {} + virtual bool parseInto( std::string const& _arg, T& _config ) const = 0; + }; + template + struct Field : IField { + Field( M const& _member ) : member( _member ) {} + bool parseInto( std::string const& _arg, T& _config ) const { + std::stringstream ss; + ss << _arg; + ss >> _config.*member; + return !ss.fail(); + } + M member; + }; + class Arg : public ArgData { + public: + Arg() : m_field( NULL ) {} + template + Arg( std::string const& _name, M const& _member ) + : ArgData( _name ), + m_field( new Field( _member ) ) + {} + void parseInto( std::string const& _arg, T& _config ) const { + if( !m_field->parseInto( strip( _arg ), _config ) ) + throw std::domain_error( "'" + _arg + "' was not valid for <" + m_name + ">" ); + } + + private: + Ptr m_field; + }; + + static bool argLess( Arg const* lhs, Arg const* rhs ) { + return *lhs < *rhs; + } + void ensureWeightedArgsAreSorted() const { + if( m_args.size() > m_argsInWeightedOrder.size() ) { + m_argsInWeightedOrder.clear(); + typename std::vector::const_iterator it = m_args.begin(), + itEnd = m_args.end(); + for( ; it != itEnd; ++it ) + m_argsInWeightedOrder.push_back( &*it ); + sort( m_argsInWeightedOrder.begin(), m_argsInWeightedOrder.end(), &Opt::argLess ); + } + } + std::string m_synopsis; + std::string m_shortOpt; + std::string m_longOpt; + std::vector m_args; + mutable std::vector m_argsInWeightedOrder; +}; + +template +class CommandLineParser +{ +public: + void addOption( Opt const& _opt ) { + m_allOptionParsers.push_back( _opt ); + if( !_opt.shortOpt().empty() ) + m_optionsByName.insert( std::make_pair( "-" + _opt.shortOpt(), &m_allOptionParsers.back() ) ); + if( !_opt.longOpt().empty() ) + m_optionsByName.insert( std::make_pair( "--" + _opt.longOpt(), &m_allOptionParsers.back() ) ); + } + bool parseArgs( int argc, const char* const argv[], T& config ) { + for( int i = 0; i < argc; ++i ) { + std::string fullArg = argv[i]; + if( fullArg[0] == '-' ) { + std::string args, optName; + std::size_t pos = fullArg.find( ':' ); + if( pos == std::string::npos ) { + optName = fullArg; + } + else { + optName = fullArg.substr(0, pos ); + args = fullArg.substr( pos+1 ); + } + typename std::map*>::const_iterator it = m_optionsByName.find( optName ); + bool used = false; + if( it != m_optionsByName.end() ) { + try { + used = it->second->parseInto( args, config ); + } + catch( std::exception& ex ) { + throw std::domain_error( "Error in " + optName + " option: " + ex.what() ); + } + } + if( !used ) + m_unusedOpts.push_back( fullArg ); + } + else { + m_args.push_back( fullArg ); + } + } + return false; + } + +private: + std::vector > m_allOptionParsers; + std::map*> m_optionsByName; + std::vector m_args; + std::vector m_unusedOpts; +}; + + +} // end namespace Catch + +struct TestOpt { + TestOpt() : number( 0 ) {} + + std::string fileName; + std::string streamName; + int number; +}; + +TEST_CASE( "Arg" ) { + SECTION( "pre and post" ) { + Catch::ArgData preAndPost( "prefixpostfix" ); + CHECK( preAndPost.prefix() == "prefix" ); + CHECK( preAndPost.postfix() == "postfix" ); + CHECK( preAndPost.name() == "arg" ); + + CHECK( preAndPost.isMatch( "prefixpayloadpostfix" ) ); + CHECK( preAndPost.strip( "prefixpayloadpostfix" ) == "payload" ); + CHECK_FALSE( preAndPost.isMatch( "payload" ) ); + CHECK_FALSE( preAndPost.isMatch( "postfixpayloadpostfix" ) ); + CHECK_FALSE( preAndPost.isMatch( "prefixpayloadpostfixx" ) ); + } + SECTION( "pre" ) { + Catch::ArgData preAndPost( "prefix" ); + CHECK( preAndPost.prefix() == "prefix" ); + CHECK( preAndPost.postfix() == "" ); + CHECK( preAndPost.name() == "arg" ); + + CHECK( preAndPost.isMatch( "prefixpayload" ) ); + CHECK( preAndPost.strip( "prefixpayload" ) == "payload" ); + CHECK_FALSE( preAndPost.isMatch( "payload" ) ); + CHECK_FALSE( preAndPost.isMatch( "postfixpayload" ) ); + } + SECTION( "post" ) { + Catch::ArgData preAndPost( "postfix" ); + CHECK( preAndPost.prefix() == "" ); + CHECK( preAndPost.postfix() == "postfix" ); + CHECK( preAndPost.name() == "arg" ); + + CHECK( preAndPost.isMatch( "payloadpostfix" ) ); + CHECK( preAndPost.strip( "payloadpostfix" ) == "payload" ); + CHECK_FALSE( preAndPost.isMatch( "payload" ) ); + CHECK_FALSE( preAndPost.isMatch( "payloadpostfixx" ) ); + } + SECTION( "none" ) { + Catch::ArgData preAndPost( "" ); + CHECK( preAndPost.prefix() == "" ); + CHECK( preAndPost.postfix() == "" ); + CHECK( preAndPost.name() == "arg" ); + + CHECK( preAndPost.isMatch( "payload" ) ); + CHECK( preAndPost.strip( "payload" ) == "payload" ); + } + SECTION( "errors" ) { + CHECK_THROWS( Catch::ArgData( "" ) ); + CHECK_THROWS( Catch::ArgData( "no brackets" ) ); + CHECK_THROWS( Catch::ArgData( "" ) ); + CHECK_THROWS( Catch::ArgData( "><" ) ); + CHECK_THROWS( Catch::ArgData( "<>" ) ); + } +} + +TEST_CASE( "cmdline", "" ) { + + Catch::Opt opt( "specifies output file" ); + opt.shortOpt( "o" ) + .longOpt( "output" ) + .addArg( "", &TestOpt::fileName ) + .addArg( "%", &TestOpt::streamName ); + + TestOpt config; + Catch::CommandLineParser parser; + parser.addOption( opt ); + +// parser.addOption( "specifies output file" ) +// .shortOpt( "o" ) +// .longOpt( "output" ) +// .addArg( "", &TestOpt::fileName ) +// .addArg( "%", &TestOpt::streamName ); + + SECTION( "plain filename" ) { + const char* argv[] = { "test", "-o:filename.ext" }; + + parser.parseArgs( sizeof(argv)/sizeof(char*), argv, config ); + CHECK( config.fileName == "filename.ext" ); + CHECK( config.streamName == "" ); + } + SECTION( "stream name" ) { + const char* argv[] = { "test", "-o:%stdout" }; + + parser.parseArgs( sizeof(argv)/sizeof(char*), argv, config ); + CHECK( config.fileName == "" ); + CHECK( config.streamName == "stdout" ); + } + SECTION( "long opt" ) { + const char* argv[] = { "test", "--output:%stdout" }; + + parser.parseArgs( sizeof(argv)/sizeof(char*), argv, config ); + CHECK( config.fileName == "" ); + CHECK( config.streamName == "stdout" ); + } + + Catch::Opt opt2( "a number" ); + opt2.shortOpt( "n" ) + .addArg( "", &TestOpt::number ); + + parser.addOption( opt2 ); + + SECTION( "a number" ) { + const char* argv[] = { "test", "-n:42" }; + + parser.parseArgs( sizeof(argv)/sizeof(char*), argv, config ); + CHECK( config.number == 42 ); + } + SECTION( "not a number" ) { + const char* argv[] = { "test", "-n:forty-two" }; + + CHECK_THROWS( parser.parseArgs( sizeof(argv)/sizeof(char*), argv, config ) ); + CHECK( config.number == 0 ); + } + +} diff --git a/projects/XCode4/CatchSelfTest/CatchSelfTest.xcodeproj/project.pbxproj b/projects/XCode4/CatchSelfTest/CatchSelfTest.xcodeproj/project.pbxproj index 748d288c..c178f5ec 100644 --- a/projects/XCode4/CatchSelfTest/CatchSelfTest.xcodeproj/project.pbxproj +++ b/projects/XCode4/CatchSelfTest/CatchSelfTest.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 266B06B816F3A60A004ED264 /* VariadicMacrosTests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 266B06B616F3A60A004ED264 /* VariadicMacrosTests.cpp */; }; + 266E9AD617290E8E0061DAB2 /* CmdLineTests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 266E9AD417290E8E0061DAB2 /* CmdLineTests.cpp */; }; 266ECD74170F3C620030D735 /* BDDTests.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 266ECD73170F3C620030D735 /* BDDTests.cpp */; }; 26847E5F16BBADB40043B9C1 /* catch_message.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 26847E5D16BBADB40043B9C1 /* catch_message.cpp */; }; 2694A1FD16A0000E004816E3 /* catch_text.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2694A1FB16A0000E004816E3 /* catch_text.cpp */; }; @@ -57,6 +58,7 @@ /* Begin PBXFileReference section */ 266B06B616F3A60A004ED264 /* VariadicMacrosTests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = VariadicMacrosTests.cpp; path = ../../../SelfTest/VariadicMacrosTests.cpp; sourceTree = ""; }; 266E9AD117230ACF0061DAB2 /* catch_text.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = catch_text.hpp; sourceTree = ""; }; + 266E9AD417290E8E0061DAB2 /* CmdLineTests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CmdLineTests.cpp; path = ../../../SelfTest/CmdLineTests.cpp; sourceTree = ""; }; 266ECD73170F3C620030D735 /* BDDTests.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = BDDTests.cpp; path = ../../../SelfTest/BDDTests.cpp; sourceTree = ""; }; 266ECD8C1713614B0030D735 /* catch_legacy_reporter_adapter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = catch_legacy_reporter_adapter.hpp; sourceTree = ""; }; 266ECD8D1713614B0030D735 /* catch_legacy_reporter_adapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = catch_legacy_reporter_adapter.h; sourceTree = ""; }; @@ -176,6 +178,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 266E9AD317290E710061DAB2 /* Introspective Tests */ = { + isa = PBXGroup; + children = ( + 266E9AD417290E8E0061DAB2 /* CmdLineTests.cpp */, + ); + name = "Introspective Tests"; + sourceTree = ""; + }; 4A6D0C15149B3D3B00DB3EAA = { isa = PBXGroup; children = ( @@ -198,6 +208,7 @@ 4A6D0C35149B3D9E00DB3EAA /* TestMain.cpp */, 4A6D0C2E149B3D9E00DB3EAA /* catch_self_test.hpp */, 4AE1840A14EE4F230066340D /* catch_self_test.cpp */, + 266E9AD317290E710061DAB2 /* Introspective Tests */, 4A6D0C40149B3DAB00DB3EAA /* Tests */, 4A6D0C41149B3DE900DB3EAA /* Catch */, 4A6D0C26149B3D3B00DB3EAA /* CatchSelfTest.1 */, @@ -493,6 +504,7 @@ 26847E5F16BBADB40043B9C1 /* catch_message.cpp in Sources */, 266B06B816F3A60A004ED264 /* VariadicMacrosTests.cpp in Sources */, 266ECD74170F3C620030D735 /* BDDTests.cpp in Sources */, + 266E9AD617290E8E0061DAB2 /* CmdLineTests.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };