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