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>&lt;name&gt;</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 &quot;#&quot; are comments; if an
300         *  argument has to start with &quot;#&quot;, it has to be escaped with
301         *  &quot;\#&quot;.</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 &#64;Option}
671     *  annotation was found on the configuration bean specification interface,
672     *  when the stop token (&quot;--&quot;) 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 */