1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
/*
  Licensed to the Apache Software Foundation (ASF) under one or more
  contributor license agreements.  See the NOTICE file distributed with
  this work for additional information regarding copyright ownership.
  The ASF licenses this file to You under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with
  the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
 */
package Apache.CLI;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

/** Default parser. */
@Torello.JavaDoc.JDHeaderBackgroundImg(EmbedTagFileID="LICENSE")
@SuppressWarnings({"rawtypes", "unchecked"})
public class DefaultParser implements CommandLineParser
{
    /**
     * A nested builder class to create {@code DefaultParser} instances
     * using descriptive methods.
     *
     * <BR /><BR />Example usage:
     * 
     * <DIV CLASS=EXAMPLE>{@code
     * DefaultParser parser = Option
     *      .builder()
     *      .setAllowPartialMatching(false)
     *      .setStripLeadingAndTrailingQuotes(false)
     *      .build();
     * }</DIV>
     */
    @Torello.JavaDoc.JDHeaderBackgroundImg(EmbedTagFileID="LICENSE")
    public static final class Builder
    {
        // Flag indicating if partial matching of long options is supported.
        private boolean allowPartialMatching = true;

        // Flag indicating if balanced leading and trailing double quotes should be stripped from
        // option arguments.

        private Boolean stripLeadingAndTrailingQuotes;

        /**
         * Constructs a new {@code Builder} for a {@code DefaultParser} instance.
         *
         * <BR /><BR />Both allowPartialMatching and stripLeadingAndTrailingQuotes are true by
         * default, mimicking the argument-less constructor.
         */
        private Builder() { }

        /**
         * Builds an DefaultParser with the values declared by this {@link Builder}.
         *
         * @return the new {@link DefaultParser}
         */
        public DefaultParser build()
        { return new DefaultParser(allowPartialMatching, stripLeadingAndTrailingQuotes); }

        /**
         * Sets if partial matching of long options is supported.
         *
         * <BR /><BR />By "partial matching" we mean that given the following code:
         *
         * <DIV CLASS=EXAMPLE>{@code
         * final Options options = new Options();
         * 
         * options.addOption(new Option("d", "debug", false, "Turn on debug."));
         * options.addOption(new Option("e", "extract", false, "Turn on extract."));
         * options.addOption(new Option("o", "option", true, "Turn on option with argument."));
         * }</DIV>
         *
         * <BR />If "partial matching" is turned on, {@code -de} only matches the {@code "debug"}
         * option. However, with "partial matching" disabled, {@code -de} would enable both
         * {@code debug} as well as {@code extract}
         *
         * @param allowPartialMatching whether to allow partial matching of long options
         * @return this builder, to allow method 
         */
        public Builder setAllowPartialMatching(final boolean allowPartialMatching)
        {
            this.allowPartialMatching = allowPartialMatching;
            return this;
        }

        /**
         * Sets if balanced leading and trailing double quotes should be stripped from option
         * arguments.
         *
         * <BR /><BR />If "stripping of balanced leading and trailing double quotes from option
         * arguments" is true, the outermost balanced double quotes of option arguments values will
         * be removed.  For example, {@code -o '"x"'} getValue() will return {@code x}, instead of
         * {@code "x"}
         *
         * <BR /><BR />If "stripping of balanced leading and trailing double quotes from option
         * arguments" is null, then quotes will be stripped from option values separated by space
         * from the option, but kept in other cases, which is the historic behavior.
         *
         * @param stripLeadingAndTrailingQuotes whether balanced leading and trailing double quotes
         * should be stripped from option arguments.
         * 
         * @return this builder, to allow method chaining
         */
        public Builder setStripLeadingAndTrailingQuotes
            (final Boolean stripLeadingAndTrailingQuotes)
        {
            this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
            return this;
        }
    }

    /**
     * Creates a new {@link Builder} to create an {@link DefaultParser} using descriptive
     * methods.
     *
     * @return a new {@link Builder} instance
     */
    public static Builder builder()
    { return new Builder(); }

    /** The command-line instance. */
    protected CommandLine cmd;

    /** The current options. */
    protected Options options;

    /**
     * Flag indicating how unrecognized tokens are handled. {@code true} to stop the parsing and
     * add the remaining tokens to the args list. {@code false} to throw an exception.
     */
    protected boolean stopAtNonOption;

    /** The token currently processed. */
    protected String currentToken;

    /** The last option parsed. */
    protected Option currentOption;

    /**
     * Flag indicating if tokens should no longer be analyzed and simply added as arguments of the
     * command line.
     */
    protected boolean skipParsing;

    /** The required options and groups expected to be found when parsing the command line. */
    protected List expectedOpts;

    // Flag indicating if partial matching of long options is supported.
    private final boolean allowPartialMatching;

    /**
     * Flag indicating if balanced leading and trailing double quotes should be stripped from
     * option arguments.  null represents the historic arbitrary behavior
     */
    private final Boolean stripLeadingAndTrailingQuotes;

    /**
     * Creates a new DefaultParser instance with partial matching enabled.
     *
     * By "partial matching" we mean that given the following code:
     *
     * <DIV CLASS=EXAMPLE>{@code
     * final Options options = new Options();
     * 
     * options.addOption(new Option("d", "debug", false, "Turn on debug."));
     * options.addOption(new Option("e", "extract", false, "Turn on extract."));
     * options.addOption(new Option("o", "option", true, "Turn on option with argument."));
     * }</DIV>
     *
     * <BR /><BR />with "partial matching" turned on, {@code -de} only matches the {@code "debug"}
     * option. However, with "partial matching" disabled, {@code -de} would enable both
     * {@code debug} as well as {@code extract} options.
     */
    public DefaultParser()
    {
        this.allowPartialMatching           = true;
        this.stripLeadingAndTrailingQuotes  = null;
    }

    /**
     * Create a new DefaultParser instance with the specified partial matching policy.
     *
     * <BR /><BR />By "partial matching" we mean that given the following code:
     *
     * <DIV CLASS=EXAMPLE>{@code
     * final Options options = new Options();
     * 
     * options.addOption(new Option("d", "debug", false, "Turn on debug."));
     * options.addOption(new Option("e", "extract", false, "Turn on extract."));
     * options.addOption(new Option("o", "option", true, "Turn on option with argument."));
     * }</DIV>
     *
     * <BR /><BR />with "partial matching" turned on, {@code -de} only matches the {@code "debug"}
     * option. However, with "partial matching" disabled, {@code -de} would enable both
     * {@code debug} as well as {@code extract} options.
     *
     * @param allowPartialMatching if partial matching of long options shall be enabled
     */
    public DefaultParser(final boolean allowPartialMatching)
    {
        this.allowPartialMatching           = allowPartialMatching;
        this.stripLeadingAndTrailingQuotes  = null;
    }

    /**
     * Creates a new DefaultParser instance with the specified partial matching and quote
     * stripping policy.
     *
     * @param allowPartialMatching if partial matching of long options shall be enabled
     * @param stripLeadingAndTrailingQuotes if balanced outer double quoutes should be stripped
     */
    private DefaultParser(
            final boolean allowPartialMatching,
            final Boolean stripLeadingAndTrailingQuotes
        )
    {
        this.allowPartialMatching = allowPartialMatching;
        this.stripLeadingAndTrailingQuotes = stripLeadingAndTrailingQuotes;
    }

    // Throws a {@link MissingArgumentException} if the current option didn't receive the number of
    // arguments expected.

    private void checkRequiredArgs() throws ParseException
    {
        if (currentOption != null && currentOption.requiresArg())
            throw new MissingArgumentException(currentOption);
    }

    /**
     * Throws a {@link MissingOptionException} if all of the required options are not present.
     * @throws MissingOptionException if any of the required Options are not present.
     */
    protected void checkRequiredOptions() throws MissingOptionException
    {
        // if there are required options that have not been processed
        if (!expectedOpts.isEmpty()) throw new MissingOptionException(expectedOpts);
    }

    /**
     * Searches for a prefix that is the long name of an option (-Xmx512m)
     * @param token
     */
    private String getLongPrefix(final String token)
    {
        final String t = Util.stripLeadingHyphens(token);

        int i;
        String opt = null;

        for (i = t.length() - 2; i > 1; i--)
        {
            final String prefix = t.substring(0, i);

            if (options.hasLongOption(prefix)) 
            {
                opt = prefix;
                break;
            }
        }

        return opt;
    }

    /**
     * Gets a list of matching option strings for the given token, depending on the selected
     * partial matching policy.
     *
     * @param token the token (may contain leading dashes)
     * 
     * @return the list of matching option strings or an empty list if no matching option could be
     * found
     */
    private List<String> getMatchingLongOptions(final String token)
    {
        if (allowPartialMatching) return options.getMatchingOptions(token);

        final List<String> matches = new ArrayList<>(1);

        if (options.hasLongOption(token))
        {
            final Option option = options.getOption(token);
            matches.add(option.getLongOpt());
        }

        return matches;
    }

    /**
     * Breaks {@code token} into its constituent parts using the following algorithm.
     *
     * <UL CLASS=JDUL>
     * 
     * <LI>ignore the first character ("<b>-</b>")</LI>
     * <LI>for each remaining character check if an {@link Option} exists with that id.</LI>
     * 
     * <LI> if an {@link Option} does exist then add that character prepended with "<b>-</b>" to
     *      the list of processed tokens.
     *      </LI>
     * 
     * <LI> if the {@link Option} can have an argument value and there are remaining characters in
     *      the token then add the remaining characters as a token to the list of processed tokens.
     *      </LI>
     * 
     * <LI> if an {@link Option} does <b>NOT</b> exist <b>AND</b> {@code stopAtNonOption} <b>IS</b>
     *      set then add the special token "<b>--</b>" followed by the remaining characters and
     *      also the remaining tokens directly to the processed tokens list.
     *      </LI>
     * 
     * <LI> if an {@link Option} does <b>NOT</b> exist <b>AND</b> {@code stopAtNonOption} <b>IS
     *      NOT</b> set then add that character prepended with "<b>-</b>".
     *      </LI>
     * 
     * </UL>
     *
     * @param token The current token to be <b>burst</b> at the first non-Option encountered.
     * 
     * @throws ParseException if there are any problems encountered while parsing the command line
     * token.
     */
    protected void handleConcatenatedOptions(final String token) throws ParseException
    {
        for (int i = 1; i < token.length(); i++)
        {
            final String ch = String.valueOf(token.charAt(i));

            if (!options.hasOption(ch))
            {
                handleUnknownToken(stopAtNonOption && i > 1 ? token.substring(i) : token);
                break;
            }

            handleOption(options.getOption(ch));

            if (currentOption != null && token.length() != i + 1)
            {
                // add the trail as an argument of the option
                currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(token.substring(i + 1)));
                break;
            }
        }
    }

    /**
     * Handles the following tokens: {@code --L --L=V --L V --l}
     * @param token the command line token to handle
     */
    private void handleLongOption(final String token) throws ParseException
    {
        if (token.indexOf('=') == -1)   handleLongOptionWithoutEqual(token);
        else                            handleLongOptionWithEqual(token);
    }

    /**
     * Handles the following tokens: {@code --L=V -L=V --l=V -l=V}
     * @param token the command line token to handle
     */
    private void handleLongOptionWithEqual(final String token) throws ParseException
    {
        final int       pos     = token.indexOf('=');
        final String    value   = token.substring(pos + 1);
        final String    opt     = token.substring(0, pos);

        final List<String> matchingOpts = getMatchingLongOptions(opt);

        if (matchingOpts.isEmpty())
            handleUnknownToken(currentToken);

        else if (matchingOpts.size() > 1 && !options.hasLongOption(opt))
            throw new AmbiguousOptionException(opt, matchingOpts);

        else
        {
            final String key    = options.hasLongOption(opt) ? opt : matchingOpts.get(0);
            final Option option = options.getOption(key);

            if (option.acceptsArg())
            {
                handleOption(option);

                currentOption.addValueForProcessing
                    (stripLeadingAndTrailingQuotesDefaultOff(value));

                currentOption = null;
            }

            else handleUnknownToken(currentToken);
        }
    }

    /**
     * Handles the following tokens: {@code --L -L --l -l}
     * @param token the command line token to handle
     */
    private void handleLongOptionWithoutEqual(final String token) throws ParseException
    {
        final List<String> matchingOpts = getMatchingLongOptions(token);

        if (matchingOpts.isEmpty())
            handleUnknownToken(currentToken);

        else if (matchingOpts.size() > 1 && !options.hasLongOption(token))
            throw new AmbiguousOptionException(token, matchingOpts);

        else
        {
            final String key = options.hasLongOption(token) ? token : matchingOpts.get(0);
            handleOption(options.getOption(key));
        }
    }

    private void handleOption(Option option) throws ParseException
    {
        // check the previous option before handling the next one
        checkRequiredArgs();

        option = (Option) option.clone();

        updateRequiredOptions(option);

        cmd.addOption(option);

        currentOption = option.hasArg() ? option : null;
    }

    /**
     * Sets the values of Options using the values in {@code properties}.
     * @param properties The value properties to be processed.
     */
    private void handleProperties(final Properties properties) throws ParseException
    {
        if (properties == null) return;

        for (final Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();)
        {
            final String option = e.nextElement().toString();

            final Option opt = options.getOption(option);

            if (opt == null)
                throw new UnrecognizedOptionException("Default option wasn't defined", option);

            // if the option is part of a group, check if another option of the group has been
            // selected

            final OptionGroup   group       = options.getOptionGroup(opt);
            final boolean       selected    = group != null && group.getSelected() != null;

            if (!cmd.hasOption(option) && !selected)
            {
                // get the value from the properties
                final String value = properties.getProperty(option);

                if (opt.hasArg())
                {
                    if (opt.getValues() == null || opt.getValues().length == 0)
                        opt.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(value));
                }

                else if (!(  "yes".equalsIgnoreCase(value)
                            ||  "true".equalsIgnoreCase(value)
                            ||  "1".equalsIgnoreCase(value)
                        ))

                    // if the value is not yes, true or 1 then don't add the option to the
                    // CommandLine
                    continue;

                handleOption(opt);
                currentOption = null;
            }
        }
    }

    /**
     * Handles the following tokens:
     * 
     * <BR /><BR /><UL CLASS=JDUL>
     * <LI>{@code -S -SV -S V -S=V -S1S2 -S1S2 V -SV1=V2}</LI>
     * <LI>{@code -L -LV -L V -L=V -l}</LI>
     * </UL>
     *
     * @param token the command line token to handle
     */
    private void handleShortAndLongOption(final String token) throws ParseException
    {
        final String t = Util.stripLeadingHyphens(token);

        final int pos = t.indexOf('=');

        if (t.length() == 1)
        {
            // -S
            if (options.hasShortOption(t))  handleOption(options.getOption(t));
            else                            handleUnknownToken(token);
        }

        else if (pos == -1)
        {
            // no equal sign found (-xxx)
            if (options.hasShortOption(t)) handleOption(options.getOption(t));

            else if (!getMatchingLongOptions(t).isEmpty())
                // -L or -l
                handleLongOptionWithoutEqual(token);

            else
            {
                // look for a long prefix (-Xmx512m)
                final String opt = getLongPrefix(t);

                if (opt != null && options.getOption(opt).acceptsArg())
                {
                    handleOption(options.getOption(opt));
                    currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(t.substring(opt.length())));
                    currentOption = null;
                }

                else if (isJavaProperty(t))
                {
                    // -SV1 (-Dflag)
                    handleOption(options.getOption(t.substring(0, 1)));
                    currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOff(t.substring(1)));
                    currentOption = null;
                }

                else
                    // -S1S2S3 or -S1S2V
                    handleConcatenatedOptions(token);
            }
        }

        else
        {
            // equal sign found (-xxx=yyy)
            final String opt    = t.substring(0, pos);
            final String value  = t.substring(pos + 1);

            if (opt.length() == 1)
            {
                // -S=V
                final Option option = options.getOption(opt);

                if (option != null && option.acceptsArg())
                {
                    handleOption(option);
                    currentOption.addValueForProcessing(value);
                    currentOption = null;
                }

                else handleUnknownToken(token);
            }

            else if (isJavaProperty(opt))
            {
                // -SV1=V2 (-Dkey=value)
                handleOption(options.getOption(opt.substring(0, 1)));

                currentOption.addValueForProcessing(opt.substring(1));
                currentOption.addValueForProcessing(value);

                currentOption = null;
            }

            else
                // -L=V or -l=V
                handleLongOptionWithEqual(token);
        }
    }

    /**
     * Handles any command line token.
     * @param token the command line token to handle
     * @throws ParseException
     */
    private void handleToken(final String token) throws ParseException
    {
        currentToken = token;

        if (skipParsing)                cmd.addArg(token);
        else if ("--".equals(token))    skipParsing = true;

        else if (currentOption != null && currentOption.acceptsArg() && isArgument(token))
            currentOption.addValueForProcessing(stripLeadingAndTrailingQuotesDefaultOn(token));

        else if (token.startsWith("--"))                        handleLongOption(token);
        else if (token.startsWith("-") && !"-".equals(token))   handleShortAndLongOption(token);
        else                                                    handleUnknownToken(token);

        if (currentOption != null && !currentOption.acceptsArg())
            currentOption = null;
    }

    /**
     * Handles an unknown token. If the token starts with a dash an UnrecognizedOptionException is thrown. Otherwise the
     * token is added to the arguments of the command line. If the stopAtNonOption flag is set, this stops the parsing and
     * the remaining tokens are added as-is in the arguments of the command line.
     *
     * @param token the command line token to handle
     */
    private void handleUnknownToken(final String token) throws ParseException
    {
        if (token.startsWith("-") && token.length() > 1 && !stopAtNonOption)
            throw new UnrecognizedOptionException("Unrecognized option: " + token, token);

        cmd.addArg(token);

        if (stopAtNonOption) skipParsing = true;
    }

    /**
     * Tests if the token is a valid argument.
     * @param token
     */
    private boolean isArgument(final String token)
    { return !isOption(token) || isNegativeNumber(token); }

    // Tests if the specified token is a Java-like property (-Dkey=value).
    private boolean isJavaProperty(final String token)
    {
        final String opt    = token.isEmpty() ? null : token.substring(0, 1);
        final Option option = options.getOption(opt);

        return      option != null
                &&  (option.getArgs() >= 2 || option.getArgs() == Option.UNLIMITED_VALUES);
    }

    // Tests if the token looks like a long option.
    private boolean isLongOption(final String token)
    {
        if (token == null || !token.startsWith("-") || token.length() == 1) return false;

        final int pos = token.indexOf("=");

        final String t = pos == -1 ? token : token.substring(0, pos);

        if (!getMatchingLongOptions(t).isEmpty())

            // long or partial long options (--L, -L, --L=V, -L=V, --l, --l=V)
            return true;

        if (getLongPrefix(token) != null && !token.startsWith("--"))

            // -LV
            return true;

        return false;
    }

    // Tests if the token is a negative number.
    private boolean isNegativeNumber(final String token)
    {
        try
        {
            Double.parseDouble(token);
            return true;
        }

        catch (final NumberFormatException e)
            { return false; }
    }

    // Tests if the token looks like an option.
    private boolean isOption(final String token)
    { return isLongOption(token) || isShortOption(token); }

    // Tests if the token looks like a short option.
    private boolean isShortOption(final String token)
    {
        // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
        if (token == null || !token.startsWith("-") || token.length() == 1) return false;

        // remove leading "-" and "=value"
        final int pos = token.indexOf("=");
        final String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);

        if (options.hasShortOption(optName)) return true;

        // check for several concatenated short options
        return      !optName.isEmpty()
                &&  options.hasShortOption(String.valueOf(optName.charAt(0)));
    }

    @Override
    public CommandLine parse(final Options options, final String[] arguments)
        throws ParseException
    { return parse(options, arguments, null); }

    @Override
    public CommandLine parse
        (final Options options, final String[] arguments, final boolean stopAtNonOption)
        throws ParseException
    { return parse(options, arguments, null, stopAtNonOption); }

    /**
     * Parses the arguments according to the specified options and properties.
     * @param options the specified Options
     * @param arguments the command line arguments
     * @param properties command line option name-value pairs
     * @return the list of atomic option and value tokens
     *
     * @throws ParseException if there are any problems encountered while parsing the command line
     * tokens.
     */
    public CommandLine parse
        (final Options options, final String[] arguments, final Properties properties)
        throws ParseException
    { return parse(options, arguments, properties, false); }

    /**
     * Parses the arguments according to the specified options and properties.
     * @param options the specified Options
     * @param arguments the command line arguments
     * @param properties command line option name-value pairs
     * 
     * @param stopAtNonOption if {@code true} an unrecognized argument stops the parsing and the
     * remaining arguments are added to the {@link CommandLine}s args list. If {@code false} an
     * unrecognized argument triggers a ParseException.
     *
     * @return the list of atomic option and value tokens
     * 
     * @throws ParseException if there are any problems encountered while parsing the command line
     * tokens.
     */
    public CommandLine parse(
            final Options       options,
            final String[]      arguments,
            final Properties    properties,
            final boolean       stopAtNonOption
        )
        throws ParseException
    {
        this.options            = options;
        this.stopAtNonOption    = stopAtNonOption;

        skipParsing     = false;
        currentOption   = null;
        expectedOpts    = new ArrayList<>(options.getRequiredOptions());

        // clear the data from the groups
        for (final OptionGroup group : options.getOptionGroups()) group.setSelected(null);

        cmd = new CommandLine();

        if (arguments != null)
            for (final String argument : arguments) handleToken(argument);

        // check the arguments of the last option
        checkRequiredArgs();

        // add the default options
        handleProperties(properties);

        checkRequiredOptions();

        return cmd;
    }

    /**
     * Strips balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
     * If stripLeadingAndTrailingQuotes is null, then do not strip
     *
     * @param token a string
     * @return token with the quotes stripped (if set)
     */
    private String stripLeadingAndTrailingQuotesDefaultOff(final String token)
    {
        if (stripLeadingAndTrailingQuotes != null && stripLeadingAndTrailingQuotes)
            return Util.stripLeadingAndTrailingQuotes(token);

        return token;
    }

    /**
     * Strips balanced leading and trailing quotes if the stripLeadingAndTrailingQuotes is set
     * If stripLeadingAndTrailingQuotes is null, then do not strip
     *
     * @param token a string
     * @return token with the quotes stripped (if set)
     */
    private String stripLeadingAndTrailingQuotesDefaultOn(final String token)
    {
        if (stripLeadingAndTrailingQuotes == null || stripLeadingAndTrailingQuotes)
            return Util.stripLeadingAndTrailingQuotes(token);

        return token;
    }

    // Removes the option or its group from the list of expected elements.
    private void updateRequiredOptions(final Option option) throws AlreadySelectedException
    {
        if (option.isRequired()) expectedOpts.remove(option.getKey());

        // if the option is in an OptionGroup make that option the selected option of the group
        if (options.getOptionGroup(option) != null)
        {
            final OptionGroup group = options.getOptionGroup(option);

            if (group.isRequired()) expectedOpts.remove(group);

            group.setSelected(option);
        }
    }
}