001 /*--------------------------------------------------------------------------+
002 $Id: CommandLine.java 26283 2010-02-18 11:18:57Z juergens $
003 | |
004 | Copyright 2005-2010 Technische Universitaet Muenchen |
005 | |
006 | Licensed under the Apache License, Version 2.0 (the "License"); |
007 | you may not use this file except in compliance with the License. |
008 | You may obtain a copy of the License at |
009 | |
010 | http://www.apache.org/licenses/LICENSE-2.0 |
011 | |
012 | Unless required by applicable law or agreed to in writing, software |
013 | distributed under the License is distributed on an "AS IS" BASIS, |
014 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
015 | See the License for the specific language governing permissions and |
016 | limitations under the License. |
017 +--------------------------------------------------------------------------*/
018 package edu.tum.cs.commons.options;
019
020 import java.io.PrintWriter;
021 import java.util.ArrayList;
022 import java.util.Collections;
023 import java.util.List;
024
025 import edu.tum.cs.commons.reflect.TypeConversionException;
026 import edu.tum.cs.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.
039 *
040 * @author Benjamin Hummel
041 * @author $Author: juergens $
042 *
043 * @version $Rev: 26283 $
044 * @levd.rating GREEN Hash: 11D10A0EF71752B4224881C9DF088659
045 */
046 public class CommandLine {
047
048 /** Registry containing the options to be used by this instance */
049 private final OptionRegistry registry;
050
051 /**
052 * Constructor.
053 *
054 * @param registry
055 * Registry containing the options to be used by this instance.
056 */
057 public CommandLine(OptionRegistry registry) {
058 this.registry = registry;
059 }
060
061 /**
062 * Parses the given command line parameters and applies the options found.
063 * The arguments not treated as options or parameters are returned (often
064 * they are treated as file arguments). If the syntax does not conform to
065 * the options in the registry, an {@link OptionException} is thrown.
066 *
067 * @param args
068 * the command line arguments to be parsed.
069 * @return the remaining arguments.
070 * @throws OptionException
071 * in case of syntax errors or invalid parameters.
072 */
073 public String[] parse(String[] args) throws OptionException {
074 return parse(new CommandLineTokenStream(args));
075 }
076
077 /**
078 * Parses the command line parameters implicitly given by the token stream
079 * and applies the options found. The arguments not treated as options or
080 * parameters are returned (often they are treated as file arguments). If
081 * the syntax does not conform to the options in the registry an
082 * IllegalArgumentException is thrown.
083 *
084 * @param ts
085 * Token stream containing the arguments.
086 * @return Remaining arguments.
087 * @throws OptionException
088 * in case of syntax errors or invalid parameters.
089 */
090 public String[] parse(CommandLineTokenStream ts) throws OptionException {
091 List<String> fileArgs = new ArrayList<String>();
092
093 while (ts.hasNext()) {
094 if (ts.nextIsLongOption()) {
095 String name = ts.nextLongOption();
096 OptionApplicator applicator = registry.getLongOption(name);
097 applyOption(applicator, formatLongOption(name), ts);
098 } else if (ts.nextIsShortOption()) {
099 char name = ts.nextShortOption();
100 OptionApplicator applicator = registry.getShortOption(name);
101 applyOption(applicator, formatShortOption(name), ts);
102 } else if (ts.nextIsFileArgument()) {
103 fileArgs.add(ts.next());
104 } else {
105 throw new OptionException("Unexpected command line argument: "
106 + ts.next());
107 }
108 }
109
110 String[] result = new String[fileArgs.size()];
111 return fileArgs.toArray(result);
112 }
113
114 /**
115 * Applies an option and tests for various errors.
116 *
117 * @param applicator
118 * the applicator for the option.
119 * @param optionName
120 * the name of the option.
121 * @param ts
122 * the token stream used to get additional parameters.
123 */
124 private void applyOption(OptionApplicator applicator, String optionName,
125 CommandLineTokenStream ts) throws OptionException {
126 if (applicator == null) {
127 throw new OptionException("Unknown option: " + optionName);
128 }
129 if (applicator.requiresParameter()) {
130 if (!ts.nextIsParameter()) {
131 throw new OptionException("Missing argument for option: "
132 + optionName);
133 }
134
135 do {
136 String parameter = ts.next();
137 try {
138 applicator.applyOption(parameter);
139 } catch (TypeConversionException e) {
140 throw new OptionException("Parameter " + parameter
141 + " for option " + optionName
142 + " is not of required type!");
143 }
144 } while (applicator.isGreedy() && ts.hasNext()
145 && !(ts.nextIsLongOption() || ts.nextIsShortOption()));
146 } else {
147 applicator.applyOption();
148 }
149 }
150
151 /**
152 * Print the list of all supported options using reasonable default values
153 * for widths.
154 *
155 * @param pw
156 * the writer used for output.
157 */
158 public void printUsage(PrintWriter pw) {
159 printUsage(pw, 20, 80);
160 }
161
162 /**
163 * Print the list of all supported options.
164 *
165 * @param pw
166 * the writer to print to.
167 * @param firstCol
168 * the width of the first column containing the option name
169 * (without the trailing space).
170 * @param width
171 * the maximal width of a line (aka terminal width).
172 */
173 public void printUsage(PrintWriter pw, int firstCol, int width) {
174 List<AOption> sortedOptions = new ArrayList<AOption>(registry
175 .getAllOptions());
176 Collections.sort(sortedOptions, new AOptionComparator());
177
178 for (AOption option : sortedOptions) {
179 printOption(option, pw, firstCol, width);
180 }
181 pw.flush();
182 }
183
184 /**
185 * Print a single option.
186 *
187 * @param option
188 * the option to be printed.
189 * @param pw
190 * the writer to print to.
191 * @param firstCol
192 * the width of the first column containing the option name
193 * (without the trailing space).
194 * @param width
195 * the maximal width of a line (aka terminal width).
196 */
197 private void printOption(AOption option, PrintWriter pw, int firstCol,
198 int width) {
199 String names = formatNames(option);
200 pw.print(names);
201
202 // start new line (if name too long for firstCol) or indent correctly
203 int pos = names.length();
204 if (pos > firstCol) {
205 pos = width + 1;
206 } else {
207 pw.print(StringUtils.fillString(firstCol - pos, ' '));
208 }
209
210 // Format description using lines no longer than width
211 String indent = StringUtils.fillString(firstCol, ' ');
212 String[] words = option.description().split("\\s+");
213 for (String word : words) {
214 if (pos + 1 + word.length() > width) {
215 pw.println();
216 pw.print(indent);
217 pos = firstCol;
218 }
219 pw.print(' ');
220 pw.print(word);
221 pos += 1 + word.length();
222 }
223 pw.println();
224 }
225
226 /**
227 * Format the names of an option for output.
228 *
229 * @param option
230 * the options to format.
231 * @return the formatted string.
232 */
233 private String formatNames(AOption option) {
234 String names = " ";
235 if (option.shortName() == 0) {
236 names += StringUtils.fillString(
237 2 + formatShortOption('x').length(), ' ');
238 } else {
239 names += formatShortOption(option.shortName());
240 if (option.longName().length() > 0) {
241 names += ", ";
242 }
243 }
244 if (option.longName().length() > 0) {
245 names += formatLongOption(option.longName());
246 }
247 return names;
248 }
249
250 /**
251 * Returns the user visible name for the given long option.
252 *
253 * @param name
254 * the name of the option to format.
255 * @return the user visible name for the given long option.
256 */
257 private String formatLongOption(String name) {
258 return "--" + name;
259 }
260
261 /**
262 * Returns the user visible name for the given short option.
263 *
264 * @param name
265 * the name of the option to format.
266 * @return the user visible name for the given short option.
267 */
268 private String formatShortOption(char name) {
269 return "-" + name;
270 }
271 }