001/* 002 * ============================================================================ 003 * Copyright © 2002-2024 by Thomas Thrien. 004 * All Rights Reserved. 005 * ============================================================================ 006 * 007 * Licensed to the public under the agreements of the GNU Lesser General Public 008 * License, version 3.0 (the "License"). You may obtain a copy of the License at 009 * 010 * http://www.gnu.org/licenses/lgpl.html 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 014 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 015 * License for the specific language governing permissions and limitations 016 * under the License. 017 */ 018 019package org.tquadrat.foundation.config.internal; 020 021import static java.lang.String.format; 022import static java.nio.file.Files.lines; 023import static java.util.Spliterator.IMMUTABLE; 024import static java.util.Spliterator.NONNULL; 025import static java.util.Spliterators.spliterator; 026import static java.util.stream.Collectors.joining; 027import static java.util.stream.Collectors.toList; 028import static org.apiguardian.api.API.Status.INTERNAL; 029import static org.tquadrat.foundation.config.CLIBeanSpec.ARG_FILE_ESCAPE; 030import static org.tquadrat.foundation.config.CLIBeanSpec.LEAD_IN; 031import static org.tquadrat.foundation.config.CmdLineException.MSGKEY_ArgumentMissing; 032import static org.tquadrat.foundation.config.CmdLineException.MSGKEY_MissingOperand; 033import static org.tquadrat.foundation.config.CmdLineException.MSGKEY_NoArgumentAllowed; 034import static org.tquadrat.foundation.config.CmdLineException.MSGKEY_OptionInvalid; 035import static org.tquadrat.foundation.config.CmdLineException.MSGKEY_OptionMissing; 036import static org.tquadrat.foundation.config.CmdLineException.MSGKEY_TooManyArguments; 037import static org.tquadrat.foundation.config.CmdLineException.MSG_ArgumentMissing; 038import static org.tquadrat.foundation.config.CmdLineException.MSG_MissingOperand; 039import static org.tquadrat.foundation.config.CmdLineException.MSG_NoArgumentAllowed; 040import static org.tquadrat.foundation.config.CmdLineException.MSG_OptionInvalid; 041import static org.tquadrat.foundation.config.CmdLineException.MSG_OptionMissing; 042import static org.tquadrat.foundation.config.CmdLineException.MSG_TooManyArguments; 043import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING; 044import static org.tquadrat.foundation.lang.Objects.isNull; 045import static org.tquadrat.foundation.lang.Objects.nonNull; 046import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument; 047import static org.tquadrat.foundation.util.StringUtils.isNotEmpty; 048import static org.tquadrat.foundation.util.SystemUtils.systemPropertiesAsStringMap; 049import static org.tquadrat.foundation.util.Template.replaceVariable; 050 051import java.io.File; 052import java.io.IOException; 053import java.util.ArrayList; 054import java.util.Collection; 055import java.util.HashSet; 056import java.util.Iterator; 057import java.util.List; 058import java.util.Map; 059import java.util.NoSuchElementException; 060import java.util.Objects; 061import java.util.TreeMap; 062import java.util.stream.Stream; 063import java.util.stream.StreamSupport; 064 065import org.apiguardian.api.API; 066import org.tquadrat.foundation.annotation.ClassVersion; 067import org.tquadrat.foundation.config.CmdLineException; 068import org.tquadrat.foundation.config.spi.CLIArgumentDefinition; 069import org.tquadrat.foundation.config.spi.CLIDefinition; 070import org.tquadrat.foundation.config.spi.CLIOptionDefinition; 071import org.tquadrat.foundation.config.spi.Parameters; 072import org.tquadrat.foundation.util.StringUtils; 073 074/** 075 * The parser for the command line arguments. 076 * 077 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 078 * @version $Id: ArgumentParser.java 1258 2026-06-04 18:33:06Z tquadrat $ 079 * @since 0.0.1 080 * 081 * @UMLGraph.link 082 */ 083@ClassVersion( sourceVersion = "$Id: ArgumentParser.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 084@API( status = INTERNAL, since = "0.0.1" ) 085public class ArgumentParser 086{ 087 /*---------------*\ 088 ====** Inner Classes **==================================================== 089 \*---------------*/ 090 /** 091 * <p>{@summary This class is essentially a pointer over the 092 * {@link String} 093 * array with the command line arguments.} It can move forward, and it can 094 * look ahead.</p> 095 * <p>But it will also resolve the references to argument files, and it 096 * will translate single letter options without blanks between option and 097 * value into two entries, as well as long entries where an equal sign is 098 * used.</p> 099 * 100 * @extauthor Thomas Thrien - thomas.thrien@tquadrat.org 101 * @version $Id: ArgumentParser.java 1258 2026-06-04 18:33:06Z tquadrat $ 102 * @since 0.0.1 103 * 104 * @UMLGraph.link 105 */ 106 @ClassVersion( sourceVersion = "$Id: ArgumentParser.java 1258 2026-06-04 18:33:06Z tquadrat $" ) 107 @API( status = INTERNAL, since = "0.0.1" ) 108 private final class CmdLineImpl implements Iterator<String>, Parameters 109 { 110 /*------------*\ 111 ====** Attributes **=================================================== 112 \*------------*/ 113 /** 114 * The arguments list. 115 */ 116 private final List<String> m_ArgumentList = new ArrayList<>(); 117 118 /** 119 * The current position in the arguments list. 120 */ 121 private int m_CurrentPos; 122 123 /*--------------*\ 124 ====** Constructors **================================================= 125 \*--------------*/ 126 /** 127 * Creates a new object for CmdLineImpl. 128 * 129 * @param args The arguments list. 130 */ 131 @SuppressWarnings( {"IfStatementWithTooManyBranches", "OverlyComplexMethod"} ) 132 public CmdLineImpl( final String... args ) 133 { 134 assert nonNull( args ) : "args is null"; 135 136 final Collection<String> failedFiles = new HashSet<>(); 137 var expandedToFile = true; // Flag that indicates whether an argument file was encountered 138 var inBuffer = List.of( args ); 139 List<String> outBuffer; 140 141 while( expandedToFile ) 142 { 143 expandedToFile = false; 144 outBuffer = new ArrayList<>( inBuffer.size() ); 145 for( final var arg : inBuffer ) 146 { 147 if( arg.startsWith( ARG_FILE_ESCAPE ) && !failedFiles.contains( arg ) ) 148 { 149 outBuffer.addAll( loadArgumentFile( arg, failedFiles ) ); 150 expandedToFile = true; 151 } 152 else 153 { 154 outBuffer.add( arg ); 155 } 156 } 157 inBuffer = outBuffer; 158 } 159 160 var parsingOptions = parsingOptions(); 161 for( final var arg : inBuffer ) 162 { 163 if( parsingOptions ) 164 { 165 //noinspection ConstantExpression 166 if( arg.equals( LEAD_IN + LEAD_IN ) ) 167 { 168 //---* The 'Stop Options Processing' token *----------- 169 m_ArgumentList.add( arg ); 170 parsingOptions = false; 171 } 172 else //noinspection ConstantExpression 173 if( arg.startsWith( LEAD_IN + LEAD_IN ) ) 174 /* 175 * Sequence is crucial! We need to check for the '--' 176 * prefix before we check for the '-' prefix, otherwise 177 * the result is ... interesting 178 */ 179 { 180 //---* Long option *----------------------------------- 181 final var pos = arg.indexOf( '=' ); 182 if( pos > 3 ) 183 { 184 m_ArgumentList.add( arg.substring( 0, pos ) ); 185 m_ArgumentList.add( arg.substring( pos + 1 ) ); 186 } 187 else 188 { 189 m_ArgumentList.add( arg ); 190 } 191 } 192 else if( arg.startsWith( LEAD_IN ) ) 193 { 194 //---* Single letter option *-------------------------- 195 if( arg.length() > 2 ) 196 { 197 m_ArgumentList.add( arg.substring( 0, 2 ) ); 198 m_ArgumentList.add( arg.substring( 2 ) ); 199 } 200 else 201 { 202 m_ArgumentList.add( arg ); 203 } 204 } 205 else 206 { 207 //---* No option at all, or an option argument *------- 208 m_ArgumentList.add( arg ); 209 } 210 } 211 else 212 { 213 m_ArgumentList.add( arg ); 214 } 215 } 216 217 //---* We start at 0 *--------------------------------------------- 218 m_CurrentPos = 0; 219 } // CmdLineImpl() 220 221 /*---------*\ 222 ====** Methods **====================================================== 223 \*---------*/ 224 /** 225 * Returns the current token from the arguments list. 226 * 227 * @return The current token. 228 */ 229 public final String getCurrentToken() { return m_ArgumentList.get( m_CurrentPos ); } 230 231 /** 232 * {@inheritDoc} 233 */ 234 @Override 235 public final String getParameter( final int index ) throws CmdLineException 236 { 237 assert index >= 0 : "index is less than 0"; 238 239 String retValue = null; 240 if( m_CurrentPos + index < m_ArgumentList.size() ) 241 { 242 retValue = m_ArgumentList.get( m_CurrentPos + index ); 243 if( retValue.startsWith( LEAD_IN ) && parsingOptions() ) 244 { 245 //---* We found the next option *-------------------------- 246 throw new CmdLineException( getCurrentOptionDefinition(), MSG_MissingOperand, MSGKEY_MissingOperand, getOptionName() ); 247 } 248 } 249 else 250 { 251 throw new CmdLineException( getCurrentOptionDefinition(), MSG_MissingOperand, MSGKEY_MissingOperand, getOptionName() ); 252 } 253 254 //---* Done *------------------------------------------------------ 255 return retValue; 256 } // getParameter() 257 258 /** 259 * Checks if there are more entries. 260 * 261 * @return {@true} if there are more entries, 262 * {@false} otherwise. 263 */ 264 @Override 265 public final boolean hasNext() { return m_CurrentPos < m_ArgumentList.size(); } 266 267 /** 268 * {@inheritDoc} 269 */ 270 @Override 271 public final boolean isParameter( final int index ) throws CmdLineException 272 { 273 assert index >= 0 : "index is less than 0"; 274 275 var retValue = m_CurrentPos + index < m_ArgumentList.size(); 276 if( retValue && parsingOptions() ) 277 { 278 var pos = m_CurrentPos + index; 279 while( retValue && (pos < m_ArgumentList.size()) ) 280 { 281 retValue = !(m_ArgumentList.get( pos++ ).startsWith( LEAD_IN ) ); 282 } 283 } 284 285 //---* Done *------------------------------------------------------ 286 return retValue; 287 } // getParameter() 288 289 /** 290 * <p>{@summary Loads an argument file as specified by the given 291 * argument and returns the contents of that file as additional 292 * command line arguments.}</p> 293 * <p>If no file could be retrieved for the name given with the 294 * argument, that argument will be added to the list of failed files 295 * and the unchanged argument will be returned.</p> 296 * <p>Variables of the form <code>${<i><name></i>}</code> will 297 * be replaced by the value for <i>name</i> from the system properties 298 * ({@link System#getProperty(String)}).</p> 299 * <p>Lines that starts with "#" are comments; if an 300 * argument has to start with "#", it has to be escaped with 301 * "\#".</p> 302 * 303 * @param arg The command line argument. 304 * @param failedFiles The list of failed files. 305 * @return The additional command line arguments as read from the 306 * file, if it could be opened. 307 */ 308 @SuppressWarnings( "resource" ) 309 private final Collection<String> loadArgumentFile( final String arg, @SuppressWarnings( "BoundedWildcard" ) final Collection<String> failedFiles ) 310 { 311 final var systemProperties = systemPropertiesAsStringMap(); 312 313 Collection<String> retValue; 314 if( failedFiles.contains( arg ) ) 315 { 316 retValue = List.of( arg ); 317 } 318 else 319 { 320 try 321 { 322 final var argumentFile = new File( arg.substring( 1 ) ) 323 .getCanonicalFile() 324 .getAbsoluteFile(); 325 retValue = lines( argumentFile.toPath() ) 326 .filter( line -> !line.startsWith( "#" ) ) // Drop the comments 327 .map( line -> line.startsWith( "\\" ) ? line.substring( 1 ) : line ) 328 .filter( StringUtils::isNotEmptyOrBlank ) // Drop empty lines 329 .map( line -> replaceVariable( line, systemProperties ) ) 330 .collect( toList() ); 331 } 332 catch( final IOException ignored ) 333 { 334 retValue = List.of( arg ); 335 failedFiles.add( arg ); 336 } 337 } 338 339 //---* Done *------------------------------------------------------ 340 return retValue; 341 } // loadArgumentFile() 342 343 /** 344 * {@inheritDoc} 345 */ 346 @SuppressWarnings( "NewExceptionWithoutArguments" ) 347 @Override 348 public final String next() throws NoSuchElementException 349 { 350 if( !hasNext() ) throw new NoSuchElementException(); 351 final var retValue = getCurrentToken(); 352 proceed( 1 ); 353 354 //---* Done *------------------------------------------------------ 355 return retValue; 356 } // next() 357 358 /** 359 * Skip the given number of entries. 360 * 361 * @param n The number of entries to skip; must be greater 0. 362 */ 363 public final void proceed( final int n ) 364 { 365 assert n >= 0 : "n less than 0"; 366 367 m_CurrentPos += n; 368 } // proceed() 369 370 /** 371 * In case the entry is a combination from option and the related 372 * parameter (like {@code --arg value} or, for single character 373 * options, {@code -p value}), this method is used to put it back to 374 * the command line. 375 * 376 * @param part1 The first part, usually the option. 377 * @param part2 The second part, usually the value. 378 */ 379 @SuppressWarnings( "unused" ) 380 public final void putback( final String part1, final String part2 ) 381 { 382 assert isNotEmpty( part1 ) : "part1 is empty"; 383 assert isNotEmpty( part2 ) : "part2 is empty"; 384 385 m_ArgumentList.set( m_CurrentPos, part2 ); 386 m_ArgumentList.add( m_CurrentPos, part1 ); 387 } // putback() 388 } 389 // class CmdLineImpl 390 391 /*------------*\ 392 ====** Attributes **======================================================= 393 \*------------*/ 394 /** 395 * The 396 * {@link CLIDefinition} 397 * instances for arguments. 398 */ 399 private final List<CLIArgumentDefinition> m_ArgumentDefinitions = new ArrayList<>(); 400 401 /** 402 * The definition for the current command line entry. 403 */ 404 @SuppressWarnings( "UseOfConcreteClass" ) 405 private CLIOptionDefinition m_CurrentOptionDefinition = null; 406 407 /** 408 * The 409 * {@link CLIDefinition} 410 * instances for options. 411 */ 412 private final Map<String,CLIOptionDefinition> m_OptionDefinitions = new TreeMap<>(); 413 414 /** 415 * {@true} (the default) if options has to be parsed. If set to 416 * {@false}, only arguments are parsed. 417 * 418 * @see #stopOptionParsing() 419 */ 420 private boolean m_ParsingOptions = false; 421 422 /*--------------*\ 423 ====** Constructors **===================================================== 424 \*--------------*/ 425 /** 426 * Creates a new {@code ArgumentParser} instance. 427 * 428 * @param cliDefinitions The definition for the command line options and 429 * arguments from the configuration bean specification. 430 */ 431 public ArgumentParser( final Collection<? extends CLIDefinition> cliDefinitions ) 432 { 433 //---* Add the definitions to the registry *--------------------------- 434 for( final var cliDefinition : requireNonNullArgument( cliDefinitions, "cliDefinitions" ) ) 435 { 436 if( cliDefinition.isArgument() ) 437 { 438 addArgument( cliDefinition ); 439 } 440 else 441 { 442 addOption( cliDefinition ); 443 } 444 } 445 446 //---* Check the consistency of the arguments list *------------------- 447 if( m_ArgumentDefinitions.stream().anyMatch( Objects::isNull ) ) 448 { 449 final var indexes = Stream.iterate( 0, i -> i < m_ArgumentDefinitions.size(), i -> i + 1 ) 450 .filter( i -> isNull( m_ArgumentDefinitions.get( i ) ) ) 451 .map( i -> Integer.toString( i ) ) 452 .collect( joining( ", " ) ); 453 throw new IllegalArgumentException( "Missing index: %s - Gap in Sequence".formatted( indexes ) ); 454 } 455 } // ArgumentParser() 456 457 /*---------*\ 458 ====** Methods **========================================================== 459 \*---------*/ 460 /** 461 * Adds an argument definition to the registry for arguments. 462 * 463 * @param definition The argument definition to add. 464 */ 465 private final void addArgument( final CLIDefinition definition ) 466 { 467 final var argumentDefinition = (CLIArgumentDefinition) requireNonNullArgument( definition, "definition" ); 468 469 //--* Make sure the argument will fit in the list *-------------------- 470 final var index = argumentDefinition.index(); 471 while( index >= m_ArgumentDefinitions.size() ) 472 { 473 m_ArgumentDefinitions.add( null ); 474 } 475 if( nonNull( m_ArgumentDefinitions.get( index ) ) ) 476 { 477 throw new IllegalArgumentException( "Argument index '%d' is used more than once".formatted( index ) ); 478 } 479 m_ArgumentDefinitions.set( index, argumentDefinition ); 480 } // addArgument() 481 482 /** 483 * Adds an option definition to the registry for options. 484 * 485 * @param definition The option definition to add. 486 */ 487 private final void addOption( final CLIDefinition definition ) 488 { 489 //---* We have at least one option ... *------------------------------- 490 m_ParsingOptions = true; 491 492 final var optionDefinition = (CLIOptionDefinition) requireNonNullArgument( definition, "definition" ); 493 checkOptionNotYetUsed( optionDefinition.name() ); 494 m_OptionDefinitions.put( optionDefinition.name(), optionDefinition ); 495 for( final var alias : optionDefinition.aliases() ) 496 { 497 checkOptionNotYetUsed( alias ); 498 m_OptionDefinitions.put( alias, optionDefinition ); 499 } 500} // addOption() 501 502 /** 503 * Finds an option definition by the given option name. 504 * 505 * @param name The option name. 506 * @return The option definition. 507 * @throws CmdLineException There is no option definition for the 508 * given option name. 509 */ 510 private final CLIOptionDefinition findOptionDefinition( final String name ) 511 { 512 assert nonNull( name ) : "name is null"; 513 514 final var retValue = m_OptionDefinitions.get( name ); 515 if( isNull( retValue ) ) 516 { 517 throw new CmdLineException( format( MSG_OptionInvalid, name ), MSGKEY_OptionInvalid, name ); 518 } 519 520 //---* Done *---------------------------------------------------------- 521 return retValue; 522 } // findOptionDefinition() 523 524 /** 525 * Returns the current CLI option definition. 526 * 527 * @return The current CLI option definition. 528 */ 529 final CLIOptionDefinition getCurrentOptionDefinition() { return m_CurrentOptionDefinition; } 530 531 /** 532 * Returns the name of the option that is being processed currently. 533 * 534 * @return The name of the current option. 535 */ 536 final String getOptionName() 537 { 538 final var retValue = nonNull( m_CurrentOptionDefinition ) ? m_CurrentOptionDefinition.name() : null; 539 540 //---* Done *---------------------------------------------------------- 541 return retValue; 542 } // getOptionName() 543 544 /** 545 * Checks if the given token is an option (as opposed to an argument). 546 * Option tokens will have a hyphen 547 * ({@value org.tquadrat.foundation.config.CLIBeanSpec#LEAD_IN}) 548 * as their first character. 549 * 550 * @param token The token to test. 551 * @return {@true} if the given token is an option, 552 * {@false} if it is an argument, or if no (more) options are 553 * expected at all. 554 * 555 * @see #stopOptionParsing() 556 * @see #m_ParsingOptions 557 */ 558 private final boolean isOption( final String token ) 559 { 560 assert nonNull( token ) : "token is null"; 561 assert !EMPTY_STRING.equals( token ) : "token is empty"; 562 563 final var retValue = m_ParsingOptions && token.startsWith( LEAD_IN ); 564 565 //---* Done *---------------------------------------------------------- 566 return retValue; 567 } // isOption() 568 569 /** 570 * Checks the command line definition whether the given name is not yet 571 * used, either as a name or an alias. 572 * 573 * @param name The name to check. 574 * @throws IllegalArgumentException The given name is already in use. 575 */ 576 private final void checkOptionNotYetUsed( final String name ) throws IllegalArgumentException 577 { 578 assert nonNull( name ) : "name is null"; 579 580 if( m_OptionDefinitions.containsKey( name ) ) 581 { 582 throw new IllegalArgumentException( "Option name '%s' is used more than once".formatted( name ) ); 583 } 584 } // checkOptionNotYetUsed() 585 586 /** 587 * Parses the given command line arguments and sets the retrieved values 588 * to the configuration bean. 589 * 590 * @param args The command line arguments to parse. 591 * @throws CmdLineException An error occurred while parsing the 592 * arguments or a mandatory option or argument is missing on the 593 * command line. 594 */ 595 @SuppressWarnings( "OverlyComplexMethod" ) 596 public final void parse( final String... args ) throws CmdLineException 597 { 598 final var cmdLine = new CmdLineImpl( requireNonNullArgument( args, "args" ) ); 599 600 final Collection<CLIDefinition> present = new HashSet<>(); 601 var argIndex = 0; 602 ParseLoop: while( cmdLine.hasNext() ) 603 { 604 final var arg = cmdLine.getCurrentToken(); 605 if( isOption( arg ) ) 606 { 607 m_CurrentOptionDefinition = findOptionDefinition( arg ); 608 present.add( m_CurrentOptionDefinition ); 609 610 //---* We know the option; skip its name *--------------------- 611 cmdLine.proceed( 1 ); 612 613 //---* Set the value *----------------------------------------- 614 cmdLine.proceed( m_CurrentOptionDefinition.processParameters( cmdLine ) ); 615 } 616 else 617 { 618 if( argIndex >= m_ArgumentDefinitions.size() ) 619 { 620 final var message = m_ArgumentDefinitions.isEmpty() ? MSG_NoArgumentAllowed : MSG_TooManyArguments; 621 final var messageKey = m_ArgumentDefinitions.isEmpty() ? MSGKEY_NoArgumentAllowed : MSGKEY_TooManyArguments; 622 throw new CmdLineException( message, messageKey, arg ); 623 } 624 625 //---* We know the argument ... *------------------------------ 626 final var currentArgumentDefinition = m_ArgumentDefinitions.get( argIndex ); 627 present.add( currentArgumentDefinition ); 628 if( !currentArgumentDefinition.isMultiValued() ) 629 { 630 /* 631 * Multivalued arguments are only allowed as the last 632 * argument, and we can have as many values for them as we 633 * want (or the operating systems allows on the command 634 * line). 635 */ 636 ++argIndex; 637 } 638 639 //---* Set the value *----------------------------------------- 640 cmdLine.proceed( currentArgumentDefinition.processParameters( cmdLine ) ); 641 } 642 } // ParseLoop: 643 644 //---* Make sure that all mandatory options are present *-------------- 645 for( final var optionDefinition : m_OptionDefinitions.values() ) 646 /* 647 * We can live with the fact that in case of an alias an option 648 * definition is inspected twice or even more often. 649 */ 650 { 651 if( optionDefinition.required() && !present.contains( optionDefinition ) ) 652 { 653 throw new CmdLineException( optionDefinition, MSG_OptionMissing, MSGKEY_OptionMissing, optionDefinition.name() ); 654 } 655 } 656 657 //---* Make sure that all mandatory arguments are present *------------ 658 for( final var argumentDefinition : m_ArgumentDefinitions ) 659 { 660 if( argumentDefinition.required() && !present.contains( argumentDefinition ) ) 661 { 662 throw new CmdLineException( argumentDefinition, MSG_ArgumentMissing, MSGKEY_ArgumentMissing, argumentDefinition.metaVar() ); 663 } 664 } 665 } // parse() 666 667 /** 668 * Returns {@true} if this {@code ArgumentParser} will parse 669 * options. This can be set to {@false} either when no 670 * {@link org.tquadrat.foundation.config.Option @Option} 671 * annotation was found on the configuration bean specification interface, 672 * when the stop token ("--") was encountered on the command 673 * line, or when 674 * {@link #stopOptionParsing()} 675 * was called manually. 676 * 677 * @return {@true} when options are parsed, {@false} 678 * if not. 679 * 680 * @see org.tquadrat.foundation.config.CLIBeanSpec#LEAD_IN 681 */ 682 @SuppressWarnings( "BooleanMethodNameMustStartWithQuestion" ) 683 public final boolean parsingOptions() { return m_ParsingOptions; } 684 685 /** 686 * <p>{@summary Resolves the given command line.}</p> 687 * <p>The method is mainly meant for test and debugging purposes.</p> 688 * 689 * @param args The command line arguments. 690 * @return The resolved command line as a single String. 691 */ 692 public final String resolveCommandLine( final String... args ) 693 { 694 final Iterator<String> cmdLine = new CmdLineImpl( args ); 695 @SuppressWarnings( "ConstantExpression" ) 696 final var spliterator = spliterator( cmdLine, args.length, IMMUTABLE | NONNULL ); 697 final var retValue = StreamSupport.stream( spliterator, false ) 698 .map( a -> a.contains( " " ) ? format( "\"%s\"", a ) : a ) 699 .collect( joining( " " ) ); 700 701 //---* Done *---------------------------------------------------------- 702 return retValue; 703 } // resolveCommandLine() 704 705 /** 706 * Stops the parsing for options. After the call, the argument list will 707 * be parsed only for 708 * {@linkplain org.tquadrat.foundation.config.Argument arguments}. 709 */ 710 public final void stopOptionParsing() { m_ParsingOptions = false; } 711} 712// class ArgumentParserImpl 713 714/* 715 * End of File 716 */