/ externals / catch / src / catch2 / internal / catch_clara.cpp
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