1 package org.kohsuke.args4j;
2
3 import org.kohsuke.args4j.spi.EnumOptionHandler;
4 import org.kohsuke.args4j.spi.OptionHandler;
5 import org.kohsuke.args4j.spi.Parameters;
6 import org.kohsuke.args4j.spi.Setter;
7 import org.kohsuke.args4j.spi.BooleanOptionHandler;
8 import org.kohsuke.args4j.spi.FileOptionHandler;
9 import org.kohsuke.args4j.spi.StringOptionHandler;
10 import org.kohsuke.args4j.spi.IntOptionHandler;
11 import org.kohsuke.args4j.spi.DoubleOptionHandler;
12
13 import java.io.OutputStream;
14 import java.io.OutputStreamWriter;
15 import java.io.PrintWriter;
16 import java.io.Writer;
17 import java.io.File;
18 import java.lang.reflect.Constructor;
19 import java.lang.reflect.Field;
20 import java.lang.reflect.InvocationTargetException;
21 import java.lang.reflect.Method;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.ResourceBundle;
27 import java.util.TreeMap;
28 import java.util.Set;
29 import java.util.HashSet;
30
31
32 /***
33 * Command line argument owner.
34 *
35 * <p>
36 * For a typical usage, see <a href="https://args4j.dev.java.net/source/browse/args4j/args4j/examples/SampleMain.java?view=markup">this example</a>.
37 *
38 * @author
39 * Kohsuke Kawaguchi (kk@kohsuke.org)
40 */
41 public class CmdLineParser {
42 /***
43 * Option bean instance.
44 */
45 private final Object bean;
46
47 /***
48 * Discovered {@link OptionHandler}s keyed by their option names.
49 */
50 private final Map<String,OptionHandler> options = new TreeMap<String,OptionHandler>();
51
52 /***
53 * {@link Setter} that accepts the arguments.
54 */
55 private Setter argumentSetter;
56
57 /***
58 * Creates a new command line owner that
59 * parses arguments/options and set them into
60 * the given object.
61 *
62 * @param bean
63 * instance of a class annotated by {@link Option} and {@link Argument}.
64 * this object will receive values.
65 *
66 * @throws IllegalAnnotationError
67 * if the option bean class is using args4j annotations incorrectly.
68 */
69 public CmdLineParser(Object bean) {
70 this.bean = bean;
71
72
73 for( Class c=bean.getClass(); c!=null; c=c.getSuperclass()) {
74 for( Method m : c.getDeclaredMethods() ) {
75 Option o = m.getAnnotation(Option.class);
76 if(o!=null) {
77 addOption(new MethodSetter(bean,m),o);
78 }
79 Argument a = m.getAnnotation(Argument.class);
80 if(a!=null) {
81 addArgument(new MethodSetter(bean,m));
82 }
83 }
84
85 for( Field f : c.getDeclaredFields() ) {
86 Option o = f.getAnnotation(Option.class);
87 if(o!=null) {
88 addOption(createFieldSetter(f),o);
89 }
90 Argument a = f.getAnnotation(Argument.class);
91 if(a!=null) {
92 addArgument(createFieldSetter(f));
93 }
94 }
95 }
96 }
97
98 private Setter createFieldSetter(Field f) {
99 if(List.class.isAssignableFrom(f.getType()))
100 return new MultiValueFieldSetter(bean,f);
101 else
102 return new FieldSetter(bean,f);
103 }
104
105 private void addArgument(Setter setter) {
106 if(argumentSetter!=null)
107 throw new IllegalAnnotationError("@Argument is used more than once");
108 argumentSetter = setter;
109 }
110
111 private void addOption(Setter setter, Option o) {
112 OptionHandler h = createOptionHandler(o,setter);
113 if(options.put(h.option.name(),h)!=null) {
114 throw new IllegalAnnotationError("Option name "+h.option.name()+" is used more than once");
115 }
116 }
117
118 /***
119 * Creates an {@link OptionHandler} that handles the given {@link Option} annotation
120 * and the {@link Setter} instance.
121 */
122 protected OptionHandler createOptionHandler(Option o, Setter setter) {
123
124 Constructor<? extends OptionHandler> handlerType;
125 Class<? extends OptionHandler> h = o.handler();
126 if(h==OptionHandler.class) {
127
128
129
130 Class t = setter.getType();
131 if(Enum.class.isAssignableFrom(t))
132 return new EnumOptionHandler(this,o,setter,t);
133
134 handlerType = handlerClasses.get(t);
135 if(handlerType==null)
136 throw new IllegalAnnotationError("No OptionHandler is registered to handle "+t);
137 } else {
138 handlerType = getConstructor(h);
139 }
140
141 try {
142 return handlerType.newInstance(this,o,setter);
143 } catch (InstantiationException e) {
144 throw new IllegalAnnotationError(e);
145 } catch (IllegalAccessException e) {
146 throw new IllegalAnnotationError(e);
147 } catch (InvocationTargetException e) {
148 throw new IllegalAnnotationError(e);
149 }
150 }
151
152 /***
153 * Formats a command line example into a string.
154 *
155 * See {@link #printExample(ExampleMode, ResourceBundle)} for more details.
156 *
157 * @param mode
158 * must not be null.
159 * @return
160 * always non-null.
161 */
162 public String printExample(ExampleMode mode) {
163 return printExample(mode,null);
164 }
165
166 /***
167 * Formats a command line example into a string.
168 *
169 * <p>
170 * This method produces a string like " -d <dir> -v -b",
171 * which is useful for printing a command line example, perhaps
172 * as a part of the usage screen.
173 *
174 *
175 * @param mode
176 * One of the {@link ExampleMode} constants. Must not be null.
177 * This determines what option should be a part of the returned string.
178 * @param rb
179 * If non-null, meta variables (<dir> in the above example)
180 * is treated as a key to this resource bundle, and the associated
181 * value is printed. See {@link Option#metaVar()}. This is to support
182 * localization.
183 *
184 * Passing <tt>null</tt> would print {@link Option#metaVar()} directly.
185 * @return
186 * always non-null. If there's no option, this method returns
187 * just the empty string "". Otherwise, this method returns a
188 * string that contains a space at the beginning (but not at the end.)
189 * This allows you to do something like:
190 *
191 * <pre>System.err.println("java -jar my.jar"+parser.printExample(REQUIRED)+" arg1 arg2");</pre>
192 */
193 public String printExample(ExampleMode mode,ResourceBundle rb) {
194 StringBuilder buf = new StringBuilder();
195
196 for (Map.Entry<String, OptionHandler> e : options.entrySet()) {
197 Option option = e.getValue().option;
198 if(option.usage().length()==0) continue;
199 if(!mode.print(option)) continue;
200
201 buf.append(' ');
202 buf.append(e.getKey());
203
204 String metaVar = e.getValue().getMetaVariable(rb);
205 if(metaVar!=null) {
206 buf.append(' ').append(metaVar);
207 }
208 }
209
210 return buf.toString();
211 }
212
213 /***
214 * Prints the list of options and their usages to the screen.
215 *
216 * <p>
217 * This is a convenience method for calling {@code printUsage(new OutputStreamWriter(out),null)}
218 * so that you can do {@code printUsage(System.err)}.
219 */
220 public void printUsage(OutputStream out) {
221 printUsage(new OutputStreamWriter(out),null);
222 }
223 /***
224 * Prints the list of options and their usages to the screen.
225 *
226 * @param rb
227 * if this is non-null, {@link Option#usage()} is treated
228 * as a key to obtain the actual message from this resource bundle.
229 */
230 public void printUsage(Writer out, ResourceBundle rb) {
231 PrintWriter w = new PrintWriter(out);
232
233 int len = 0;
234 for (Map.Entry<String, OptionHandler> e : options.entrySet()) {
235 String usage = e.getValue().option.usage();
236 if(usage.length()==0) continue;
237
238 String metaVar = e.getValue().getMetaVariable(rb);
239 int metaLen = (metaVar!=null?metaVar.length()+1:0);
240 len = Math.max(len,e.getKey().length()+metaLen);
241 }
242
243 int descriptionWidth = 72-len-4;
244
245
246 for (Map.Entry<String, OptionHandler> e : options.entrySet()) {
247 String usage = e.getValue().option.usage();
248 if(usage.length()==0) continue;
249
250 String option = e.getKey();
251 int headLen = option.length();
252 w.print(' ');
253 w.print(option);
254
255 String metaVar = e.getValue().getMetaVariable(rb);
256 if(metaVar!=null) {
257 w.print(' ');
258 w.print(metaVar);
259 headLen += metaVar.length()+1;
260 }
261 for( ; headLen<len; headLen++ )
262 w.print(' ');
263 w.print(" : ");
264
265 if(rb!=null)
266 usage = rb.getString(usage);
267
268 while(usage!=null && usage.length()>0) {
269 int idx = usage.indexOf('\n');
270 if(idx>=0 && idx<=descriptionWidth) {
271 w.println(usage.substring(0,idx));
272 usage = usage.substring(idx+1);
273 if(usage.length()>0)
274 indent(w,len+4);
275 continue;
276 }
277 if(usage.length()<=descriptionWidth) {
278 w.println(usage);
279 break;
280 }
281
282 w.println(usage.substring(0,descriptionWidth));
283 usage = usage.substring(descriptionWidth);
284 indent(w,len+4);
285 }
286 }
287
288 w.flush();
289 }
290
291 private void indent(PrintWriter w, int i) {
292 for( ; i>0; i-- )
293 w.print(' ');
294 }
295
296
297 /***
298 * Essentially a pointer over a {@link String} array.
299 * Can move forward, can look ahead.
300 */
301 private class CmdLineImpl extends Parameters {
302 private final String[] args;
303 private int pos;
304
305 CmdLineImpl( String[] args ) {
306 this.args = args;
307 pos = 0;
308 }
309
310 private boolean hasMore() {
311 return pos<args.length;
312 }
313
314 private String getCurrentToken() {
315 return args[pos];
316 }
317
318 private void proceed( int n ) {
319 pos += n;
320 }
321
322
323 public String getOptionName() {
324 return getCurrentToken();
325 }
326
327 public String getParameter(int idx) throws CmdLineException {
328 if( pos+idx+1>=args.length )
329 throw new CmdLineException(Messages.MISSING_OPERAND.format(getOptionName()));
330 return args[pos+idx+1];
331 }
332
333 public int getParameterCount() {
334 return args.length-(pos+1);
335 }
336 }
337
338 /***
339 * Parses the command line arguments and set them to the option bean
340 * given in the constructor.
341 *
342 * @throws CmdLineException
343 * if there's any error parsing arguments, or if
344 * {@link Option#required() required} option was not given.
345 */
346 public void parseArgument(final String... args) throws CmdLineException {
347 CmdLineImpl cmdLine = new CmdLineImpl(args);
348
349 Set<OptionHandler> present = new HashSet<OptionHandler>();
350
351 while( cmdLine.hasMore() ) {
352 String arg = cmdLine.getOptionName();
353 if( isOption(arg) ) {
354
355 OptionHandler handler = options.get(arg);
356 if(handler!=null) {
357
358 int diff = handler.parseArguments(cmdLine);
359 cmdLine.proceed(diff+1);
360 present.add(handler);
361 continue;
362 }
363
364
365
366 throw new CmdLineException(Messages.UNDEFINED_OPTION.format(arg));
367 } else {
368
369 if(argumentSetter==null)
370 throw new CmdLineException(Messages.NO_ARGUMENT_ALLOWED.format(arg));
371 argumentSetter.addValue(arg);
372 cmdLine.proceed(1);
373 }
374 }
375
376
377 for (OptionHandler handler : options.values())
378 if(handler.option.required() && !present.contains(handler))
379 throw new CmdLineException(Messages.REQUIRED_OPTION_MISSING.format(handler.option.name()));
380 }
381
382 /***
383 * Returns true if the given token is an option
384 * (as opposed to an argument.)
385 */
386 protected boolean isOption(String arg) {
387 return arg.startsWith("-");
388 }
389
390
391 /***
392 * All {@link OptionHandler}s known to the {@link CmdLineParser}.
393 *
394 * Constructors of {@link OptionHandler}-derived class keyed by their supported types.
395 */
396 private static final Map<Class,Constructor<? extends OptionHandler>> handlerClasses =
397 Collections.synchronizedMap(new HashMap<Class,Constructor<? extends OptionHandler>>());
398
399 /***
400 * Registers a user-defined {@link OptionHandler} class with args4j.
401 *
402 * <p>
403 * This method allows users to extend the behavior of args4j by writing
404 * their own {@link OptionHandler} implementation.
405 *
406 * @param valueType
407 * The specified handler is used when the field/method annotated by {@link Option}
408 * is of this type.
409 * @param handlerClass
410 * This class must have the constructor that has the same signature as
411 * {@link OptionHandler#OptionHandler(CmdLineParser, Option, Setter)}.
412 */
413 public static void registerHandler( Class valueType, Class<? extends OptionHandler> handlerClass ) {
414 if(valueType==null || handlerClass==null)
415 throw new IllegalArgumentException();
416 if(!OptionHandler.class.isAssignableFrom(handlerClass))
417 throw new IllegalArgumentException("Not an OptionHandler class");
418
419 Constructor<? extends OptionHandler> c = getConstructor(handlerClass);
420 handlerClasses.put(valueType,c);
421 }
422
423 private static Constructor<? extends OptionHandler> getConstructor(Class<? extends OptionHandler> handlerClass) {
424 try {
425 return handlerClass.getConstructor(CmdLineParser.class, Option.class, Setter.class);
426 } catch (NoSuchMethodException e) {
427 throw new IllegalArgumentException(handlerClass+" does not have the proper constructor");
428 }
429 }
430
431 static {
432 registerHandler(Boolean.class,BooleanOptionHandler.class);
433 registerHandler(boolean.class,BooleanOptionHandler.class);
434 registerHandler(File.class,FileOptionHandler.class);
435 registerHandler(Integer.class,IntOptionHandler.class);
436 registerHandler(int.class,IntOptionHandler.class);
437 registerHandler(Double.class, DoubleOptionHandler.class);
438 registerHandler(double.class,DoubleOptionHandler.class);
439 registerHandler(String.class,StringOptionHandler.class);
440
441 }
442 }