001/*-------------------------------------------------------------------------+
002|                                                                          |
003| Copyright 2005-2011 The ConQAT Project                                   |
004|                                                                          |
005| Licensed under the Apache License, Version 2.0 (the "License");          |
006| you may not use this file except in compliance with the License.         |
007| 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+-------------------------------------------------------------------------*/
017package org.conqat.lib.commons.options;
018
019import java.io.PrintWriter;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.List;
023
024import org.conqat.lib.commons.collections.CollectionUtils;
025import org.conqat.lib.commons.reflect.TypeConversionException;
026import org.conqat.lib.commons.string.StringUtils;
027
028/**
029 * A class providing command line parsing and usage messages using GNU syntax.
030 * <p>
031 * The GNU syntax is implemented as follows. There are short (single character)
032 * and long (multi character) options, just as provided by the AOption
033 * annotation. Short options are introduced using a single minus (e.g. '-h')
034 * while long options are introduced using a double minus (e.g. '--help'). The
035 * parameter for an option is either the next argument, or--in case of long
036 * options--possibly separated by an equals sign (e.g. '--file=test.txt'). Short
037 * options may be chained (e.g. '-xvf abc' instead of '-x -v -f abc'). For
038 * chained short options, only the last option may take a parameter. The
039 * separator '--' may be used to switch off argument processing for the
040 * remaining arguments, i.e. all other arguments are treated as left-overs.
041 */
042public class CommandLine {
043
044        /** Registry containing the options to be used by this instance */
045        private final OptionRegistry registry;
046
047        /**
048         * Constructor.
049         *
050         * @param registry
051         *            Registry containing the options to be used by this instance.
052         */
053        public CommandLine(OptionRegistry registry) {
054                this.registry = registry;
055        }
056
057        /**
058         * Parses the given command line parameters and applies the options found. The
059         * arguments not treated as options or parameters are returned (often they are
060         * treated as file arguments). If the syntax does not conform to the options in
061         * the registry, an {@link OptionException} is thrown.
062         *
063         * @param args
064         *            the command line arguments to be parsed.
065         * @return the remaining arguments.
066         * @throws OptionException
067         *             in case of syntax errors or invalid parameters.
068         */
069        public String[] parse(String[] args) throws OptionException {
070                return parse(new CommandLineTokenStream(args));
071        }
072
073        /**
074         * Parses the command line parameters implicitly given by the token stream and
075         * applies the options found. The arguments not treated as options or parameters
076         * are returned (often they are treated as file arguments). If the syntax does
077         * not conform to the options in the registry an IllegalArgumentException is
078         * thrown.
079         *
080         * @param ts
081         *            Token stream containing the arguments.
082         * @return Remaining arguments.
083         * @throws OptionException
084         *             in case of syntax errors or invalid parameters.
085         */
086        public String[] parse(CommandLineTokenStream ts) throws OptionException {
087                List<String> remainingArgs = new ArrayList<>();
088
089                while (ts.hasNext()) {
090                        if (ts.nextIsSeparator()) {
091                                // discard separator '--'
092                                ts.next();
093                                // and swallow everything else
094                                while (ts.hasNext()) {
095                                        remainingArgs.add(ts.next());
096                                }
097                        } else if (ts.nextIsLongOption()) {
098                                String name = ts.nextLongOption();
099                                OptionApplicator applicator = registry.getLongOption(name);
100                                applyOption(applicator, formatLongOption(name), ts);
101                        } else if (ts.nextIsShortOption()) {
102                                char name = ts.nextShortOption();
103                                OptionApplicator applicator = registry.getShortOption(name);
104                                applyOption(applicator, formatShortOption(name), ts);
105                        } else if (ts.nextIsFileArgument()) {
106                                remainingArgs.add(ts.next());
107                        } else {
108                                throw new OptionException("Unexpected command line argument: " + ts.next());
109                        }
110                }
111
112                return CollectionUtils.toArray(remainingArgs, String.class);
113        }
114
115        /**
116         * Applies an option and tests for various errors.
117         *
118         * @param applicator
119         *            the applicator for the option.
120         * @param optionName
121         *            the name of the option.
122         * @param ts
123         *            the token stream used to get additional parameters.
124         */
125        private static void applyOption(OptionApplicator applicator, String optionName, CommandLineTokenStream ts)
126                        throws OptionException {
127                if (applicator == null) {
128                        throw new OptionException("Unknown option: " + optionName);
129                }
130                if (applicator.requiresParameter()) {
131                        if (!ts.nextIsParameter()) {
132                                throw new OptionException("Missing argument for option: " + optionName);
133                        }
134
135                        do {
136                                String parameter = ts.next();
137                                try {
138                                        applicator.applyOption(parameter);
139                                } catch (TypeConversionException e) {
140                                        throw new OptionException(
141                                                        "Parameter " + parameter + " for option " + optionName + " is not of required type!");
142                                }
143                        } while (applicator.isGreedy() && ts.hasNext() && !(ts.nextIsLongOption() || ts.nextIsShortOption()));
144                } else {
145                        applicator.applyOption();
146                }
147        }
148
149        /**
150         * Print the list of all supported options using reasonable default values for
151         * widths.
152         *
153         * @param pw
154         *            the writer used for output.
155         */
156        public void printUsage(PrintWriter pw) {
157                printUsage(pw, 20, 80);
158        }
159
160        /**
161         * Print the list of all supported options.
162         *
163         * @param pw
164         *            the writer to print to.
165         * @param firstCol
166         *            the width of the first column containing the option name (without
167         *            the trailing space).
168         * @param width
169         *            the maximal width of a line (aka terminal width).
170         */
171        public void printUsage(PrintWriter pw, int firstCol, int width) {
172                List<Option> sortedOptions = new ArrayList<>(registry.getAllOptions());
173                Collections.sort(sortedOptions, new AOptionComparator());
174
175                for (Option option : sortedOptions) {
176                        printOption(option, pw, firstCol, width);
177                }
178                pw.flush();
179        }
180
181        /**
182         * Print a single option.
183         *
184         * @param option
185         *            the option to be printed.
186         * @param pw
187         *            the writer to print to.
188         * @param firstCol
189         *            the width of the first column containing the option name (without
190         *            the trailing space).
191         * @param width
192         *            the maximal width of a line (aka terminal width).
193         */
194        private static void printOption(Option option, PrintWriter pw, int firstCol, int width) {
195                String names = formatNames(option);
196                pw.print(names);
197
198                // start new line (if name too long for firstCol) or indent correctly
199                int pos = names.length();
200                if (pos > firstCol) {
201                        pos = width + 1;
202                } else {
203                        pw.print(StringUtils.fillString(firstCol - pos, ' '));
204                }
205
206                // Format description using lines no longer than width
207                String indent = StringUtils.fillString(firstCol, ' ');
208                String[] words = option.description().split("\\s+");
209                for (String word : words) {
210                        if (pos + 1 + word.length() > width) {
211                                pw.println();
212                                pw.print(indent);
213                                pos = firstCol;
214                        }
215                        pw.print(' ');
216                        pw.print(word);
217                        pos += 1 + word.length();
218                }
219                pw.println();
220        }
221
222        /**
223         * Format the names of an option for output.
224         *
225         * @param option
226         *            the options to format.
227         * @return the formatted string.
228         */
229        private static String formatNames(Option option) {
230                String names = "  ";
231                if (option.shortName() == 0) {
232                        names += StringUtils.fillString(2 + formatShortOption('x').length(), ' ');
233                } else {
234                        names += formatShortOption(option.shortName());
235                        if (option.longName().length() > 0) {
236                                names += ", ";
237                        }
238                }
239                if (option.longName().length() > 0) {
240                        names += formatLongOption(option.longName());
241                }
242                return names;
243        }
244
245        /**
246         * Returns the user visible name for the given long option.
247         *
248         * @param name
249         *            the name of the option to format.
250         * @return the user visible name for the given long option.
251         */
252        private static String formatLongOption(String name) {
253                return "--" + name;
254        }
255
256        /**
257         * Returns the user visible name for the given short option.
258         *
259         * @param name
260         *            the name of the option to format.
261         * @return the user visible name for the given short option.
262         */
263        private static String formatShortOption(char name) {
264                return "-" + name;
265        }
266}