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