1 /*
2  * BasicCmdLineHandler.java
3  *
4  * Classes:
5  *   public   BasicCmdLineHandler
6  *
7  * ***** BEGIN LICENSE BLOCK *****
8  * Version: MPL 1.1
9  *
10  * The contents of this file are subject to the Mozilla Public License Version
11  * 1.1 (the "License"); you may not use this file except in compliance with
12  * the License. You may obtain a copy of the License at
13  * http://www.mozilla.org/MPL/
14  *
15  * Software distributed under the License is distributed on an "AS IS" basis,
16  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
17  * for the specific language governing rights and limitations under the
18  * License.
19  *
20  * The Original Code is the Java jcmdline (command line management) package.
21  *
22  * The Initial Developer of the Original Code is Lynne Lawrence.
23  *
24  * Portions created by the Initial Developer are Copyright (C) 2002
25  * the Initial Developer. All Rights Reserved.
26  *
27  * Contributor(s):  Lynne Lawrence <lgl@visuallink.com>
28  *
29  * ***** END LICENSE BLOCK *****
30  */
31 
32 package jcmdline;
33 
34 import java.util.ArrayList;
35 import java.util.Collection;
36 import java.util.HashMap;
37 import java.util.List;
38 
39 /**
40  * Used to define, parse, and validate the parameters associated with an
41  * executable's command line.
42  * <P>
43  * Objects of this class use a {@link CmdLineParser} to do the actual parsing of
44  * the command line. By default, this class uses a {@link PosixCmdLineParser}.
45  * <p>
46  * The following semantics are used throughout the documentation for this class.
47  * A command line <i>parameter</i> refers to either a command line
48  * <i>option</i>, or a command line <i>argument</i>.
49  * <P>
50  * A command line <i>option</i> is identified by a <i>tag</i> and may or not
51  * have an associated value. For instance, in the following posix-type option,
52  * "outfile" is the <i>tag</i> and "/tmp/myoutfile" is the <i>value</i>:
53  *
54  * <pre>
55  * --outfile / tmp / myoutfile
56  * </pre>
57  * <P>
58  * Command line <i>arguments</i> are what are left on the command line after all
59  * options have been processed. For example, again using a posix style command
60  * line, "filename1" and "filename2" would be the <i>arguments</i>:
61  *
62  * <pre>
63  *  -e 's/red/blue/' filename1 filename2
64  * </pre>
65  * <P>
66  * Parameters may be designated as <i>hidden</i>. <i>Hidden parameters</i> are
67  * those that can be specified, but whose documentation is not displayed to the
68  * user when a normal usage is printed.
69  * <P>
70  * This class is used as in the following example of a 'cat' facsimile in java:
71  *
72  * <pre>
73  * public class Concat {
74  * 	static FileParam outfile = new FileParam(&quot;out&quot;,
75  * 			&quot;a file to receive the concatenated files (default is stdout)&quot;);
76  * 	static BooleanParam delete = new BooleanParam(&quot;delete&quot;,
77  * 			&quot;specifies that all of the original files are to be deleted&quot;);
78  * 	static FileParam infiles = new FileParam(&quot;filename&quot;,
79  * 			&quot;files to be concatenated&quot;, FileParam.IS_FILE
80  * 					&amp; FileParam.IS_READABLE, FileParam.REQUIRED,
81  * 			FileParam.MULTI_VALUED);
82  *
83  * 	public static void main(String[] args) {
84  *         outfile.setOptionLabel(&quot;outfile&quot;);
85  *         BasicCmdLineHandler clp = new BasicCmdLineHandler(
86  *                 &quot;Concat&quot;, &quot;concatenates the specified files&quot;,
87  *                 new Parameter[] { outfile, delete },
88  *                 new Parameter[] { infiles });
89  *         clp.parse(args);
90  *
91  *         if (outfile.isSet()) {
92  *             ....
93  *         } else {
94  *             ...
95  *         }
96  *         for (Iterator itr = infiles.getFiles().iterator(); itr.hasNext(); ) {
97  *             ...
98  *         }
99  *         if (delete.isTrue()) {
100  *             ...
101  *         }
102  *     }
103  * }
104  *
105  * </pre>
106  * <P>
107  * This class implements no options on its own. It it typically used in
108  * conjunction with one or more of the {@link AbstractHandlerDecorator} classes
109  * that provide some useful options.
110  * <P>
111  * <b>Post Processing Command Line Parameters</b>
112  * <P>
113  * It may be the case that none of the supplied Parameter types fully
114  * accomodates a program's needs. For instance, a program may require a filename
115  * option that is an html filename, ending with '.html'. In this case, the
116  * programmer has the options of creating their own Parameter subclass, or
117  * post-processing the returned FileParam parameter and generating their own
118  * error message. The {@link #exitUsageError(String) exitUsageError()} method is
119  * provided so that programs that post-process parameters can take the same exit
120  * as would be taken for "normal" parameter processing failures. For instance,
121  * in the case just described, the following code could be used to exit the
122  * program if the specified file did not end with '.html' (<code>myfile</code>
123  * being a FileParam object, and <code>cl</code> being the BasicCmdLineHandler
124  * object):
125  *
126  * <pre>
127  * cl.parse(args);
128  * if (!myfile.getFile().getPath().endsWith(&quot;.html&quot;)) {
129  * 	cl.exitUsageError(&quot;Filename specified for '&quot; + myfile.getTag()
130  * 			+ &quot;' must end with '.html'&quot;);
131  * }
132  * </pre>
133  *
134  * @author Lynne Lawrence
135  * @version jcmdline Rel. @VERSION@ $Id: BasicCmdLineHandler.java,v 1.4
136  *          2005/02/06 13:19:19 lglawrence Exp $
137  * @see Parameter
138  * @see CmdLineParser
139  */
140 public class BasicCmdLineHandler implements CmdLineHandler {
141 
142 	/**
143 	 * the arguments (after the options have been processed) associated with the
144 	 * command
145 	 */
146 	private ArrayList<Parameter<?>> args = new ArrayList<Parameter<?>>();
147 
148 	/**
149 	 * a short description of the command's purpose
150 	 */
151 	private String cmdDesc;
152 
153 	/**
154 	 * the name of the command whose command line is to be parsed
155 	 */
156 	private String cmdName;
157 
158 	/**
159 	 * indicates that the command usage should be displayed, and System.exit(1)
160 	 * called in the case of a parse error.
161 	 *
162 	 * @see #setDieOnParseError(boolean) setDieOnParseError()
163 	 */
164 	private boolean dieOnParseError = true;
165 
166 	/**
167 	 * the options associated with the command
168 	 */
169 	private HashMap<String, Parameter<?>> options = new HashMap<String, Parameter<?>>();
170 
171 	/**
172 	 * the error message from the last call to parse()
173 	 *
174 	 * @see #setParseError(String) setParseError()
175 	 * @see #getParseError()
176 	 */
177 	private String parseError;
178 
179 	/**
180 	 * the parser to be used to parse the command line
181 	 *
182 	 * @see #setParser(CmdLineParser) setParser()
183 	 * @see #getParser()
184 	 */
185 	private CmdLineParser parser;
186 
187 	/**
188 	 * constructor - uses the PosixCmdLineParser to parse the command line
189 	 *
190 	 * @param cmdName
191 	 *            the name of the command creating this BasicCmdLineHandler
192 	 * @param cmdDesc
193 	 *            a short description of the command's purpose
194 	 * @param options
195 	 *            a collection of Parameter objects, describing the command's
196 	 *            command-line options
197 	 * @param args
198 	 *            a collection of Parameter objects, describing the command's
199 	 *            command-line arguments (what is left on the command line after
200 	 *            all options and their parameters have been processed)
201 	 * @throws IllegalArgumentException
202 	 *             if any of the parameters are not correctly specified.
203 	 * @see #setCmdName(String) setCmdName()
204 	 * @see #setCmdDesc(String) setCmdDesc()
205 	 * @see #setOptions(Parameter[]) setOptions()
206 	 * @see PosixCmdLineParser
207 	 */
BasicCmdLineHandler(String cmdName, String cmdDesc, Collection<Parameter<?>> options, Collection<Parameter<?>> args)208 	public BasicCmdLineHandler(String cmdName, String cmdDesc,
209 			Collection<Parameter<?>> options, Collection<Parameter<?>> args) {
210 		this(cmdName, cmdDesc, (Parameter[]) options.toArray(new Parameter[0]),
211 				(Parameter[]) args.toArray(new Parameter[0]));
212 	}
213 
214 	/**
215 	 * constructor - uses the PosixCmdLineParser to parse the command line
216 	 *
217 	 * @param cmdName
218 	 *            the name of the command
219 	 * @param cmdDesc
220 	 *            a short description of the command
221 	 * @param options
222 	 *            a collection of Parameter objects, describing the command's
223 	 *            command-line options
224 	 * @param args
225 	 *            a collection of Parameter objects, describing the command's
226 	 *            command-line arguments (what is left on the command line after
227 	 *            all options and their parameters have been processed)
228 	 * @throws IllegalArgumentException
229 	 *             if any of the parameters are not correctly specified.
230 	 * @see #setCmdName(String) setCmdName()
231 	 * @see #setCmdDesc(String) setCmdDesc()
232 	 * @see #setOptions(Parameter[]) setOptions()
233 	 * @see #setArgs(Parameter[]) setArgs()
234 	 * @see PosixCmdLineParser
235 	 */
BasicCmdLineHandler(String cmdName, String cmdDesc, Parameter<?>[] options, Parameter<?>[] args)236 	public BasicCmdLineHandler(String cmdName, String cmdDesc,
237 			Parameter<?>[] options, Parameter<?>[] args) {
238 		this(cmdName, cmdDesc, options, args, new PosixCmdLineParser());
239 	}
240 
241 	/**
242 	 * constructor
243 	 *
244 	 * @param cmdName
245 	 *            the name of the command
246 	 * @param cmdDesc
247 	 *            a short description of the command
248 	 * @param options
249 	 *            a collection of Parameter objects, describing the command's
250 	 *            command-line options
251 	 * @param args
252 	 *            a collection of Parameter objects, describing the command's
253 	 *            command-line arguments (what is left on the command line after
254 	 *            all options and their parameters have been processed)
255 	 * @param parser
256 	 *            a CmdLineParser to be used to parse the command line
257 	 * @throws IllegalArgumentException
258 	 *             if any of the parameters are not correctly specified.
259 	 * @see #setCmdName(String) setCmdName()
260 	 * @see #setCmdDesc(String) setCmdDesc()
261 	 * @see #setOptions(Parameter[]) setOptions()
262 	 * @see #setArgs(Parameter[]) setArgs()
263 	 * @see #setParser(CmdLineParser) setParser()
264 	 */
BasicCmdLineHandler(String cmdName, String cmdDesc, Parameter<?>[] options, Parameter<?>[] args, CmdLineParser parser)265 	public BasicCmdLineHandler(String cmdName, String cmdDesc,
266 			Parameter<?>[] options, Parameter<?>[] args, CmdLineParser parser) {
267 
268 		setCmdName(cmdName);
269 		setCmdDesc(cmdDesc);
270 		setOptions(options);
271 		setArgs(args);
272 		setParser(parser);
273 	}
274 
275 	/**
276 	 * Adds a command line arguement. Command line arguments must be added in
277 	 * the order that they will be specified on the command line.
278 	 *
279 	 * @param arg
280 	 *            the new command line argument
281 	 * @throws IllegalArgumentException
282 	 *             if:
283 	 *             <ul>
284 	 *             <li><code>arg</code> is null <li>the previously added
285 	 *             argument was multi-valued (only the last argument can be
286 	 *             multi-valued) <li><code>arg</code> is a required argument but
287 	 *             the previous argument was optional
288 	 *             </ul>
289 	 */
addArg(Parameter<?> arg)290 	public void addArg(Parameter<?> arg) {
291 		if (arg == null) {
292 			throw new IllegalArgumentException(Strings
293 					.get("BasicCmdLineHandler.nullArgNotAllowed"));
294 		}
295 		if (args.size() > 0) {
296 			Parameter<?> lastArg = args.get(args.size() - 1);
297 			if (lastArg.isMultiValued()) {
298 				throw new IllegalArgumentException(Strings.get(
299 						"BasicCmdLineHandler.multiValueArgNotLast",
300 						new Object[] { lastArg.getTag() }));
301 			}
302 			if (!arg.isOptional() && lastArg.isOptional()) {
303 				throw new IllegalArgumentException(Strings.get(
304 						"BasicCmdLineHandler.requiredArgAfterOptArg",
305 						new Object[] { arg.getTag(), lastArg.getTag() }));
306 			}
307 		}
308 		args.add(arg);
309 	}
310 
311 	/**
312 	 * Adds a command line option.
313 	 *
314 	 * @param opt
315 	 *            the new command line option
316 	 * @throws NullPointerException
317 	 *             if <code>opt</code> is null.
318 	 * @throws IllegalArgumentException
319 	 *             if an option with the same tag has already been added.
320 	 */
addOption(Parameter<?> opt)321 	public void addOption(Parameter<?> opt) {
322 		if (opt == null) {
323 			throw new NullPointerException();
324 		}
325 		if (options.containsKey(opt.getTag())) {
326 			throw new IllegalArgumentException(Strings.get(
327 					"BasicCmdLineHandler.duplicateOption", new Object[] { opt
328 							.getTag() }));
329 		}
330 		options.put(opt.getTag().toLowerCase(), opt);
331 	}
332 
333 	/**
334 	 * Prints the usage, followed by the specified error message, to stderr and
335 	 * exits the program with exit status = 1. The error message will be
336 	 * prefaced with 'ERROR: '. This method never returns - it exits the program
337 	 * with exit status of 1.
338 	 *
339 	 * @param errMsg
340 	 *            the error message
341 	 */
exitUsageError(String errMsg)342 	public void exitUsageError(String errMsg) {
343 		System.err.println(getUsage(false));
344 		System.err.println("\n"
345 				+ parser.getUsageFormatter().formatErrorMsg(errMsg));
346 		quitProgram(1);
347 	}
348 
349 	/**
350 	 * gets the argument specified by <code>tag</code>
351 	 *
352 	 * @param tag
353 	 *            identifies the argument to be returned
354 	 * @return The argument associated with <code>tag</code>. If no matching
355 	 *         argument is found, null is returned.
356 	 */
getArg(String tag)357 	public Parameter<?> getArg(String tag) {
358 		for (Parameter<?> arg : args) {
359 			if (arg.getTag().equals(tag)) {
360 				return arg;
361 			}
362 		}
363 		return null;
364 	}
365 
366 	/**
367 	 * gets the value of the arguments (what is left on the command line after
368 	 * all options, and their parameters, have been processed) associated with
369 	 * the command
370 	 *
371 	 * @return the command's options
372 	 */
getArgs()373 	public List<Parameter<?>> getArgs() {
374 		return args;
375 	}
376 
377 	/**
378 	 * gets a description of the command's purpose
379 	 *
380 	 * @return the command's description
381 	 */
getCmdDesc()382 	public String getCmdDesc() {
383 		return cmdDesc;
384 	}
385 
386 	/**
387 	 * gets the value of the command name associated with this
388 	 * BasicCmdLineHandler
389 	 *
390 	 * @return the command name
391 	 */
getCmdName()392 	public String getCmdName() {
393 		return cmdName;
394 	}
395 
396 	/**
397 	 * Gets a flag indicating that the program should exit in the case of a
398 	 * parse error (after displaying the usage and an error message).
399 	 *
400 	 * @return <code>true</code> (the default) if the <code>
401      *                      parse</code>
402 	 *         method should call System.exit() in case of a parse error,
403 	 *         <code>false</code> if <code>parse()</code> should return to the
404 	 *         user for error processing.
405 	 * @see #parse(String[]) parse()
406 	 */
getDieOnParseError()407 	public boolean getDieOnParseError() {
408 		return dieOnParseError;
409 	}
410 
411 	/**
412 	 * gets the option specified by <code>tag</code>
413 	 *
414 	 * @param tag
415 	 *            identifies the option to be returned
416 	 * @return the option associated with <code>tag</code>
417 	 */
getOption(String tag)418 	public Parameter<?> getOption(String tag) {
419 		return options.get(tag);
420 	}
421 
422 	/**
423 	 * gets the value of the options associated with the command
424 	 *
425 	 * @return the command's options
426 	 */
getOptions()427 	public Collection<Parameter<?>> getOptions() {
428 		return options.values();
429 	}
430 
431 	/**
432 	 * Gets the error message from the last call to parse().
433 	 *
434 	 * @return the error message from the last call to parse()
435 	 * @see #setParseError(String) setParseError()
436 	 */
getParseError()437 	public String getParseError() {
438 		return parseError;
439 	}
440 
441 	/**
442 	 * Gets the parser to be used to parse the command line.
443 	 *
444 	 * @return the parser to be used to parse the command line
445 	 * @see #setParser(CmdLineParser) setParser()
446 	 */
getParser()447 	public CmdLineParser getParser() {
448 		return parser;
449 	}
450 
451 	/**
452 	 * Gets the usage statement associated with the command.
453 	 *
454 	 * @param hidden
455 	 *            indicates whether hidden options are to be included in the
456 	 *            usage.
457 	 * @return the usage statement associated with the command
458 	 */
getUsage(boolean hidden)459 	public String getUsage(boolean hidden) {
460 		return parser.getUsageFormatter().formatUsage(cmdName, cmdDesc,
461 				options, args, hidden);
462 	}
463 
464 	/**
465 	 * Parse the specified command line arguments. This method will fail if:
466 	 * <ul>
467 	 * <li>the CmdLineParser is unable to parse the command line parameters into
468 	 * the required options and arguments. <li>a required Parameter has not been
469 	 * set by the parser.
470 	 * </ul>
471 	 *
472 	 * @param clargs
473 	 *            command line arguments passed to the main() method of
474 	 *            BasicCmdLineHandler's creating class.
475 	 * @return If <code>dieOnParseError</code> is set to <code>false</code>,
476 	 *         this method will return true if there are no parse errors. If
477 	 *         there are parse errors, <code>false</code>is returned and an
478 	 *         appropriate error message may be obtained by calling
479 	 *         {@link #getParseError()}.
480 	 *         <P>
481 	 *         If <code>dieOnParseError</code> is set to <code>true</code> and
482 	 *         the method fails, the program will exit with exit code 1 after
483 	 *         printing the usage to stderr.
484 	 */
parse(String[] clargs)485 	public boolean parse(String[] clargs) {
486 		if (clargs == null) {
487 			clargs = new String[] {};
488 		}
489 		try {
490 			parser.parse(clargs, options, args);
491 			if (!canSkipRequiredCheck()) {
492 				checkForRequired();
493 			}
494 		} catch (CmdLineException e) {
495 			if (dieOnParseError) {
496 				exitUsageError(e.getMessage());
497 			} else {
498 				parseError = e.getMessage();
499 				return false;
500 			}
501 		}
502 		return true;
503 	}
504 
505 	/**
506 	 * sets the value of the arguments (what is left on the command line after
507 	 * all options, and their parameters, have been processed) associated with
508 	 * the command
509 	 *
510 	 * @param args
511 	 *            A Collection of {@link Parameter} objects. This may be null if
512 	 *            the command accepts no command line arguments.
513 	 */
setArgs(Parameter<?>[] args)514 	public void setArgs(Parameter<?>[] args) {
515 		this.args.clear();
516 		if (args != null) {
517 			for (int i = 0; i < args.length; i++) {
518 				addArg(args[i]);
519 			}
520 		}
521 	}
522 
523 	/**
524 	 * sets a description of the command's purpose
525 	 *
526 	 * @param cmdDesc
527 	 *            a short description of the command's purpose
528 	 * @throws IllegalArgumentException
529 	 *             if <code>cmdDesc
530      *                      </code> is null or of 0 length.
531 	 */
setCmdDesc(String cmdDesc)532 	public void setCmdDesc(String cmdDesc) {
533 		if (cmdDesc == null || cmdDesc.length() <= 0) {
534 			throw new IllegalArgumentException(Strings.get(
535 					"BasicCmdLineHandler.cmdDescTooShort",
536 					new Object[] { cmdDesc }));
537 		}
538 		this.cmdDesc = cmdDesc;
539 	}
540 
541 	/**
542 	 * sets the value of the command name associated with this
543 	 * BasicCmdLineHandler
544 	 *
545 	 * @param cmdName
546 	 *            the name of the command associated with this
547 	 *            BasicCmdLineHandler
548 	 * @throws IllegalArgumentException
549 	 *             if cmdName is null, or of 0 length
550 	 */
setCmdName(String cmdName)551 	public void setCmdName(String cmdName) {
552 		if (cmdName == null || cmdName.length() <= 0) {
553 			throw new IllegalArgumentException(Strings.get(
554 					"BasicCmdLineHandler.cmdNameTooShort",
555 					new Object[] { cmdName }));
556 		}
557 		this.cmdName = cmdName;
558 	}
559 
560 	/**
561 	 * Sets a flag indicating that the program should exit in the case of a
562 	 * parse error (after displaying the usage and an error message) - defaults
563 	 * to <code>true</code>.
564 	 *
565 	 * @param val
566 	 *            <code>true</code> (the default) if the <code>
567      *                      parse</code>
568 	 *            method should call System.exit() in case of a parse error,
569 	 *            <code>false</code> if <code>parse()</code> should return to
570 	 *            the user for error processing.
571 	 * @see #parse(String[]) parse()
572 	 */
setDieOnParseError(boolean val)573 	public void setDieOnParseError(boolean val) {
574 		dieOnParseError = val;
575 	}
576 
577 	/**
578 	 * Sets the value of the options associated with the command
579 	 *
580 	 * @param options
581 	 *            A Collection of {@link Parameter} objects. This may be null if
582 	 *            the command accepts no command line options.
583 	 */
setOptions(Parameter<?>[] options)584 	public void setOptions(Parameter<?>[] options) {
585 		this.options.clear();
586 		if (options != null) {
587 			for (int i = 0; i < options.length; i++) {
588 				addOption(options[i]);
589 			}
590 		}
591 	}
592 
593 	/**
594 	 * Sets the error message from the last call to parse().
595 	 *
596 	 * @param parseError
597 	 *            the error message from the last call to parse()
598 	 * @see #getParseError()
599 	 */
setParseError(String parseError)600 	public void setParseError(String parseError) {
601 		this.parseError = parseError;
602 	}
603 
604 	/**
605 	 * Sets the parser to be used to parse the command line.
606 	 *
607 	 * @param parser
608 	 *            the parser to be used to parse the command line
609 	 * @see #getParser()
610 	 */
setParser(CmdLineParser parser)611 	public void setParser(CmdLineParser parser) {
612 		this.parser = parser;
613 	}
614 
615 	/**
616 	 * Indicates whether the check for required Parameters can be skipped due to
617 	 * a Parameter with flag <code>ignoreRequired = true</code> having been set.
618 	 *
619 	 * @return true if the check for required parameters may be skipped
620 	 */
canSkipRequiredCheck()621 	private boolean canSkipRequiredCheck() {
622 		for (Parameter<?> p : options.values()) {
623 			if (p.getIgnoreRequired() && p.isSet()) {
624 				return true;
625 			}
626 		}
627 		for (Parameter<?> p : args) {
628 			if (p.getIgnoreRequired() && p.isSet()) {
629 				return true;
630 			}
631 		}
632 		return false;
633 	}
634 
635 	/**
636 	 * Verifies that all required options and arguments have been specified.
637 	 *
638 	 * @throws CmdLineException
639 	 *             if a required option or argument is not set.
640 	 */
checkForRequired()641 	private void checkForRequired() throws CmdLineException {
642 		for (Parameter<?> p : options.values()) {
643 			if (!p.isOptional() && !p.isSet()) {
644 				throw new CmdLineException(Strings.get(
645 						"BasicCmdLineHandler.missingRequiredOpt",
646 						new Object[] { p.getTag() }));
647 			}
648 		}
649 		for (Parameter<?> p : args) {
650 			if (!p.isOptional() && !p.isSet()) {
651 				throw new CmdLineException(Strings.get(
652 						"BasicCmdLineHandler.missingRequiredArg",
653 						new Object[] { p.getTag() }));
654 			}
655 		}
656 	}
657 
658 	/**
659 	 * Exits the program with the specified exit status.
660 	 *
661 	 * @param exitStatus
662 	 *            the program's exit status
663 	 * @return this method never returns - the program is terminated
664 	 */
quitProgram(int exitStatus)665 	private void quitProgram(int exitStatus) {
666 		System.exit(exitStatus);
667 	}
668 }
669