mirror of
https://github.com/catchorg/Catch2.git
synced 2025-05-01 04:53:53 +00:00
This removes about 200 pointless copies from printing the help message (the original motivation for the change), and also nicely improves performance of the various reporters that depend on TextFlow.
465 lines
18 KiB
C++
465 lines
18 KiB
C++
|
|
// Copyright Catch2 Authors
|
|
// Distributed under the Boost Software License, Version 1.0.
|
|
// (See accompanying file LICENSE.txt or copy at
|
|
// https://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
// SPDX-License-Identifier: BSL-1.0
|
|
|
|
#include <catch2/internal/catch_clara.hpp>
|
|
#include <catch2/internal/catch_console_width.hpp>
|
|
#include <catch2/internal/catch_platform.hpp>
|
|
#include <catch2/internal/catch_string_manip.hpp>
|
|
#include <catch2/internal/catch_textflow.hpp>
|
|
#include <catch2/internal/catch_reusable_string_stream.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <ostream>
|
|
|
|
namespace {
|
|
bool isOptPrefix( char c ) {
|
|
return c == '-'
|
|
#ifdef CATCH_PLATFORM_WINDOWS
|
|
|| c == '/'
|
|
#endif
|
|
;
|
|
}
|
|
|
|
Catch::StringRef normaliseOpt( Catch::StringRef optName ) {
|
|
if ( optName[0] == '-'
|
|
#if defined(CATCH_PLATFORM_WINDOWS)
|
|
|| optName[0] == '/'
|
|
#endif
|
|
) {
|
|
return optName.substr( 1, optName.size() );
|
|
}
|
|
|
|
return optName;
|
|
}
|
|
|
|
static size_t find_first_separator(Catch::StringRef sr) {
|
|
auto is_separator = []( char c ) {
|
|
return c == ' ' || c == ':' || c == '=';
|
|
};
|
|
size_t pos = 0;
|
|
while (pos < sr.size()) {
|
|
if (is_separator(sr[pos])) { return pos; }
|
|
++pos;
|
|
}
|
|
|
|
return Catch::StringRef::npos;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace Catch {
|
|
namespace Clara {
|
|
namespace Detail {
|
|
|
|
void TokenStream::loadBuffer() {
|
|
m_tokenBuffer.clear();
|
|
|
|
// Skip any empty strings
|
|
while ( it != itEnd && it->empty() ) {
|
|
++it;
|
|
}
|
|
|
|
if ( it != itEnd ) {
|
|
StringRef next = *it;
|
|
if ( isOptPrefix( next[0] ) ) {
|
|
auto delimiterPos = find_first_separator(next);
|
|
if ( delimiterPos != StringRef::npos ) {
|
|
m_tokenBuffer.push_back(
|
|
{ TokenType::Option,
|
|
next.substr( 0, delimiterPos ) } );
|
|
m_tokenBuffer.push_back(
|
|
{ TokenType::Argument,
|
|
next.substr( delimiterPos + 1, next.size() ) } );
|
|
} else {
|
|
if ( next[1] != '-' && next.size() > 2 ) {
|
|
// Combined short args, e.g. "-ab" for "-a -b"
|
|
for ( size_t i = 1; i < next.size(); ++i ) {
|
|
m_tokenBuffer.push_back(
|
|
{ TokenType::Option,
|
|
next.substr( i, 1 ) } );
|
|
}
|
|
} else {
|
|
m_tokenBuffer.push_back(
|
|
{ TokenType::Option, next } );
|
|
}
|
|
}
|
|
} else {
|
|
m_tokenBuffer.push_back(
|
|
{ TokenType::Argument, next } );
|
|
}
|
|
}
|
|
}
|
|
|
|
TokenStream::TokenStream( Args const& args ):
|
|
TokenStream( args.m_args.begin(), args.m_args.end() ) {}
|
|
|
|
TokenStream::TokenStream( Iterator it_, Iterator itEnd_ ):
|
|
it( it_ ), itEnd( itEnd_ ) {
|
|
loadBuffer();
|
|
}
|
|
|
|
TokenStream& TokenStream::operator++() {
|
|
if ( m_tokenBuffer.size() >= 2 ) {
|
|
m_tokenBuffer.erase( m_tokenBuffer.begin() );
|
|
} else {
|
|
if ( it != itEnd )
|
|
++it;
|
|
loadBuffer();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
ParserResult convertInto( std::string const& source,
|
|
std::string& target ) {
|
|
target = source;
|
|
return ParserResult::ok( ParseResultType::Matched );
|
|
}
|
|
|
|
ParserResult convertInto( std::string const& source,
|
|
bool& target ) {
|
|
std::string srcLC = toLower( source );
|
|
|
|
if ( srcLC == "y" || srcLC == "1" || srcLC == "true" ||
|
|
srcLC == "yes" || srcLC == "on" ) {
|
|
target = true;
|
|
} else if ( srcLC == "n" || srcLC == "0" || srcLC == "false" ||
|
|
srcLC == "no" || srcLC == "off" ) {
|
|
target = false;
|
|
} else {
|
|
return ParserResult::runtimeError(
|
|
"Expected a boolean value but did not recognise: '" +
|
|
source + '\'' );
|
|
}
|
|
return ParserResult::ok( ParseResultType::Matched );
|
|
}
|
|
|
|
size_t ParserBase::cardinality() const { return 1; }
|
|
|
|
InternalParseResult ParserBase::parse( Args const& args ) const {
|
|
return parse( static_cast<std::string>(args.exeName()), TokenStream( args ) );
|
|
}
|
|
|
|
ParseState::ParseState( ParseResultType type,
|
|
TokenStream remainingTokens ):
|
|
m_type( type ), m_remainingTokens( CATCH_MOVE(remainingTokens) ) {}
|
|
|
|
ParserResult BoundFlagRef::setFlag( bool flag ) {
|
|
m_ref = flag;
|
|
return ParserResult::ok( ParseResultType::Matched );
|
|
}
|
|
|
|
ResultBase::~ResultBase() = default;
|
|
|
|
bool BoundRef::isContainer() const { return false; }
|
|
|
|
bool BoundRef::isFlag() const { return false; }
|
|
|
|
bool BoundFlagRefBase::isFlag() const { return true; }
|
|
|
|
} // namespace Detail
|
|
|
|
Detail::InternalParseResult Arg::parse(std::string const&,
|
|
Detail::TokenStream tokens) const {
|
|
auto validationResult = validate();
|
|
if (!validationResult)
|
|
return Detail::InternalParseResult(validationResult);
|
|
|
|
auto token = *tokens;
|
|
if (token.type != Detail::TokenType::Argument)
|
|
return Detail::InternalParseResult::ok(Detail::ParseState(
|
|
ParseResultType::NoMatch, CATCH_MOVE(tokens)));
|
|
|
|
assert(!m_ref->isFlag());
|
|
auto valueRef =
|
|
static_cast<Detail::BoundValueRefBase*>(m_ref.get());
|
|
|
|
auto result = valueRef->setValue(static_cast<std::string>(token.token));
|
|
if ( !result )
|
|
return Detail::InternalParseResult( result );
|
|
else
|
|
return Detail::InternalParseResult::ok(
|
|
Detail::ParseState( ParseResultType::Matched,
|
|
CATCH_MOVE( ++tokens ) ) );
|
|
}
|
|
|
|
Opt::Opt(bool& ref) :
|
|
ParserRefImpl(std::make_shared<Detail::BoundFlagRef>(ref)) {}
|
|
|
|
Detail::HelpColumns Opt::getHelpColumns() const {
|
|
ReusableStringStream oss;
|
|
bool first = true;
|
|
for (auto const& opt : m_optNames) {
|
|
if (first)
|
|
first = false;
|
|
else
|
|
oss << ", ";
|
|
oss << opt;
|
|
}
|
|
if (!m_hint.empty())
|
|
oss << " <" << m_hint << '>';
|
|
return { oss.str(), m_description };
|
|
}
|
|
|
|
bool Opt::isMatch(StringRef optToken) const {
|
|
auto normalisedToken = normaliseOpt(optToken);
|
|
for (auto const& name : m_optNames) {
|
|
if (normaliseOpt(name) == normalisedToken)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Detail::InternalParseResult Opt::parse(std::string const&,
|
|
Detail::TokenStream tokens) const {
|
|
auto validationResult = validate();
|
|
if (!validationResult)
|
|
return Detail::InternalParseResult(validationResult);
|
|
|
|
if (tokens &&
|
|
tokens->type == Detail::TokenType::Option) {
|
|
auto const& token = *tokens;
|
|
if (isMatch(token.token)) {
|
|
if (m_ref->isFlag()) {
|
|
auto flagRef =
|
|
static_cast<Detail::BoundFlagRefBase*>(
|
|
m_ref.get());
|
|
auto result = flagRef->setFlag(true);
|
|
if (!result)
|
|
return Detail::InternalParseResult(result);
|
|
if (result.value() ==
|
|
ParseResultType::ShortCircuitAll)
|
|
return Detail::InternalParseResult::ok(Detail::ParseState(
|
|
result.value(), CATCH_MOVE(tokens)));
|
|
} else {
|
|
auto valueRef =
|
|
static_cast<Detail::BoundValueRefBase*>(
|
|
m_ref.get());
|
|
++tokens;
|
|
if (!tokens)
|
|
return Detail::InternalParseResult::runtimeError(
|
|
"Expected argument following " +
|
|
token.token);
|
|
auto const& argToken = *tokens;
|
|
if (argToken.type != Detail::TokenType::Argument)
|
|
return Detail::InternalParseResult::runtimeError(
|
|
"Expected argument following " +
|
|
token.token);
|
|
const auto result = valueRef->setValue(static_cast<std::string>(argToken.token));
|
|
if (!result)
|
|
return Detail::InternalParseResult(result);
|
|
if (result.value() ==
|
|
ParseResultType::ShortCircuitAll)
|
|
return Detail::InternalParseResult::ok(Detail::ParseState(
|
|
result.value(), CATCH_MOVE(tokens)));
|
|
}
|
|
return Detail::InternalParseResult::ok(Detail::ParseState(
|
|
ParseResultType::Matched, CATCH_MOVE(++tokens)));
|
|
}
|
|
}
|
|
return Detail::InternalParseResult::ok(
|
|
Detail::ParseState(ParseResultType::NoMatch, CATCH_MOVE(tokens)));
|
|
}
|
|
|
|
Detail::Result Opt::validate() const {
|
|
if (m_optNames.empty())
|
|
return Detail::Result::logicError("No options supplied to Opt");
|
|
for (auto const& name : m_optNames) {
|
|
if (name.empty())
|
|
return Detail::Result::logicError(
|
|
"Option name cannot be empty");
|
|
#ifdef CATCH_PLATFORM_WINDOWS
|
|
if (name[0] != '-' && name[0] != '/')
|
|
return Detail::Result::logicError(
|
|
"Option name must begin with '-' or '/'");
|
|
#else
|
|
if (name[0] != '-')
|
|
return Detail::Result::logicError(
|
|
"Option name must begin with '-'");
|
|
#endif
|
|
}
|
|
return ParserRefImpl::validate();
|
|
}
|
|
|
|
ExeName::ExeName() :
|
|
m_name(std::make_shared<std::string>("<executable>")) {}
|
|
|
|
ExeName::ExeName(std::string& ref) : ExeName() {
|
|
m_ref = std::make_shared<Detail::BoundValueRef<std::string>>(ref);
|
|
}
|
|
|
|
Detail::InternalParseResult
|
|
ExeName::parse(std::string const&,
|
|
Detail::TokenStream tokens) const {
|
|
return Detail::InternalParseResult::ok(
|
|
Detail::ParseState(ParseResultType::NoMatch, CATCH_MOVE(tokens)));
|
|
}
|
|
|
|
ParserResult ExeName::set(std::string const& newName) {
|
|
auto lastSlash = newName.find_last_of("\\/");
|
|
auto filename = (lastSlash == std::string::npos)
|
|
? newName
|
|
: newName.substr(lastSlash + 1);
|
|
|
|
*m_name = filename;
|
|
if (m_ref)
|
|
return m_ref->setValue(filename);
|
|
else
|
|
return ParserResult::ok(ParseResultType::Matched);
|
|
}
|
|
|
|
|
|
|
|
|
|
Parser& Parser::operator|=( Parser const& other ) {
|
|
m_options.insert( m_options.end(),
|
|
other.m_options.begin(),
|
|
other.m_options.end() );
|
|
m_args.insert(
|
|
m_args.end(), other.m_args.begin(), other.m_args.end() );
|
|
return *this;
|
|
}
|
|
|
|
std::vector<Detail::HelpColumns> Parser::getHelpColumns() const {
|
|
std::vector<Detail::HelpColumns> cols;
|
|
cols.reserve( m_options.size() );
|
|
for ( auto const& o : m_options ) {
|
|
cols.push_back(o.getHelpColumns());
|
|
}
|
|
return cols;
|
|
}
|
|
|
|
void Parser::writeToStream( std::ostream& os ) const {
|
|
if ( !m_exeName.name().empty() ) {
|
|
os << "usage:\n"
|
|
<< " " << m_exeName.name() << ' ';
|
|
bool required = true, first = true;
|
|
for ( auto const& arg : m_args ) {
|
|
if ( first )
|
|
first = false;
|
|
else
|
|
os << ' ';
|
|
if ( arg.isOptional() && required ) {
|
|
os << '[';
|
|
required = false;
|
|
}
|
|
os << '<' << arg.hint() << '>';
|
|
if ( arg.cardinality() == 0 )
|
|
os << " ... ";
|
|
}
|
|
if ( !required )
|
|
os << ']';
|
|
if ( !m_options.empty() )
|
|
os << " options";
|
|
os << "\n\nwhere options are:\n";
|
|
}
|
|
|
|
auto rows = getHelpColumns();
|
|
size_t consoleWidth = CATCH_CONFIG_CONSOLE_WIDTH;
|
|
size_t optWidth = 0;
|
|
for ( auto const& cols : rows )
|
|
optWidth = ( std::max )( optWidth, cols.left.size() + 2 );
|
|
|
|
optWidth = ( std::min )( optWidth, consoleWidth / 2 );
|
|
|
|
for ( auto& cols : rows ) {
|
|
auto row = TextFlow::Column( CATCH_MOVE(cols.left) )
|
|
.width( optWidth )
|
|
.indent( 2 ) +
|
|
TextFlow::Spacer( 4 ) +
|
|
TextFlow::Column( static_cast<std::string>(cols.descriptions) )
|
|
.width( consoleWidth - 7 - optWidth );
|
|
os << row << '\n';
|
|
}
|
|
}
|
|
|
|
Detail::Result Parser::validate() const {
|
|
for ( auto const& opt : m_options ) {
|
|
auto result = opt.validate();
|
|
if ( !result )
|
|
return result;
|
|
}
|
|
for ( auto const& arg : m_args ) {
|
|
auto result = arg.validate();
|
|
if ( !result )
|
|
return result;
|
|
}
|
|
return Detail::Result::ok();
|
|
}
|
|
|
|
Detail::InternalParseResult
|
|
Parser::parse( std::string const& exeName,
|
|
Detail::TokenStream tokens ) const {
|
|
|
|
struct ParserInfo {
|
|
ParserBase const* parser = nullptr;
|
|
size_t count = 0;
|
|
};
|
|
std::vector<ParserInfo> parseInfos;
|
|
parseInfos.reserve( m_options.size() + m_args.size() );
|
|
for ( auto const& opt : m_options ) {
|
|
parseInfos.push_back( { &opt, 0 } );
|
|
}
|
|
for ( auto const& arg : m_args ) {
|
|
parseInfos.push_back( { &arg, 0 } );
|
|
}
|
|
|
|
m_exeName.set( exeName );
|
|
|
|
auto result = Detail::InternalParseResult::ok(
|
|
Detail::ParseState( ParseResultType::NoMatch, CATCH_MOVE(tokens) ) );
|
|
while ( result.value().remainingTokens() ) {
|
|
bool tokenParsed = false;
|
|
|
|
for ( auto& parseInfo : parseInfos ) {
|
|
if ( parseInfo.parser->cardinality() == 0 ||
|
|
parseInfo.count < parseInfo.parser->cardinality() ) {
|
|
result = parseInfo.parser->parse(
|
|
exeName, CATCH_MOVE(result).value().remainingTokens() );
|
|
if ( !result )
|
|
return result;
|
|
if ( result.value().type() !=
|
|
ParseResultType::NoMatch ) {
|
|
tokenParsed = true;
|
|
++parseInfo.count;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( result.value().type() == ParseResultType::ShortCircuitAll )
|
|
return result;
|
|
if ( !tokenParsed )
|
|
return Detail::InternalParseResult::runtimeError(
|
|
"Unrecognised token: " +
|
|
result.value().remainingTokens()->token );
|
|
}
|
|
// !TBD Check missing required options
|
|
return result;
|
|
}
|
|
|
|
Args::Args(int argc, char const* const* argv) :
|
|
m_exeName(argv[0]), m_args(argv + 1, argv + argc) {}
|
|
|
|
Args::Args(std::initializer_list<StringRef> args) :
|
|
m_exeName(*args.begin()),
|
|
m_args(args.begin() + 1, args.end()) {}
|
|
|
|
|
|
Help::Help( bool& showHelpFlag ):
|
|
Opt( [&]( bool flag ) {
|
|
showHelpFlag = flag;
|
|
return ParserResult::ok( ParseResultType::ShortCircuitAll );
|
|
} ) {
|
|
static_cast<Opt&> ( *this )(
|
|
"display usage information" )["-?"]["-h"]["--help"]
|
|
.optional();
|
|
}
|
|
|
|
} // namespace Clara
|
|
} // namespace Catch
|