001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.cli;
019
020import java.util.ArrayList;
021import java.util.Enumeration;
022import java.util.List;
023import java.util.Properties;
024
025/**
026 * Default parser.
027 * 
028 * @version $Id: DefaultParser.java 1783175 2017-02-16 07:52:05Z britter $
029 * @since 1.3
030 */
031public class DefaultParser implements CommandLineParser
032{
033    /** The command-line instance. */
034    protected CommandLine cmd;
035    
036    /** The current options. */
037    protected Options options;
038
039    /**
040     * Flag indicating how unrecognized tokens are handled. <tt>true</tt> to stop
041     * the parsing and add the remaining tokens to the args list.
042     * <tt>false</tt> to throw an exception. 
043     */
044    protected boolean stopAtNonOption;
045
046    /** The token currently processed. */
047    protected String currentToken;
048 
049    /** The last option parsed. */
050    protected Option currentOption;
051 
052    /** Flag indicating if tokens should no longer be analyzed and simply added as arguments of the command line. */
053    protected boolean skipParsing;
054 
055    /** The required options and groups expected to be found when parsing the command line. */
056    protected List expectedOpts;
057 
058    public CommandLine parse(Options options, String[] arguments) throws ParseException
059    {
060        return parse(options, arguments, null);
061    }
062
063    /**
064     * Parse the arguments according to the specified options and properties.
065     *
066     * @param options    the specified Options
067     * @param arguments  the command line arguments
068     * @param properties command line option name-value pairs
069     * @return the list of atomic option and value tokens
070     *
071     * @throws ParseException if there are any problems encountered
072     * while parsing the command line tokens.
073     */
074    public CommandLine parse(Options options, String[] arguments, Properties properties) throws ParseException
075    {
076        return parse(options, arguments, properties, false);
077    }
078
079    public CommandLine parse(Options options, String[] arguments, boolean stopAtNonOption) throws ParseException
080    {
081        return parse(options, arguments, null, stopAtNonOption);
082    }
083
084    /**
085     * Parse the arguments according to the specified options and properties.
086     *
087     * @param options         the specified Options
088     * @param arguments       the command line arguments
089     * @param properties      command line option name-value pairs
090     * @param stopAtNonOption if <tt>true</tt> an unrecognized argument stops
091     *     the parsing and the remaining arguments are added to the 
092     *     {@link CommandLine}s args list. If <tt>false</tt> an unrecognized
093     *     argument triggers a ParseException.
094     *
095     * @return the list of atomic option and value tokens
096     * @throws ParseException if there are any problems encountered
097     * while parsing the command line tokens.
098     */
099    public CommandLine parse(Options options, String[] arguments, Properties properties, boolean stopAtNonOption)
100            throws ParseException
101    {
102        this.options = options;
103        this.stopAtNonOption = stopAtNonOption;
104        skipParsing = false;
105        currentOption = null;
106        expectedOpts = new ArrayList(options.getRequiredOptions());
107
108        // clear the data from the groups
109        for (OptionGroup group : options.getOptionGroups())
110        {
111            group.setSelected(null);
112        }
113
114        cmd = new CommandLine();
115
116        if (arguments != null)
117        {
118            for (String argument : arguments)
119            {
120                handleToken(argument);
121            }
122        }
123
124        // check the arguments of the last option
125        checkRequiredArgs();
126
127        // add the default options
128        handleProperties(properties);
129
130        checkRequiredOptions();
131
132        return cmd;
133    }
134
135    /**
136     * Sets the values of Options using the values in <code>properties</code>.
137     *
138     * @param properties The value properties to be processed.
139     */
140    private void handleProperties(Properties properties) throws ParseException
141    {
142        if (properties == null)
143        {
144            return;
145        }
146
147        for (Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();)
148        {
149            String option = e.nextElement().toString();
150
151            Option opt = options.getOption(option);
152            if (opt == null)
153            {
154                throw new UnrecognizedOptionException("Default option wasn't defined", option);
155            }
156
157            // if the option is part of a group, check if another option of the group has been selected
158            OptionGroup group = options.getOptionGroup(opt);
159            boolean selected = group != null && group.getSelected() != null;
160
161            if (!cmd.hasOption(option) && !selected)
162            {
163                // get the value from the properties
164                String value = properties.getProperty(option);
165
166                if (opt.hasArg())
167                {
168                    if (opt.getValues() == null || opt.getValues().length == 0)
169                    {
170                        opt.addValueForProcessing(value);
171                    }
172                }
173                else if (!("yes".equalsIgnoreCase(value)
174                        || "true".equalsIgnoreCase(value)
175                        || "1".equalsIgnoreCase(value)))
176                {
177                    // if the value is not yes, true or 1 then don't add the option to the CommandLine
178                    continue;
179                }
180
181                handleOption(opt);
182                currentOption = null;
183            }
184        }
185    }
186
187    /**
188     * Throws a {@link MissingOptionException} if all of the required options
189     * are not present.
190     *
191     * @throws MissingOptionException if any of the required Options
192     * are not present.
193     */
194    private void checkRequiredOptions() throws MissingOptionException
195    {
196        // if there are required options that have not been processed
197        if (!expectedOpts.isEmpty())
198        {
199            throw new MissingOptionException(expectedOpts);
200        }
201    }
202
203    /**
204     * Throw a {@link MissingArgumentException} if the current option
205     * didn't receive the number of arguments expected.
206     */
207    private void checkRequiredArgs() throws ParseException
208    {
209        if (currentOption != null && currentOption.requiresArg())
210        {
211            throw new MissingArgumentException(currentOption);
212        }
213    }
214
215    /**
216     * Handle any command line token.
217     *
218     * @param token the command line token to handle
219     * @throws ParseException
220     */
221    private void handleToken(String token) throws ParseException
222    {
223        currentToken = token;
224
225        if (skipParsing)
226        {
227            cmd.addArg(token);
228        }
229        else if ("--".equals(token))
230        {
231            skipParsing = true;
232        }
233        else if (currentOption != null && currentOption.acceptsArg() && isArgument(token))
234        {
235            currentOption.addValueForProcessing(Util.stripLeadingAndTrailingQuotes(token));
236        }
237        else if (token.startsWith("--"))
238        {
239            handleLongOption(token);
240        }
241        else if (token.startsWith("-") && !"-".equals(token))
242        {
243            handleShortAndLongOption(token);
244        }
245        else
246        {
247            handleUnknownToken(token);
248        }
249
250        if (currentOption != null && !currentOption.acceptsArg())
251        {
252            currentOption = null;
253        }
254    }
255
256    /**
257     * Returns true is the token is a valid argument.
258     *
259     * @param token
260     */
261    private boolean isArgument(String token)
262    {
263        return !isOption(token) || isNegativeNumber(token);
264    }
265
266    /**
267     * Check if the token is a negative number.
268     *
269     * @param token
270     */
271    private boolean isNegativeNumber(String token)
272    {
273        try
274        {
275            Double.parseDouble(token);
276            return true;
277        }
278        catch (NumberFormatException e)
279        {
280            return false;
281        }
282    }
283
284    /**
285     * Tells if the token looks like an option.
286     *
287     * @param token
288     */
289    private boolean isOption(String token)
290    {
291        return isLongOption(token) || isShortOption(token);
292    }
293
294    /**
295     * Tells if the token looks like a short option.
296     * 
297     * @param token
298     */
299    private boolean isShortOption(String token)
300    {
301        // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
302        if (!token.startsWith("-") || token.length() == 1)
303        {
304            return false;
305        }
306
307        // remove leading "-" and "=value"
308        int pos = token.indexOf("=");
309        String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);
310        if (options.hasShortOption(optName))
311        {
312            return true;
313        }
314        // check for several concatenated short options
315        return optName.length() > 0 && options.hasShortOption(String.valueOf(optName.charAt(0)));
316    }
317
318    /**
319     * Tells if the token looks like a long option.
320     *
321     * @param token
322     */
323    private boolean isLongOption(String token)
324    {
325        if (!token.startsWith("-") || token.length() == 1)
326        {
327            return false;
328        }
329
330        int pos = token.indexOf("=");
331        String t = pos == -1 ? token : token.substring(0, pos);
332
333        if (!options.getMatchingOptions(t).isEmpty())
334        {
335            // long or partial long options (--L, -L, --L=V, -L=V, --l, --l=V)
336            return true;
337        }
338        else if (getLongPrefix(token) != null && !token.startsWith("--"))
339        {
340            // -LV
341            return true;
342        }
343
344        return false;
345    }
346
347    /**
348     * Handles an unknown token. If the token starts with a dash an 
349     * UnrecognizedOptionException is thrown. Otherwise the token is added 
350     * to the arguments of the command line. If the stopAtNonOption flag 
351     * is set, this stops the parsing and the remaining tokens are added 
352     * as-is in the arguments of the command line.
353     *
354     * @param token the command line token to handle
355     */
356    private void handleUnknownToken(String token) throws ParseException
357    {
358        if (token.startsWith("-") && token.length() > 1 && !stopAtNonOption)
359        {
360            throw new UnrecognizedOptionException("Unrecognized option: " + token, token);
361        }
362
363        cmd.addArg(token);
364        if (stopAtNonOption)
365        {
366            skipParsing = true;
367        }
368    }
369
370    /**
371     * Handles the following tokens:
372     *
373     * --L
374     * --L=V
375     * --L V
376     * --l
377     *
378     * @param token the command line token to handle
379     */
380    private void handleLongOption(String token) throws ParseException
381    {
382        if (token.indexOf('=') == -1)
383        {
384            handleLongOptionWithoutEqual(token);
385        }
386        else
387        {
388            handleLongOptionWithEqual(token);
389        }
390    }
391
392    /**
393     * Handles the following tokens:
394     *
395     * --L
396     * -L
397     * --l
398     * -l
399     * 
400     * @param token the command line token to handle
401     */
402    private void handleLongOptionWithoutEqual(String token) throws ParseException
403    {
404        List<String> matchingOpts = options.getMatchingOptions(token);
405        if (matchingOpts.isEmpty())
406        {
407            handleUnknownToken(currentToken);
408        }
409        else if (matchingOpts.size() > 1)
410        {
411            throw new AmbiguousOptionException(token, matchingOpts);
412        }
413        else
414        {
415            handleOption(options.getOption(matchingOpts.get(0)));
416        }
417    }
418
419    /**
420     * Handles the following tokens:
421     *
422     * --L=V
423     * -L=V
424     * --l=V
425     * -l=V
426     *
427     * @param token the command line token to handle
428     */
429    private void handleLongOptionWithEqual(String token) throws ParseException
430    {
431        int pos = token.indexOf('=');
432
433        String value = token.substring(pos + 1);
434
435        String opt = token.substring(0, pos);
436
437        List<String> matchingOpts = options.getMatchingOptions(opt);
438        if (matchingOpts.isEmpty())
439        {
440            handleUnknownToken(currentToken);
441        }
442        else if (matchingOpts.size() > 1)
443        {
444            throw new AmbiguousOptionException(opt, matchingOpts);
445        }
446        else
447        {
448            Option option = options.getOption(matchingOpts.get(0));
449
450            if (option.acceptsArg())
451            {
452                handleOption(option);
453                currentOption.addValueForProcessing(value);
454                currentOption = null;
455            }
456            else
457            {
458                handleUnknownToken(currentToken);
459            }
460        }
461    }
462
463    /**
464     * Handles the following tokens:
465     *
466     * -S
467     * -SV
468     * -S V
469     * -S=V
470     * -S1S2
471     * -S1S2 V
472     * -SV1=V2
473     *
474     * -L
475     * -LV
476     * -L V
477     * -L=V
478     * -l
479     *
480     * @param token the command line token to handle
481     */
482    private void handleShortAndLongOption(String token) throws ParseException
483    {
484        String t = Util.stripLeadingHyphens(token);
485
486        int pos = t.indexOf('=');
487
488        if (t.length() == 1)
489        {
490            // -S
491            if (options.hasShortOption(t))
492            {
493                handleOption(options.getOption(t));
494            }
495            else
496            {
497                handleUnknownToken(token);
498            }
499        }
500        else if (pos == -1)
501        {
502            // no equal sign found (-xxx)
503            if (options.hasShortOption(t))
504            {
505                handleOption(options.getOption(t));
506            }
507            else if (!options.getMatchingOptions(t).isEmpty())
508            {
509                // -L or -l
510                handleLongOptionWithoutEqual(token);
511            }
512            else
513            {
514                // look for a long prefix (-Xmx512m)
515                String opt = getLongPrefix(t);
516
517                if (opt != null && options.getOption(opt).acceptsArg())
518                {
519                    handleOption(options.getOption(opt));
520                    currentOption.addValueForProcessing(t.substring(opt.length()));
521                    currentOption = null;
522                }
523                else if (isJavaProperty(t))
524                {
525                    // -SV1 (-Dflag)
526                    handleOption(options.getOption(t.substring(0, 1)));
527                    currentOption.addValueForProcessing(t.substring(1));
528                    currentOption = null;
529                }
530                else
531                {
532                    // -S1S2S3 or -S1S2V
533                    handleConcatenatedOptions(token);
534                }
535            }
536        }
537        else
538        {
539            // equal sign found (-xxx=yyy)
540            String opt = t.substring(0, pos);
541            String value = t.substring(pos + 1);
542
543            if (opt.length() == 1)
544            {
545                // -S=V
546                Option option = options.getOption(opt);
547                if (option != null && option.acceptsArg())
548                {
549                    handleOption(option);
550                    currentOption.addValueForProcessing(value);
551                    currentOption = null;
552                }
553                else
554                {
555                    handleUnknownToken(token);
556                }
557            }
558            else if (isJavaProperty(opt))
559            {
560                // -SV1=V2 (-Dkey=value)
561                handleOption(options.getOption(opt.substring(0, 1)));
562                currentOption.addValueForProcessing(opt.substring(1));
563                currentOption.addValueForProcessing(value);
564                currentOption = null;
565            }
566            else
567            {
568                // -L=V or -l=V
569                handleLongOptionWithEqual(token);
570            }
571        }
572    }
573
574    /**
575     * Search for a prefix that is the long name of an option (-Xmx512m)
576     *
577     * @param token
578     */
579    private String getLongPrefix(String token)
580    {
581        String t = Util.stripLeadingHyphens(token);
582
583        int i;
584        String opt = null;
585        for (i = t.length() - 2; i > 1; i--)
586        {
587            String prefix = t.substring(0, i);
588            if (options.hasLongOption(prefix))
589            {
590                opt = prefix;
591                break;
592            }
593        }
594        
595        return opt;
596    }
597
598    /**
599     * Check if the specified token is a Java-like property (-Dkey=value).
600     */
601    private boolean isJavaProperty(String token)
602    {
603        String opt = token.substring(0, 1);
604        Option option = options.getOption(opt);
605
606        return option != null && (option.getArgs() >= 2 || option.getArgs() == Option.UNLIMITED_VALUES);
607    }
608
609    private void handleOption(Option option) throws ParseException
610    {
611        // check the previous option before handling the next one
612        checkRequiredArgs();
613
614        option = (Option) option.clone();
615
616        updateRequiredOptions(option);
617
618        cmd.addOption(option);
619
620        if (option.hasArg())
621        {
622            currentOption = option;
623        }
624        else
625        {
626            currentOption = null;
627        }
628    }
629
630    /**
631     * Removes the option or its group from the list of expected elements.
632     *
633     * @param option
634     */
635    private void updateRequiredOptions(Option option) throws AlreadySelectedException
636    {
637        if (option.isRequired())
638        {
639            expectedOpts.remove(option.getKey());
640        }
641
642        // if the option is in an OptionGroup make that option the selected option of the group
643        if (options.getOptionGroup(option) != null)
644        {
645            OptionGroup group = options.getOptionGroup(option);
646
647            if (group.isRequired())
648            {
649                expectedOpts.remove(group);
650            }
651
652            group.setSelected(option);
653        }
654    }
655
656    /**
657     * Breaks <code>token</code> into its constituent parts
658     * using the following algorithm.
659     *
660     * <ul>
661     *  <li>ignore the first character ("<b>-</b>")</li>
662     *  <li>for each remaining character check if an {@link Option}
663     *  exists with that id.</li>
664     *  <li>if an {@link Option} does exist then add that character
665     *  prepended with "<b>-</b>" to the list of processed tokens.</li>
666     *  <li>if the {@link Option} can have an argument value and there
667     *  are remaining characters in the token then add the remaining
668     *  characters as a token to the list of processed tokens.</li>
669     *  <li>if an {@link Option} does <b>NOT</b> exist <b>AND</b>
670     *  <code>stopAtNonOption</code> <b>IS</b> set then add the special token
671     *  "<b>--</b>" followed by the remaining characters and also
672     *  the remaining tokens directly to the processed tokens list.</li>
673     *  <li>if an {@link Option} does <b>NOT</b> exist <b>AND</b>
674     *  <code>stopAtNonOption</code> <b>IS NOT</b> set then add that
675     *  character prepended with "<b>-</b>".</li>
676     * </ul>
677     *
678     * @param token The current token to be <b>burst</b>
679     * at the first non-Option encountered.
680     * @throws ParseException if there are any problems encountered
681     *                        while parsing the command line token.
682     */
683    protected void handleConcatenatedOptions(String token) throws ParseException
684    {
685        for (int i = 1; i < token.length(); i++)
686        {
687            String ch = String.valueOf(token.charAt(i));
688
689            if (options.hasOption(ch))
690            {
691                handleOption(options.getOption(ch));
692
693                if (currentOption != null && token.length() != i + 1)
694                {
695                    // add the trail as an argument of the option
696                    currentOption.addValueForProcessing(token.substring(i + 1));
697                    break;
698                }
699            }
700            else
701            {
702                handleUnknownToken(stopAtNonOption && i > 1 ? token.substring(i) : token);
703                break;
704            }
705        }
706    }
707}