catch_clara.cpp
1 2 // Copyright Catch2 Authors 3 // Distributed under the Boost Software License, Version 1.0. 4 // (See accompanying file LICENSE.txt or copy at 5 // https://www.boost.org/LICENSE_1_0.txt) 6 7 // SPDX-License-Identifier: BSL-1.0 8 9 #include <catch2/internal/catch_clara.hpp> 10 #include <catch2/internal/catch_console_width.hpp> 11 #include <catch2/internal/catch_platform.hpp> 12 #include <catch2/internal/catch_string_manip.hpp> 13 #include <catch2/internal/catch_textflow.hpp> 14 15 #include <algorithm> 16 #include <ostream> 17 18 namespace { 19 bool isOptPrefix( char c ) { 20 return c == '-' 21 #ifdef CATCH_PLATFORM_WINDOWS 22 || c == '/' 23 #endif 24 ; 25 } 26 27 std::string normaliseOpt( std::string const& optName ) { 28 #ifdef CATCH_PLATFORM_WINDOWS 29 if ( optName[0] == '/' ) 30 return "-" + optName.substr( 1 ); 31 else 32 #endif 33 return optName; 34 } 35 36 } // namespace 37 38 namespace Catch { 39 namespace Clara { 40 namespace Detail { 41 42 void TokenStream::loadBuffer() { 43 m_tokenBuffer.clear(); 44 45 // Skip any empty strings 46 while ( it != itEnd && it->empty() ) { 47 ++it; 48 } 49 50 if ( it != itEnd ) { 51 auto const& next = *it; 52 if ( isOptPrefix( next[0] ) ) { 53 auto delimiterPos = next.find_first_of( " :=" ); 54 if ( delimiterPos != std::string::npos ) { 55 m_tokenBuffer.push_back( 56 { TokenType::Option, 57 next.substr( 0, delimiterPos ) } ); 58 m_tokenBuffer.push_back( 59 { TokenType::Argument, 60 next.substr( delimiterPos + 1 ) } ); 61 } else { 62 if ( next[1] != '-' && next.size() > 2 ) { 63 std::string opt = "- "; 64 for ( size_t i = 1; i < next.size(); ++i ) { 65 opt[1] = next[i]; 66 m_tokenBuffer.push_back( 67 { TokenType::Option, opt } ); 68 } 69 } else { 70 m_tokenBuffer.push_back( 71 { TokenType::Option, next } ); 72 } 73 } 74 } else { 75 m_tokenBuffer.push_back( 76 { TokenType::Argument, next } ); 77 } 78 } 79 } 80 81 TokenStream::TokenStream( Args const& args ): 82 TokenStream( args.m_args.begin(), args.m_args.end() ) {} 83 84 TokenStream::TokenStream( Iterator it_, Iterator itEnd_ ): 85 it( it_ ), itEnd( itEnd_ ) { 86 loadBuffer(); 87 } 88 89 TokenStream& TokenStream::operator++() { 90 if ( m_tokenBuffer.size() >= 2 ) { 91 m_tokenBuffer.erase( m_tokenBuffer.begin() ); 92 } else { 93 if ( it != itEnd ) 94 ++it; 95 loadBuffer(); 96 } 97 return *this; 98 } 99 100 ParserResult convertInto( std::string const& source, 101 std::string& target ) { 102 target = source; 103 return ParserResult::ok( ParseResultType::Matched ); 104 } 105 106 ParserResult convertInto( std::string const& source, 107 bool& target ) { 108 std::string srcLC = toLower( source ); 109 110 if ( srcLC == "y" || srcLC == "1" || srcLC == "true" || 111 srcLC == "yes" || srcLC == "on" ) { 112 target = true; 113 } else if ( srcLC == "n" || srcLC == "0" || srcLC == "false" || 114 srcLC == "no" || srcLC == "off" ) { 115 target = false; 116 } else { 117 return ParserResult::runtimeError( 118 "Expected a boolean value but did not recognise: '" + 119 source + '\'' ); 120 } 121 return ParserResult::ok( ParseResultType::Matched ); 122 } 123 124 size_t ParserBase::cardinality() const { return 1; } 125 126 InternalParseResult ParserBase::parse( Args const& args ) const { 127 return parse( args.exeName(), TokenStream( args ) ); 128 } 129 130 ParseState::ParseState( ParseResultType type, 131 TokenStream const& remainingTokens ): 132 m_type( type ), m_remainingTokens( remainingTokens ) {} 133 134 ParserResult BoundFlagRef::setFlag( bool flag ) { 135 m_ref = flag; 136 return ParserResult::ok( ParseResultType::Matched ); 137 } 138 139 ResultBase::~ResultBase() = default; 140 141 bool BoundRef::isContainer() const { return false; } 142 143 bool BoundRef::isFlag() const { return false; } 144 145 bool BoundFlagRefBase::isFlag() const { return true; } 146 147 } // namespace Detail 148 149 Detail::InternalParseResult Arg::parse(std::string const&, 150 Detail::TokenStream const& tokens) const { 151 auto validationResult = validate(); 152 if (!validationResult) 153 return Detail::InternalParseResult(validationResult); 154 155 auto remainingTokens = tokens; 156 auto const& token = *remainingTokens; 157 if (token.type != Detail::TokenType::Argument) 158 return Detail::InternalParseResult::ok(Detail::ParseState( 159 ParseResultType::NoMatch, remainingTokens)); 160 161 assert(!m_ref->isFlag()); 162 auto valueRef = 163 static_cast<Detail::BoundValueRefBase*>(m_ref.get()); 164 165 auto result = valueRef->setValue(remainingTokens->token); 166 if (!result) 167 return Detail::InternalParseResult(result); 168 else 169 return Detail::InternalParseResult::ok(Detail::ParseState( 170 ParseResultType::Matched, ++remainingTokens)); 171 } 172 173 Opt::Opt(bool& ref) : 174 ParserRefImpl(std::make_shared<Detail::BoundFlagRef>(ref)) {} 175 176 std::vector<Detail::HelpColumns> Opt::getHelpColumns() const { 177 std::ostringstream oss; 178 bool first = true; 179 for (auto const& opt : m_optNames) { 180 if (first) 181 first = false; 182 else 183 oss << ", "; 184 oss << opt; 185 } 186 if (!m_hint.empty()) 187 oss << " <" << m_hint << '>'; 188 return { { oss.str(), m_description } }; 189 } 190 191 bool Opt::isMatch(std::string const& optToken) const { 192 auto normalisedToken = normaliseOpt(optToken); 193 for (auto const& name : m_optNames) { 194 if (normaliseOpt(name) == normalisedToken) 195 return true; 196 } 197 return false; 198 } 199 200 Detail::InternalParseResult Opt::parse(std::string const&, 201 Detail::TokenStream const& tokens) const { 202 auto validationResult = validate(); 203 if (!validationResult) 204 return Detail::InternalParseResult(validationResult); 205 206 auto remainingTokens = tokens; 207 if (remainingTokens && 208 remainingTokens->type == Detail::TokenType::Option) { 209 auto const& token = *remainingTokens; 210 if (isMatch(token.token)) { 211 if (m_ref->isFlag()) { 212 auto flagRef = 213 static_cast<Detail::BoundFlagRefBase*>( 214 m_ref.get()); 215 auto result = flagRef->setFlag(true); 216 if (!result) 217 return Detail::InternalParseResult(result); 218 if (result.value() == 219 ParseResultType::ShortCircuitAll) 220 return Detail::InternalParseResult::ok(Detail::ParseState( 221 result.value(), remainingTokens)); 222 } else { 223 auto valueRef = 224 static_cast<Detail::BoundValueRefBase*>( 225 m_ref.get()); 226 ++remainingTokens; 227 if (!remainingTokens) 228 return Detail::InternalParseResult::runtimeError( 229 "Expected argument following " + 230 token.token); 231 auto const& argToken = *remainingTokens; 232 if (argToken.type != Detail::TokenType::Argument) 233 return Detail::InternalParseResult::runtimeError( 234 "Expected argument following " + 235 token.token); 236 const auto result = valueRef->setValue(argToken.token); 237 if (!result) 238 return Detail::InternalParseResult(result); 239 if (result.value() == 240 ParseResultType::ShortCircuitAll) 241 return Detail::InternalParseResult::ok(Detail::ParseState( 242 result.value(), remainingTokens)); 243 } 244 return Detail::InternalParseResult::ok(Detail::ParseState( 245 ParseResultType::Matched, ++remainingTokens)); 246 } 247 } 248 return Detail::InternalParseResult::ok( 249 Detail::ParseState(ParseResultType::NoMatch, remainingTokens)); 250 } 251 252 Detail::Result Opt::validate() const { 253 if (m_optNames.empty()) 254 return Detail::Result::logicError("No options supplied to Opt"); 255 for (auto const& name : m_optNames) { 256 if (name.empty()) 257 return Detail::Result::logicError( 258 "Option name cannot be empty"); 259 #ifdef CATCH_PLATFORM_WINDOWS 260 if (name[0] != '-' && name[0] != '/') 261 return Detail::Result::logicError( 262 "Option name must begin with '-' or '/'"); 263 #else 264 if (name[0] != '-') 265 return Detail::Result::logicError( 266 "Option name must begin with '-'"); 267 #endif 268 } 269 return ParserRefImpl::validate(); 270 } 271 272 ExeName::ExeName() : 273 m_name(std::make_shared<std::string>("<executable>")) {} 274 275 ExeName::ExeName(std::string& ref) : ExeName() { 276 m_ref = std::make_shared<Detail::BoundValueRef<std::string>>(ref); 277 } 278 279 Detail::InternalParseResult 280 ExeName::parse(std::string const&, 281 Detail::TokenStream const& tokens) const { 282 return Detail::InternalParseResult::ok( 283 Detail::ParseState(ParseResultType::NoMatch, tokens)); 284 } 285 286 ParserResult ExeName::set(std::string const& newName) { 287 auto lastSlash = newName.find_last_of("\\/"); 288 auto filename = (lastSlash == std::string::npos) 289 ? newName 290 : newName.substr(lastSlash + 1); 291 292 *m_name = filename; 293 if (m_ref) 294 return m_ref->setValue(filename); 295 else 296 return ParserResult::ok(ParseResultType::Matched); 297 } 298 299 300 301 302 Parser& Parser::operator|=( Parser const& other ) { 303 m_options.insert( m_options.end(), 304 other.m_options.begin(), 305 other.m_options.end() ); 306 m_args.insert( 307 m_args.end(), other.m_args.begin(), other.m_args.end() ); 308 return *this; 309 } 310 311 std::vector<Detail::HelpColumns> Parser::getHelpColumns() const { 312 std::vector<Detail::HelpColumns> cols; 313 for ( auto const& o : m_options ) { 314 auto childCols = o.getHelpColumns(); 315 cols.insert( cols.end(), childCols.begin(), childCols.end() ); 316 } 317 return cols; 318 } 319 320 void Parser::writeToStream( std::ostream& os ) const { 321 if ( !m_exeName.name().empty() ) { 322 os << "usage:\n" 323 << " " << m_exeName.name() << ' '; 324 bool required = true, first = true; 325 for ( auto const& arg : m_args ) { 326 if ( first ) 327 first = false; 328 else 329 os << ' '; 330 if ( arg.isOptional() && required ) { 331 os << '['; 332 required = false; 333 } 334 os << '<' << arg.hint() << '>'; 335 if ( arg.cardinality() == 0 ) 336 os << " ... "; 337 } 338 if ( !required ) 339 os << ']'; 340 if ( !m_options.empty() ) 341 os << " options"; 342 os << "\n\nwhere options are:\n"; 343 } 344 345 auto rows = getHelpColumns(); 346 size_t consoleWidth = CATCH_CONFIG_CONSOLE_WIDTH; 347 size_t optWidth = 0; 348 for ( auto const& cols : rows ) 349 optWidth = ( std::max )( optWidth, cols.left.size() + 2 ); 350 351 optWidth = ( std::min )( optWidth, consoleWidth / 2 ); 352 353 for ( auto const& cols : rows ) { 354 auto row = TextFlow::Column( cols.left ) 355 .width( optWidth ) 356 .indent( 2 ) + 357 TextFlow::Spacer( 4 ) + 358 TextFlow::Column( cols.right ) 359 .width( consoleWidth - 7 - optWidth ); 360 os << row << '\n'; 361 } 362 } 363 364 Detail::Result Parser::validate() const { 365 for ( auto const& opt : m_options ) { 366 auto result = opt.validate(); 367 if ( !result ) 368 return result; 369 } 370 for ( auto const& arg : m_args ) { 371 auto result = arg.validate(); 372 if ( !result ) 373 return result; 374 } 375 return Detail::Result::ok(); 376 } 377 378 Detail::InternalParseResult 379 Parser::parse( std::string const& exeName, 380 Detail::TokenStream const& tokens ) const { 381 382 struct ParserInfo { 383 ParserBase const* parser = nullptr; 384 size_t count = 0; 385 }; 386 std::vector<ParserInfo> parseInfos; 387 parseInfos.reserve( m_options.size() + m_args.size() ); 388 for ( auto const& opt : m_options ) { 389 parseInfos.push_back( { &opt, 0 } ); 390 } 391 for ( auto const& arg : m_args ) { 392 parseInfos.push_back( { &arg, 0 } ); 393 } 394 395 m_exeName.set( exeName ); 396 397 auto result = Detail::InternalParseResult::ok( 398 Detail::ParseState( ParseResultType::NoMatch, tokens ) ); 399 while ( result.value().remainingTokens() ) { 400 bool tokenParsed = false; 401 402 for ( auto& parseInfo : parseInfos ) { 403 if ( parseInfo.parser->cardinality() == 0 || 404 parseInfo.count < parseInfo.parser->cardinality() ) { 405 result = parseInfo.parser->parse( 406 exeName, result.value().remainingTokens() ); 407 if ( !result ) 408 return result; 409 if ( result.value().type() != 410 ParseResultType::NoMatch ) { 411 tokenParsed = true; 412 ++parseInfo.count; 413 break; 414 } 415 } 416 } 417 418 if ( result.value().type() == ParseResultType::ShortCircuitAll ) 419 return result; 420 if ( !tokenParsed ) 421 return Detail::InternalParseResult::runtimeError( 422 "Unrecognised token: " + 423 result.value().remainingTokens()->token ); 424 } 425 // !TBD Check missing required options 426 return result; 427 } 428 429 Args::Args(int argc, char const* const* argv) : 430 m_exeName(argv[0]), m_args(argv + 1, argv + argc) {} 431 432 Args::Args(std::initializer_list<std::string> args) : 433 m_exeName(*args.begin()), 434 m_args(args.begin() + 1, args.end()) {} 435 436 437 Help::Help( bool& showHelpFlag ): 438 Opt( [&]( bool flag ) { 439 showHelpFlag = flag; 440 return ParserResult::ok( ParseResultType::ShortCircuitAll ); 441 } ) { 442 static_cast<Opt&> ( *this )( 443 "display usage information" )["-?"]["-h"]["--help"] 444 .optional(); 445 } 446 447 } // namespace Clara 448 } // namespace Catch