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 1677406 2015-05-03 14:27:31Z 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 analysed 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        return token.startsWith("-") && token.length() >= 2 && options.hasShortOption(token.substring(1, 2));
303    }
304
305    /**
306     * Tells if the token looks like a long option.
307     *
308     * @param token
309     */
310    private boolean isLongOption(String token)
311    {
312        if (!token.startsWith("-") || token.length() == 1)
313        {
314            return false;
315        }
316
317        int pos = token.indexOf("=");
318        String t = pos == -1 ? token : token.substring(0, pos);
319
320        if (!options.getMatchingOptions(t).isEmpty())
321        {
322            // long or partial long options (--L, -L, --L=V, -L=V, --l, --l=V)
323            return true;
324        }
325        else if (getLongPrefix(token) != null && !token.startsWith("--"))
326        {
327            // -LV
328            return true;
329        }
330
331        return false;
332    }
333
334    /**
335     * Handles an unknown token. If the token starts with a dash an 
336     * UnrecognizedOptionException is thrown. Otherwise the token is added 
337     * to the arguments of the command line. If the stopAtNonOption flag 
338     * is set, this stops the parsing and the remaining tokens are added 
339     * as-is in the arguments of the command line.
340     *
341     * @param token the command line token to handle
342     */
343    private void handleUnknownToken(String token) throws ParseException
344    {
345        if (token.startsWith("-") && token.length() > 1 && !stopAtNonOption)
346        {
347            throw new UnrecognizedOptionException("Unrecognized option: " + token, token);
348        }
349
350        cmd.addArg(token);
351        if (stopAtNonOption)
352        {
353            skipParsing = true;
354        }
355    }
356
357    /**
358     * Handles the following tokens:
359     *
360     * --L
361     * --L=V
362     * --L V
363     * --l
364     *
365     * @param token the command line token to handle
366     */
367    private void handleLongOption(String token) throws ParseException
368    {
369        if (token.indexOf('=') == -1)
370        {
371            handleLongOptionWithoutEqual(token);
372        }
373        else
374        {
375            handleLongOptionWithEqual(token);
376        }
377    }
378
379    /**
380     * Handles the following tokens:
381     *
382     * --L
383     * -L
384     * --l
385     * -l
386     * 
387     * @param token the command line token to handle
388     */
389    private void handleLongOptionWithoutEqual(String token) throws ParseException
390    {
391        List<String> matchingOpts = options.getMatchingOptions(token);
392        if (matchingOpts.isEmpty())
393        {
394            handleUnknownToken(currentToken);
395        }
396        else if (matchingOpts.size() > 1)
397        {
398            throw new AmbiguousOptionException(token, matchingOpts);
399        }
400        else
401        {
402            handleOption(options.getOption(matchingOpts.get(0)));
403        }
404    }
405
406    /**
407     * Handles the following tokens:
408     *
409     * --L=V
410     * -L=V
411     * --l=V
412     * -l=V
413     *
414     * @param token the command line token to handle
415     */
416    private void handleLongOptionWithEqual(String token) throws ParseException
417    {
418        int pos = token.indexOf('=');
419
420        String value = token.substring(pos + 1);
421
422        String opt = token.substring(0, pos);
423
424        List<String> matchingOpts = options.getMatchingOptions(opt);
425        if (matchingOpts.isEmpty())
426        {
427            handleUnknownToken(currentToken);
428        }
429        else if (matchingOpts.size() > 1)
430        {
431            throw new AmbiguousOptionException(opt, matchingOpts);
432        }
433        else
434        {
435            Option option = options.getOption(matchingOpts.get(0));
436
437            if (option.acceptsArg())
438            {
439                handleOption(option);
440                currentOption.addValueForProcessing(value);
441                currentOption = null;
442            }
443            else
444            {
445                handleUnknownToken(currentToken);
446            }
447        }
448    }
449
450    /**
451     * Handles the following tokens:
452     *
453     * -S
454     * -SV
455     * -S V
456     * -S=V
457     * -S1S2
458     * -S1S2 V
459     * -SV1=V2
460     *
461     * -L
462     * -LV
463     * -L V
464     * -L=V
465     * -l
466     *
467     * @param token the command line token to handle
468     */
469    private void handleShortAndLongOption(String token) throws ParseException
470    {
471        String t = Util.stripLeadingHyphens(token);
472
473        int pos = t.indexOf('=');
474
475        if (t.length() == 1)
476        {
477            // -S
478            if (options.hasShortOption(t))
479            {
480                handleOption(options.getOption(t));
481            }
482            else
483            {
484                handleUnknownToken(token);
485            }
486        }
487        else if (pos == -1)
488        {
489            // no equal sign found (-xxx)
490            if (options.hasShortOption(t))
491            {
492                handleOption(options.getOption(t));
493            }
494            else if (!options.getMatchingOptions(t).isEmpty())
495            {
496                // -L or -l
497                handleLongOptionWithoutEqual(token);
498            }
499            else
500            {
501                // look for a long prefix (-Xmx512m)
502                String opt = getLongPrefix(t);
503
504                if (opt != null && options.getOption(opt).acceptsArg())
505                {
506                    handleOption(options.getOption(opt));
507                    currentOption.addValueForProcessing(t.substring(opt.length()));
508                    currentOption = null;
509                }
510                else if (isJavaProperty(t))
511                {
512                    // -SV1 (-Dflag)
513                    handleOption(options.getOption(t.substring(0, 1)));
514                    currentOption.addValueForProcessing(t.substring(1));
515                    currentOption = null;
516                }
517                else
518                {
519                    // -S1S2S3 or -S1S2V
520                    handleConcatenatedOptions(token);
521                }
522            }
523        }
524        else
525        {
526            // equal sign found (-xxx=yyy)
527            String opt = t.substring(0, pos);
528            String value = t.substring(pos + 1);
529
530            if (opt.length() == 1)
531            {
532                // -S=V
533                Option option = options.getOption(opt);
534                if (option != null && option.acceptsArg())
535                {
536                    handleOption(option);
537                    currentOption.addValueForProcessing(value);
538                    currentOption = null;
539                }
540                else
541                {
542                    handleUnknownToken(token);
543                }
544            }
545            else if (isJavaProperty(opt))
546            {
547                // -SV1=V2 (-Dkey=value)
548                handleOption(options.getOption(opt.substring(0, 1)));
549                currentOption.addValueForProcessing(opt.substring(1));
550                currentOption.addValueForProcessing(value);
551                currentOption = null;
552            }
553            else
554            {
555                // -L=V or -l=V
556                handleLongOptionWithEqual(token);
557            }
558        }
559    }
560
561    /**
562     * Search for a prefix that is the long name of an option (-Xmx512m)
563     *
564     * @param token
565     */
566    private String getLongPrefix(String token)
567    {
568        String t = Util.stripLeadingHyphens(token);
569
570        int i;
571        String opt = null;
572        for (i = t.length() - 2; i > 1; i--)
573        {
574            String prefix = t.substring(0, i);
575            if (options.hasLongOption(prefix))
576            {
577                opt = prefix;
578                break;
579            }
580        }
581        
582        return opt;
583    }
584
585    /**
586     * Check if the specified token is a Java-like property (-Dkey=value).
587     */
588    private boolean isJavaProperty(String token)
589    {
590        String opt = token.substring(0, 1);
591        Option option = options.getOption(opt);
592
593        return option != null && (option.getArgs() >= 2 || option.getArgs() == Option.UNLIMITED_VALUES);
594    }
595
596    private void handleOption(Option option) throws ParseException
597    {
598        // check the previous option before handling the next one
599        checkRequiredArgs();
600
601        option = (Option) option.clone();
602
603        updateRequiredOptions(option);
604
605        cmd.addOption(option);
606
607        if (option.hasArg())
608        {
609            currentOption = option;
610        }
611        else
612        {
613            currentOption = null;
614        }
615    }
616
617    /**
618     * Removes the option or its group from the list of expected elements.
619     *
620     * @param option
621     */
622    private void updateRequiredOptions(Option option) throws AlreadySelectedException
623    {
624        if (option.isRequired())
625        {
626            expectedOpts.remove(option.getKey());
627        }
628
629        // if the option is in an OptionGroup make that option the selected option of the group
630        if (options.getOptionGroup(option) != null)
631        {
632            OptionGroup group = options.getOptionGroup(option);
633
634            if (group.isRequired())
635            {
636                expectedOpts.remove(group);
637            }
638
639            group.setSelected(option);
640        }
641    }
642
643    /**
644     * Breaks <code>token</code> into its constituent parts
645     * using the following algorithm.
646     *
647     * <ul>
648     *  <li>ignore the first character ("<b>-</b>")</li>
649     *  <li>foreach remaining character check if an {@link Option}
650     *  exists with that id.</li>
651     *  <li>if an {@link Option} does exist then add that character
652     *  prepended with "<b>-</b>" to the list of processed tokens.</li>
653     *  <li>if the {@link Option} can have an argument value and there
654     *  are remaining characters in the token then add the remaining
655     *  characters as a token to the list of processed tokens.</li>
656     *  <li>if an {@link Option} does <b>NOT</b> exist <b>AND</b>
657     *  <code>stopAtNonOption</code> <b>IS</b> set then add the special token
658     *  "<b>--</b>" followed by the remaining characters and also
659     *  the remaining tokens directly to the processed tokens list.</li>
660     *  <li>if an {@link Option} does <b>NOT</b> exist <b>AND</b>
661     *  <code>stopAtNonOption</code> <b>IS NOT</b> set then add that
662     *  character prepended with "<b>-</b>".</li>
663     * </ul>
664     *
665     * @param token The current token to be <b>burst</b>
666     * at the first non-Option encountered.
667     * @throws ParseException if there are any problems encountered
668     *                        while parsing the command line token.
669     */
670    protected void handleConcatenatedOptions(String token) throws ParseException
671    {
672        for (int i = 1; i < token.length(); i++)
673        {
674            String ch = String.valueOf(token.charAt(i));
675
676            if (options.hasOption(ch))
677            {
678                handleOption(options.getOption(ch));
679
680                if (currentOption != null && token.length() != i + 1)
681                {
682                    // add the trail as an argument of the option
683                    currentOption.addValueForProcessing(token.substring(i + 1));
684                    break;
685                }
686            }
687            else
688            {
689                handleUnknownToken(stopAtNonOption && i > 1 ? token.substring(i) : token);
690                break;
691            }
692        }
693    }
694}