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.Arrays;
022import java.util.Enumeration;
023import java.util.List;
024import java.util.ListIterator;
025import java.util.Properties;
026
027/**
028 * {@code Parser} creates {@link CommandLine}s.
029 *
030 * @deprecated since 1.3, the two-pass parsing with the flatten method is not enough flexible to handle complex cases
031 */
032@Deprecated
033public abstract class Parser implements CommandLineParser {
034    /** CommandLine instance */
035    protected CommandLine cmd;
036
037    /** current Options */
038    private Options options;
039
040    /** list of required options strings */
041    private List requiredOptions;
042
043    /**
044     * Throws a {@link MissingOptionException} if all of the required options are not present.
045     *
046     * @throws MissingOptionException if any of the required Options are not present.
047     */
048    protected void checkRequiredOptions() throws MissingOptionException {
049        // if there are required options that have not been processed
050        if (!getRequiredOptions().isEmpty()) {
051            throw new MissingOptionException(getRequiredOptions());
052        }
053    }
054
055    /**
056     * Subclasses must implement this method to reduce the {@code arguments} that have been passed to the parse method.
057     *
058     * @param opts The Options to parse the arguments by.
059     * @param arguments The arguments that have to be flattened.
060     * @param stopAtNonOption specifies whether to stop flattening when a non option has been encountered
061     * @return a String array of the flattened arguments
062     * @throws ParseException if there are any problems encountered while parsing the command line tokens.
063     */
064    protected abstract String[] flatten(Options opts, String[] arguments, boolean stopAtNonOption) throws ParseException;
065
066    protected Options getOptions() {
067        return options;
068    }
069
070    protected List getRequiredOptions() {
071        return requiredOptions;
072    }
073
074    /**
075     * Parses the specified {@code arguments} based on the specified {@link Options}.
076     *
077     * @param options the {@code Options}
078     * @param arguments the {@code arguments}
079     * @return the {@code CommandLine}
080     * @throws ParseException if there are any problems encountered while parsing the command line tokens.
081     */
082    @Override
083    public CommandLine parse(final Options options, final String[] arguments) throws ParseException {
084        return parse(options, arguments, null, false);
085    }
086
087    /**
088     * Parses the specified {@code arguments} based on the specified {@link Options}.
089     *
090     * @param options the {@code Options}
091     * @param arguments the {@code arguments}
092     * @param stopAtNonOption if {@code true} an unrecognized argument stops the parsing and the remaining arguments
093     *        are added to the {@link CommandLine}s args list. If {@code false} an unrecognized argument triggers a
094     *        ParseException.
095     * @return the {@code CommandLine}
096     * @throws ParseException if an error occurs when parsing the arguments.
097     */
098    @Override
099    public CommandLine parse(final Options options, final String[] arguments, final boolean stopAtNonOption) throws ParseException {
100        return parse(options, arguments, null, stopAtNonOption);
101    }
102
103    /**
104     * Parse the arguments according to the specified options and properties.
105     *
106     * @param options the specified Options
107     * @param arguments the command line arguments
108     * @param properties command line option name-value pairs
109     * @return the list of atomic option and value tokens
110     * @throws ParseException if there are any problems encountered while parsing the command line tokens.
111     *
112     * @since 1.1
113     */
114    public CommandLine parse(final Options options, final String[] arguments, final Properties properties) throws ParseException {
115        return parse(options, arguments, properties, false);
116    }
117
118    /**
119     * Parse the arguments according to the specified options and properties.
120     *
121     * @param options the specified Options
122     * @param arguments the command line arguments
123     * @param properties command line option name-value pairs
124     * @param stopAtNonOption if {@code true} an unrecognized argument stops the parsing and the remaining arguments
125     *        are added to the {@link CommandLine}s args list. If {@code false} an unrecognized argument triggers a
126     *        ParseException.
127     *
128     * @return the list of atomic option and value tokens
129     *
130     * @throws ParseException if there are any problems encountered while parsing the command line tokens.
131     *
132     * @since 1.1
133     */
134    public CommandLine parse(final Options options, String[] arguments, final Properties properties, final boolean stopAtNonOption) throws ParseException {
135        // clear out the data in options in case it's been used before (CLI-71)
136        for (final Option opt : options.helpOptions()) {
137            opt.clearValues();
138        }
139
140        // clear the data from the groups
141        for (final OptionGroup group : options.getOptionGroups()) {
142            group.setSelected(null);
143        }
144
145        // initialize members
146        setOptions(options);
147
148        cmd = new CommandLine();
149
150        boolean eatTheRest = false;
151
152        if (arguments == null) {
153            arguments = new String[0];
154        }
155
156        final List<String> tokenList = Arrays.asList(flatten(getOptions(), arguments, stopAtNonOption));
157
158        final ListIterator<String> iterator = tokenList.listIterator();
159
160        // process each flattened token
161        while (iterator.hasNext()) {
162            final String t = iterator.next();
163
164            // the value is the double-dash
165            if ("--".equals(t)) {
166                eatTheRest = true;
167            }
168
169            // the value is a single dash
170            else if ("-".equals(t)) {
171                if (stopAtNonOption) {
172                    eatTheRest = true;
173                } else {
174                    cmd.addArg(t);
175                }
176            }
177
178            // the value is an option
179            else if (t.startsWith("-")) {
180                if (stopAtNonOption && !getOptions().hasOption(t)) {
181                    eatTheRest = true;
182                    cmd.addArg(t);
183                } else {
184                    processOption(t, iterator);
185                }
186            }
187
188            // the value is an argument
189            else {
190                cmd.addArg(t);
191
192                if (stopAtNonOption) {
193                    eatTheRest = true;
194                }
195            }
196
197            // eat the remaining tokens
198            if (eatTheRest) {
199                while (iterator.hasNext()) {
200                    final String str = iterator.next();
201
202                    // ensure only one double-dash is added
203                    if (!"--".equals(str)) {
204                        cmd.addArg(str);
205                    }
206                }
207            }
208        }
209
210        processProperties(properties);
211        checkRequiredOptions();
212
213        return cmd;
214    }
215
216    /**
217     * Process the argument values for the specified Option {@code opt} using the values retrieved from the specified
218     * iterator {@code iter}.
219     *
220     * @param opt The current Option
221     * @param iter The iterator over the flattened command line Options.
222     *
223     * @throws ParseException if an argument value is required and it is has not been found.
224     */
225    public void processArgs(final Option opt, final ListIterator<String> iter) throws ParseException {
226        // loop until an option is found
227        while (iter.hasNext()) {
228            final String str = iter.next();
229
230            // found an Option, not an argument
231            if (getOptions().hasOption(str) && str.startsWith("-")) {
232                iter.previous();
233                break;
234            }
235
236            // found a value
237            try {
238                opt.addValueForProcessing(Util.stripLeadingAndTrailingQuotes(str));
239            } catch (final RuntimeException exp) {
240                iter.previous();
241                break;
242            }
243        }
244
245        if (opt.getValues() == null && !opt.hasOptionalArg()) {
246            throw new MissingArgumentException(opt);
247        }
248    }
249
250    /**
251     * Process the Option specified by {@code arg} using the values retrieved from the specified iterator
252     * {@code iter}.
253     *
254     * @param arg The String value representing an Option
255     * @param iter The iterator over the flattened command line arguments.
256     *
257     * @throws ParseException if {@code arg} does not represent an Option
258     */
259    protected void processOption(final String arg, final ListIterator<String> iter) throws ParseException {
260        final boolean hasOption = getOptions().hasOption(arg);
261
262        // if there is no option throw an UnrecognizedOptionException
263        if (!hasOption) {
264            throw new UnrecognizedOptionException("Unrecognized option: " + arg, arg);
265        }
266
267        // get the option represented by arg
268        final Option opt = (Option) getOptions().getOption(arg).clone();
269
270        // update the required options and groups
271        updateRequiredOptions(opt);
272
273        // if the option takes an argument value
274        if (opt.hasArg()) {
275            processArgs(opt, iter);
276        }
277
278        // set the option on the command line
279        cmd.addOption(opt);
280    }
281
282    /**
283     * Sets the values of Options using the values in {@code properties}.
284     *
285     * @param properties The value properties to be processed.
286     * @throws ParseException if there are any problems encountered while processing the properties.
287     */
288    protected void processProperties(final Properties properties) throws ParseException {
289        if (properties == null) {
290            return;
291        }
292
293        for (final Enumeration<?> e = properties.propertyNames(); e.hasMoreElements();) {
294            final String option = e.nextElement().toString();
295
296            final Option opt = options.getOption(option);
297            if (opt == null) {
298                throw new UnrecognizedOptionException("Default option wasn't defined", option);
299            }
300
301            // if the option is part of a group, check if another option of the group has been selected
302            final OptionGroup group = options.getOptionGroup(opt);
303            final boolean selected = group != null && group.getSelected() != null;
304
305            if (!cmd.hasOption(option) && !selected) {
306                // get the value from the properties instance
307                final String value = properties.getProperty(option);
308
309                if (opt.hasArg()) {
310                    if (opt.getValues() == null || opt.getValues().length == 0) {
311                        try {
312                            opt.addValueForProcessing(value);
313                        } catch (final RuntimeException exp) { // NOPMD
314                            // if we cannot add the value don't worry about it
315                        }
316                    }
317                } else if (!("yes".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value) || "1".equalsIgnoreCase(value))) {
318                    // if the value is not yes, true or 1 then don't add the
319                    // option to the CommandLine
320                    continue;
321                }
322
323                cmd.addOption(opt);
324                updateRequiredOptions(opt);
325            }
326        }
327    }
328
329    protected void setOptions(final Options options) {
330        this.options = options;
331        this.requiredOptions = new ArrayList<>(options.getRequiredOptions());
332    }
333
334    /**
335     * Removes the option or its group from the list of expected elements.
336     *
337     * @param opt
338     */
339    private void updateRequiredOptions(final Option opt) throws ParseException {
340        // if the option is a required option remove the option from
341        // the requiredOptions list
342        if (opt.isRequired()) {
343            getRequiredOptions().remove(opt.getKey());
344        }
345
346        // if the option is in an OptionGroup make that option the selected
347        // option of the group
348        if (getOptions().getOptionGroup(opt) != null) {
349            final OptionGroup group = getOptions().getOptionGroup(opt);
350
351            if (group.isRequired()) {
352                getRequiredOptions().remove(group);
353            }
354
355            group.setSelected(opt);
356        }
357    }
358}