You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rp...@apache.org on 2017/10/24 04:14:04 UTC
[4/5] logging-log4j2 git commit: LOG4J2-2088 Upgrade picocli to 2.0
from 0.9.8
http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/82879764/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java
----------------------------------------------------------------------
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java
index 7f51af3..ed6cec1 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java
@@ -16,7 +16,6 @@
*/
package org.apache.logging.log4j.core.tools.picocli;
-import java.awt.Point;
import java.io.File;
import java.io.PrintStream;
import java.lang.annotation.ElementType;
@@ -26,6 +25,9 @@ import java.lang.annotation.Target;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
@@ -49,17 +51,18 @@ import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Queue;
import java.util.Set;
-import java.util.SortedMap;
import java.util.SortedSet;
import java.util.Stack;
-import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
+import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi.IStyle;
@@ -78,7 +81,8 @@ import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Colum
* </p><h2>Example</h2>
* <pre>import static picocli.CommandLine.*;
*
- * @Command(header = "Encrypt FILE(s), or standard input, to standard output or to the output file.")
+ * @Command(header = "Encrypt FILE(s), or standard input, to standard output or to the output file.",
+ * version = "v1.2.3")
* public class Encrypt {
*
* @Parameters(type = File.class, description = "Any number of input files")
@@ -90,24 +94,26 @@ import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Colum
* @Option(names = { "-v", "--verbose"}, description = "Verbosely list files processed")
* private boolean verbose;
*
- * @Option(names = { "-h", "--help", "-?", "-help"}, help = true, description = "Display this help and exit")
+ * @Option(names = { "-h", "--help", "-?", "-help"}, usageHelp = true, description = "Display this help and exit")
* private boolean help;
+ *
+ * @Option(names = { "-V", "--version"}, versionHelp = true, description = "Display version info and exit")
+ * private boolean versionHelp;
* }
* </pre>
* <p>
* Use {@code CommandLine} to initialize a domain object as follows:
* </p><pre>
* public static void main(String... args) {
+ * Encrypt encrypt = new Encrypt();
* try {
- * Encrypt encrypt = CommandLine.populateCommand(new Encrypt(), args);
- * if (encrypt.help) {
- * CommandLine.usage(encrypt, System.out);
- * } else {
+ * List<CommandLine> parsedCommands = new CommandLine(encrypt).parse(args);
+ * if (!CommandLine.printHelpIfRequested(parsedCommands, System.err, Help.Ansi.AUTO)) {
* runProgram(encrypt);
* }
* } catch (ParameterException ex) { // command line arguments could not be parsed
* System.err.println(ex.getMessage());
- * CommandLine.usage(new Encrypt(), System.err);
+ * ex.getCommandLine().usage(System.err);
* }
* }
* </pre><p>
@@ -124,18 +130,14 @@ import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Colum
* -v -ooutfile in1 in2
* -vooutfile in1 in2
* </pre>
- *
- * <p>
- * Copied and modified from <a href="http://github.com/remkop/picocli/">picocli</a>.
- * </p>
- *
- * @since 2.9
*/
public class CommandLine {
/** This is picocli version {@value}. */
- public static final String VERSION = "0.9.8";
+ public static final String VERSION = "2.0.0";
+ private final Tracer tracer = new Tracer();
private final Interpreter interpreter;
+ private String commandName = Help.DEFAULT_COMMAND_NAME;
private boolean overwrittenOptionsAllowed = false;
private boolean unmatchedArgumentsAllowed = false;
private List<String> unmatchedArguments = new ArrayList<String>();
@@ -149,7 +151,7 @@ public class CommandLine {
* When the {@link #parse(String...)} method is called, fields of the specified object that are annotated
* with {@code @Option} or {@code @Parameters} will be initialized based on command line arguments.
* @param command the object to initialize from the command line arguments
- * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
*/
public CommandLine(Object command) {
interpreter = new Interpreter(command);
@@ -186,6 +188,7 @@ public class CommandLine {
* converters are registered only with the subcommand hierarchy as it existed when the custom type was registered.
* To ensure a custom type converter is available to all subcommands, register the type converter last, after
* adding subcommands.</p>
+ * <p>See also the {@link Command#subcommands()} annotation to register subcommands declaratively.</p>
*
* @param name the string to recognize on the command line as a subcommand
* @param command the object to initialize with command line arguments following the subcommand name.
@@ -193,6 +196,7 @@ public class CommandLine {
* @return this CommandLine object, to allow method chaining
* @see #registerConverter(Class, ITypeConverter)
* @since 0.9.7
+ * @see Command#subcommands()
*/
public CommandLine addSubcommand(String name, Object command) {
CommandLine commandLine = toCommandLine(command);
@@ -218,21 +222,23 @@ public class CommandLine {
return parent;
}
- /**
- * Returns the annotated object that this {@code CommandLine} instance was constructed with.
+ /** Returns the annotated object that this {@code CommandLine} instance was constructed with.
+ * @param <T> the type of the variable that the return value is being assigned to
* @return the annotated object that this {@code CommandLine} instance was constructed with
* @since 0.9.7
*/
- public Object getCommand() {
- return interpreter.command;
+ public <T> T getCommand() {
+ return (T) interpreter.command;
}
/** Returns {@code true} if an option annotated with {@link Option#usageHelp()} was specified on the command line.
- * @return whether the parser encountered an option annotated with {@link Option#usageHelp()} */
+ * @return whether the parser encountered an option annotated with {@link Option#usageHelp()}.
+ * @since 0.9.8 */
public boolean isUsageHelpRequested() { return usageHelpRequested; }
/** Returns {@code true} if an option annotated with {@link Option#versionHelp()} was specified on the command line.
- * @return whether the parser encountered an option annotated with {@link Option#versionHelp()} */
+ * @return whether the parser encountered an option annotated with {@link Option#versionHelp()}.
+ * @since 0.9.8 */
public boolean isVersionHelpRequested() { return versionHelpRequested; }
/** Returns whether options for single-value fields can be specified multiple times on the command line.
@@ -278,9 +284,10 @@ public class CommandLine {
* subcommands and nested sub-subcommands <em>at the moment this method is called</em>. Subcommands added
* later will have the default setting. To ensure a setting is applied to all
* subcommands, call the setter last, after adding subcommands.</p>
- * @param newValue the new setting
+ * @param newValue the new setting. When {@code true}, the last unmatched arguments are available via the {@link #getUnmatchedArguments()} method.
* @return this {@code CommandLine} object, to allow method chaining
* @since 0.9.7
+ * @see #getUnmatchedArguments()
*/
public CommandLine setUnmatchedArgumentsAllowed(boolean newValue) {
this.unmatchedArgumentsAllowed = newValue;
@@ -315,7 +322,7 @@ public class CommandLine {
* @param args the command line arguments to parse
* @param <T> the type of the annotated object
* @return the specified annotated object
- * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
* @throws ParameterException if the specified command line arguments are invalid
* @since 0.9.7
*/
@@ -325,21 +332,332 @@ public class CommandLine {
return command;
}
- /**
+ /** Parses the specified command line arguments and returns a list of {@code CommandLine} objects representing the
+ * top-level command and any subcommands (if any) that were recognized and initialized during the parsing process.
* <p>
- * Initializes the annotated object that this {@code CommandLine} was constructed with as well as
- * possibly any registered commands, based on the specified command line arguments,
- * and returns a list of all commands and subcommands that were initialized by this method.
+ * If parsing succeeds, the first element in the returned list is always {@code this CommandLine} object. The
+ * returned list may contain more elements if subcommands were {@linkplain #addSubcommand(String, Object) registered}
+ * and these subcommands were initialized by matching command line arguments. If parsing fails, a
+ * {@link ParameterException} is thrown.
* </p>
*
* @param args the command line arguments to parse
- * @return a list with all commands and subcommands initialized by this method
- * @throws ParameterException if the specified command line arguments are invalid
+ * @return a list with the top-level command and any subcommands initialized by this method
+ * @throws ParameterException if the specified command line arguments are invalid; use
+ * {@link ParameterException#getCommandLine()} to get the command or subcommand whose user input was invalid
*/
public List<CommandLine> parse(String... args) {
return interpreter.parse(args);
}
-
+ /**
+ * Represents a function that can process a List of {@code CommandLine} objects resulting from successfully
+ * {@linkplain #parse(String...) parsing} the command line arguments. This is a
+ * <a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html">functional interface</a>
+ * whose functional method is {@link #handleParseResult(List, PrintStream, CommandLine.Help.Ansi)}.
+ * <p>
+ * Implementations of this functions can be passed to the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) CommandLine::parseWithHandler}
+ * methods to take some next step after the command line was successfully parsed.
+ * </p>
+ * @see RunFirst
+ * @see RunLast
+ * @see RunAll
+ * @since 2.0 */
+ public static interface IParseResultHandler {
+ /** Processes a List of {@code CommandLine} objects resulting from successfully
+ * {@linkplain #parse(String...) parsing} the command line arguments and optionally returns a list of results.
+ * @param parsedCommands the {@code CommandLine} objects that resulted from successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return a list of results, or an empty list if there are no results
+ * @throws ExecutionException if a problem occurred while processing the parse results; use
+ * {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ */
+ List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) throws ExecutionException;
+ }
+ /**
+ * Represents a function that can handle a {@code ParameterException} that occurred while
+ * {@linkplain #parse(String...) parsing} the command line arguments. This is a
+ * <a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html">functional interface</a>
+ * whose functional method is {@link #handleException(CommandLine.ParameterException, PrintStream, CommandLine.Help.Ansi, String...)}.
+ * <p>
+ * Implementations of this functions can be passed to the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) CommandLine::parseWithHandler}
+ * methods to handle situations when the command line could not be parsed.
+ * </p>
+ * @see DefaultExceptionHandler
+ * @since 2.0 */
+ public static interface IExceptionHandler {
+ /** Handles a {@code ParameterException} that occurred while {@linkplain #parse(String...) parsing} the command
+ * line arguments and optionally returns a list of results.
+ * @param ex the ParameterException describing the problem that occurred while parsing the command line arguments,
+ * and the CommandLine representing the command or subcommand whose input was invalid
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @param args the command line arguments that could not be parsed
+ * @return a list of results, or an empty list if there are no results
+ */
+ List<Object> handleException(ParameterException ex, PrintStream out, Help.Ansi ansi, String... args);
+ }
+ /**
+ * Default exception handler that prints the exception message to the specified {@code PrintStream}, followed by the
+ * usage message for the command or subcommand whose input was invalid.
+ * <p>Implementation roughly looks like this:</p>
+ * <pre>
+ * System.err.println(paramException.getMessage());
+ * paramException.getCommandLine().usage(System.err);
+ * </pre>
+ * @since 2.0 */
+ public static class DefaultExceptionHandler implements IExceptionHandler {
+ @Override
+ public List<Object> handleException(ParameterException ex, PrintStream out, Help.Ansi ansi, String... args) {
+ out.println(ex.getMessage());
+ ex.getCommandLine().usage(out, ansi);
+ return Collections.emptyList();
+ }
+ }
+ /**
+ * Helper method that may be useful when processing the list of {@code CommandLine} objects that result from successfully
+ * {@linkplain #parse(String...) parsing} command line arguments. This method prints out
+ * {@linkplain #usage(PrintStream, Help.Ansi) usage help} if {@linkplain #isUsageHelpRequested() requested}
+ * or {@linkplain #printVersionHelp(PrintStream, Help.Ansi) version help} if {@linkplain #isVersionHelpRequested() requested}
+ * and returns {@code true}. Otherwise, if none of the specified {@code CommandLine} objects have help requested,
+ * this method returns {@code false}.
+ * <p>
+ * Note that this method <em>only</em> looks at the {@link Option#usageHelp() usageHelp} and
+ * {@link Option#versionHelp() versionHelp} attributes. The {@link Option#help() help} attribute is ignored.
+ * </p>
+ * @param parsedCommands the list of {@code CommandLine} objects to check if help was requested
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return {@code true} if help was printed, {@code false} otherwise
+ * @since 2.0 */
+ public static boolean printHelpIfRequested(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
+ for (CommandLine parsed : parsedCommands) {
+ if (parsed.isUsageHelpRequested()) {
+ parsed.usage(out, ansi);
+ return true;
+ } else if (parsed.isVersionHelpRequested()) {
+ parsed.printVersionHelp(out, ansi);
+ return true;
+ }
+ }
+ return false;
+ }
+ private static Object execute(CommandLine parsed) {
+ Object command = parsed.getCommand();
+ if (command instanceof Runnable) {
+ try {
+ ((Runnable) command).run();
+ return null;
+ } catch (Exception ex) {
+ throw new ExecutionException(parsed, "Error while running command (" + command + ")", ex);
+ }
+ } else if (command instanceof Callable) {
+ try {
+ return ((Callable<Object>) command).call();
+ } catch (Exception ex) {
+ throw new ExecutionException(parsed, "Error while calling command (" + command + ")", ex);
+ }
+ }
+ throw new ExecutionException(parsed, "Parsed command (" + command + ") is not Runnable or Callable");
+ }
+ /**
+ * Command line parse result handler that prints help if requested, and otherwise executes the top-level
+ * {@code Runnable} or {@code Callable} command.
+ * For use in the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) parseWithHandler} methods.
+ * <p>
+ * From picocli v2.0, {@code RunFirst} is used to implement the {@link #run(Runnable, PrintStream, Help.Ansi, String...) run}
+ * and {@link #call(Callable, PrintStream, Help.Ansi, String...) call} convenience methods.
+ * </p>
+ * @since 2.0 */
+ public static class RunFirst implements IParseResultHandler {
+ /** Prints help if requested, and otherwise executes the top-level {@code Runnable} or {@code Callable} command.
+ * If the top-level command does not implement either {@code Runnable} or {@code Callable}, a {@code ExecutionException}
+ * is thrown detailing the problem and capturing the offending {@code CommandLine} object.
+ *
+ * @param parsedCommands the {@code CommandLine} objects that resulted from successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return an empty list if help was requested, or a list containing a single element: the result of calling the
+ * {@code Callable}, or a {@code null} element if the top-level command was a {@code Runnable}
+ * @throws ExecutionException if a problem occurred while processing the parse results; use
+ * {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ */
+ public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
+ if (printHelpIfRequested(parsedCommands, out, ansi)) { return Collections.emptyList(); }
+ return Arrays.asList(execute(parsedCommands.get(0)));
+ }
+ }
+ /**
+ * Command line parse result handler that prints help if requested, and otherwise executes the most specific
+ * {@code Runnable} or {@code Callable} subcommand.
+ * For use in the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) parseWithHandler} methods.
+ * <p>
+ * Something like this:</p>
+ * <pre>
+ * // RunLast implementation: print help if requested, otherwise execute the most specific subcommand
+ * if (CommandLine.printHelpIfRequested(parsedCommands, System.err, Help.Ansi.AUTO)) {
+ * return emptyList();
+ * }
+ * CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
+ * Object command = last.getCommand();
+ * if (command instanceof Runnable) {
+ * try {
+ * ((Runnable) command).run();
+ * } catch (Exception ex) {
+ * throw new ExecutionException(last, "Error in runnable " + command, ex);
+ * }
+ * } else if (command instanceof Callable) {
+ * Object result;
+ * try {
+ * result = ((Callable) command).call();
+ * } catch (Exception ex) {
+ * throw new ExecutionException(last, "Error in callable " + command, ex);
+ * }
+ * // ...do something with result
+ * } else {
+ * throw new ExecutionException(last, "Parsed command (" + command + ") is not Runnable or Callable");
+ * }
+ * </pre>
+ * @since 2.0 */
+ public static class RunLast implements IParseResultHandler {
+ /** Prints help if requested, and otherwise executes the most specific {@code Runnable} or {@code Callable} subcommand.
+ * If the last (sub)command does not implement either {@code Runnable} or {@code Callable}, a {@code ExecutionException}
+ * is thrown detailing the problem and capturing the offending {@code CommandLine} object.
+ *
+ * @param parsedCommands the {@code CommandLine} objects that resulted from successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return an empty list if help was requested, or a list containing a single element: the result of calling the
+ * {@code Callable}, or a {@code null} element if the last (sub)command was a {@code Runnable}
+ * @throws ExecutionException if a problem occurred while processing the parse results; use
+ * {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ */
+ public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
+ if (printHelpIfRequested(parsedCommands, out, ansi)) { return Collections.emptyList(); }
+ CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
+ return Arrays.asList(execute(last));
+ }
+ }
+ /**
+ * Command line parse result handler that prints help if requested, and otherwise executes the top-level command and
+ * all subcommands as {@code Runnable} or {@code Callable}.
+ * For use in the {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...) parseWithHandler} methods.
+ * @since 2.0 */
+ public static class RunAll implements IParseResultHandler {
+ /** Prints help if requested, and otherwise executes the top-level command and all subcommands as {@code Runnable}
+ * or {@code Callable}. If any of the {@code CommandLine} commands does not implement either
+ * {@code Runnable} or {@code Callable}, a {@code ExecutionException}
+ * is thrown detailing the problem and capturing the offending {@code CommandLine} object.
+ *
+ * @param parsedCommands the {@code CommandLine} objects that resulted from successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @return an empty list if help was requested, or a list containing the result of executing all commands:
+ * the return values from calling the {@code Callable} commands, {@code null} elements for commands that implement {@code Runnable}
+ * @throws ExecutionException if a problem occurred while processing the parse results; use
+ * {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ */
+ public List<Object> handleParseResult(List<CommandLine> parsedCommands, PrintStream out, Help.Ansi ansi) {
+ if (printHelpIfRequested(parsedCommands, out, ansi)) {
+ return null;
+ }
+ List<Object> result = new ArrayList<Object>();
+ for (CommandLine parsed : parsedCommands) {
+ result.add(execute(parsed));
+ }
+ return result;
+ }
+ }
+ /**
+ * Returns the result of calling {@link #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)}
+ * with {@code Help.Ansi.AUTO} and a new {@link DefaultExceptionHandler} in addition to the specified parse result handler,
+ * {@code PrintStream}, and the specified command line arguments.
+ * <p>
+ * This is a convenience method intended to offer the same ease of use as the {@link #run(Runnable, PrintStream, Help.Ansi, String...) run}
+ * and {@link #call(Callable, PrintStream, Help.Ansi, String...) call} methods, but with more flexibility and better
+ * support for nested subcommands.
+ * </p>
+ * <p>Calling this method roughly expands to:</p>
+ * <pre>
+ * try {
+ * List<CommandLine> parsedCommands = parse(args);
+ * return parseResultsHandler.handleParseResult(parsedCommands, out, Help.Ansi.AUTO);
+ * } catch (ParameterException ex) {
+ * return new DefaultExceptionHandler().handleException(ex, out, ansi, args);
+ * }
+ * </pre>
+ * <p>
+ * Picocli provides some default handlers that allow you to accomplish some common tasks with very little code.
+ * The following handlers are available:</p>
+ * <ul>
+ * <li>{@link RunLast} handler prints help if requested, and otherwise gets the last specified command or subcommand
+ * and tries to execute it as a {@code Runnable} or {@code Callable}.</li>
+ * <li>{@link RunFirst} handler prints help if requested, and otherwise executes the top-level command as a {@code Runnable} or {@code Callable}.</li>
+ * <li>{@link RunAll} handler prints help if requested, and otherwise executes all recognized commands and subcommands as {@code Runnable} or {@code Callable} tasks.</li>
+ * <li>{@link DefaultExceptionHandler} prints the error message followed by usage help</li>
+ * </ul>
+ * @param handler the function that will process the result of successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param args the command line arguments
+ * @return a list of results, or an empty list if there are no results
+ * @throws ExecutionException if the command line arguments were parsed successfully but a problem occurred while processing the
+ * parse results; use {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ * @see RunLast
+ * @see RunAll
+ * @since 2.0 */
+ public List<Object> parseWithHandler(IParseResultHandler handler, PrintStream out, String... args) {
+ return parseWithHandlers(handler, out, Help.Ansi.AUTO, new DefaultExceptionHandler(), args);
+ }
+ /**
+ * Tries to {@linkplain #parse(String...) parse} the specified command line arguments, and if successful, delegates
+ * the processing of the resulting list of {@code CommandLine} objects to the specified {@linkplain IParseResultHandler handler}.
+ * If the command line arguments were invalid, the {@code ParameterException} thrown from the {@code parse} method
+ * is caught and passed to the specified {@link IExceptionHandler}.
+ * <p>
+ * This is a convenience method intended to offer the same ease of use as the {@link #run(Runnable, PrintStream, Help.Ansi, String...) run}
+ * and {@link #call(Callable, PrintStream, Help.Ansi, String...) call} methods, but with more flexibility and better
+ * support for nested subcommands.
+ * </p>
+ * <p>Calling this method roughly expands to:</p>
+ * <pre>
+ * try {
+ * List<CommandLine> parsedCommands = parse(args);
+ * return parseResultsHandler.handleParseResult(parsedCommands, out, ansi);
+ * } catch (ParameterException ex) {
+ * return new exceptionHandler.handleException(ex, out, ansi, args);
+ * }
+ * </pre>
+ * <p>
+ * Picocli provides some default handlers that allow you to accomplish some common tasks with very little code.
+ * The following handlers are available:</p>
+ * <ul>
+ * <li>{@link RunLast} handler prints help if requested, and otherwise gets the last specified command or subcommand
+ * and tries to execute it as a {@code Runnable} or {@code Callable}.</li>
+ * <li>{@link RunFirst} handler prints help if requested, and otherwise executes the top-level command as a {@code Runnable} or {@code Callable}.</li>
+ * <li>{@link RunAll} handler prints help if requested, and otherwise executes all recognized commands and subcommands as {@code Runnable} or {@code Callable} tasks.</li>
+ * <li>{@link DefaultExceptionHandler} prints the error message followed by usage help</li>
+ * </ul>
+ *
+ * @param handler the function that will process the result of successfully parsing the command line arguments
+ * @param out the {@code PrintStream} to print help to if requested
+ * @param ansi for printing help messages using ANSI styles and colors
+ * @param exceptionHandler the function that can handle the {@code ParameterException} thrown when the command line arguments are invalid
+ * @param args the command line arguments
+ * @return a list of results produced by the {@code IParseResultHandler} or the {@code IExceptionHandler}, or an empty list if there are no results
+ * @throws ExecutionException if the command line arguments were parsed successfully but a problem occurred while processing the parse
+ * result {@code CommandLine} objects; use {@link ExecutionException#getCommandLine()} to get the command or subcommand where processing failed
+ * @see RunLast
+ * @see RunAll
+ * @see DefaultExceptionHandler
+ * @since 2.0 */
+ public List<Object> parseWithHandlers(IParseResultHandler handler, PrintStream out, Help.Ansi ansi, IExceptionHandler exceptionHandler, String... args) {
+ try {
+ List<CommandLine> result = parse(args);
+ return handler.handleParseResult(result, out, ansi);
+ } catch (ParameterException ex) {
+ return exceptionHandler.handleException(ex, out, ansi, args);
+ }
+ }
/**
* Equivalent to {@code new CommandLine(command).usage(out)}. See {@link #usage(PrintStream)} for details.
* @param command the object annotated with {@link Command}, {@link Option} and {@link Parameters}
@@ -426,6 +744,13 @@ public class CommandLine {
*/
public void usage(PrintStream out, Help.ColorScheme colorScheme) {
Help help = new Help(interpreter.command, colorScheme).addAllSubcommands(getSubcommands());
+ if (!Help.DEFAULT_SEPARATOR.equals(getSeparator())) {
+ help.separator = getSeparator();
+ help.parameterLabelRenderer = help.createDefaultParamLabelRenderer(); // update for new separator
+ }
+ if (!Help.DEFAULT_COMMAND_NAME.equals(getCommandName())) {
+ help.commandName = getCommandName();
+ }
StringBuilder sb = new StringBuilder()
.append(help.headerHeading())
.append(help.header())
@@ -448,6 +773,7 @@ public class CommandLine {
* Delegates to {@link #printVersionHelp(PrintStream, Help.Ansi)} with the {@linkplain Help.Ansi#AUTO platform default}.
* @param out the printStream to print to
* @see #printVersionHelp(PrintStream, Help.Ansi)
+ * @since 0.9.8
*/
public void printVersionHelp(PrintStream out) { printVersionHelp(out, Help.Ansi.AUTO); }
@@ -460,57 +786,169 @@ public class CommandLine {
* @see Command#version()
* @see Option#versionHelp()
* @see #isVersionHelpRequested()
+ * @since 0.9.8
*/
public void printVersionHelp(PrintStream out, Help.Ansi ansi) {
for (String versionInfo : versionLines) {
out.println(ansi.new Text(versionInfo));
}
}
+ /**
+ * Prints version information from the {@link Command#version()} annotation to the specified {@code PrintStream}.
+ * Each element of the array of version strings is {@linkplain String#format(String, Object...) formatted} with the
+ * specified parameters, and printed on a separate line. Both version strings and parameters may contain
+ * <a href="http://picocli.info/#_usage_help_with_styles_and_colors">markup for colors and style</a>.
+ * @param out the printStream to print to
+ * @param ansi whether the usage message should include ANSI escape codes or not
+ * @param params Arguments referenced by the format specifiers in the version strings
+ * @see Command#version()
+ * @see Option#versionHelp()
+ * @see #isVersionHelpRequested()
+ * @since 1.0.0
+ */
+ public void printVersionHelp(PrintStream out, Help.Ansi ansi, Object... params) {
+ for (String versionInfo : versionLines) {
+ out.println(ansi.new Text(String.format(versionInfo, params)));
+ }
+ }
+
+ /**
+ * Delegates to {@link #call(Callable, PrintStream, Help.Ansi, String...)} with {@link Help.Ansi#AUTO}.
+ * <p>
+ * From picocli v2.0, this method prints usage help or version help if {@linkplain #printHelpIfRequested(List, PrintStream, Help.Ansi) requested},
+ * and any exceptions thrown by the {@code Callable} are caught and rethrown wrapped in an {@code ExecutionException}.
+ * </p>
+ * @param callable the command to call when {@linkplain #parse(String...) parsing} succeeds.
+ * @param out the printStream to print to
+ * @param args the command line arguments to parse
+ * @param <C> the annotated object must implement Callable
+ * @param <T> the return type of the most specific command (must implement {@code Callable})
+ * @see #call(Callable, PrintStream, Help.Ansi, String...)
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws ExecutionException if the Callable throws an exception
+ * @return {@code null} if an error occurred while parsing the command line options, otherwise returns the result of calling the Callable
+ * @see #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)
+ * @see RunFirst
+ */
+ public static <C extends Callable<T>, T> T call(C callable, PrintStream out, String... args) {
+ return call(callable, out, Help.Ansi.AUTO, args);
+ }
+ /**
+ * Convenience method to allow command line application authors to avoid some boilerplate code in their application.
+ * The annotated object needs to implement {@link Callable}. Calling this method is equivalent to:
+ * <pre>
+ * CommandLine cmd = new CommandLine(callable);
+ * List<CommandLine> parsedCommands;
+ * try {
+ * parsedCommands = cmd.parse(args);
+ * } catch (ParameterException ex) {
+ * out.println(ex.getMessage());
+ * cmd.usage(out, ansi);
+ * return null;
+ * }
+ * if (CommandLine.printHelpIfRequested(parsedCommands, out, ansi)) {
+ * return null;
+ * }
+ * CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
+ * try {
+ * Callable<Object> subcommand = last.getCommand();
+ * return subcommand.call();
+ * } catch (Exception ex) {
+ * throw new ExecutionException(last, "Error calling " + last.getCommand(), ex);
+ * }
+ * </pre>
+ * <p>
+ * If the specified Callable command has subcommands, the {@linkplain RunLast last} subcommand specified on the
+ * command line is executed.
+ * Commands with subcommands may be interested in calling the {@link #parseWithHandler(IParseResultHandler, PrintStream, String...) parseWithHandler}
+ * method with a {@link RunAll} handler or a custom handler.
+ * </p><p>
+ * From picocli v2.0, this method prints usage help or version help if {@linkplain #printHelpIfRequested(List, PrintStream, Help.Ansi) requested},
+ * and any exceptions thrown by the {@code Callable} are caught and rethrown wrapped in an {@code ExecutionException}.
+ * </p>
+ * @param callable the command to call when {@linkplain #parse(String...) parsing} succeeds.
+ * @param out the printStream to print to
+ * @param ansi whether the usage message should include ANSI escape codes or not
+ * @param args the command line arguments to parse
+ * @param <C> the annotated object must implement Callable
+ * @param <T> the return type of the specified {@code Callable}
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws ExecutionException if the Callable throws an exception
+ * @return {@code null} if an error occurred while parsing the command line options, or if help was requested and printed. Otherwise returns the result of calling the Callable
+ * @see #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)
+ * @see RunLast
+ */
+ public static <C extends Callable<T>, T> T call(C callable, PrintStream out, Help.Ansi ansi, String... args) {
+ CommandLine cmd = new CommandLine(callable); // validate command outside of try-catch
+ List<Object> results = cmd.parseWithHandlers(new RunLast(), out, ansi, new DefaultExceptionHandler(), args);
+ return results == null || results.isEmpty() ? null : (T) results.get(0);
+ }
/**
* Delegates to {@link #run(Runnable, PrintStream, Help.Ansi, String...)} with {@link Help.Ansi#AUTO}.
- * @param command the command to run when {@linkplain #populateCommand(Object, String...) parsing} succeeds.
+ * <p>
+ * From picocli v2.0, this method prints usage help or version help if {@linkplain #printHelpIfRequested(List, PrintStream, Help.Ansi) requested},
+ * and any exceptions thrown by the {@code Runnable} are caught and rethrown wrapped in an {@code ExecutionException}.
+ * </p>
+ * @param runnable the command to run when {@linkplain #parse(String...) parsing} succeeds.
* @param out the printStream to print to
* @param args the command line arguments to parse
* @param <R> the annotated object must implement Runnable
* @see #run(Runnable, PrintStream, Help.Ansi, String...)
- * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws ExecutionException if the Runnable throws an exception
+ * @see #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)
+ * @see RunFirst
*/
- public static <R extends Runnable> void run(R command, PrintStream out, String... args) {
- run(command, out, Help.Ansi.AUTO, args);
+ public static <R extends Runnable> void run(R runnable, PrintStream out, String... args) {
+ run(runnable, out, Help.Ansi.AUTO, args);
}
/**
* Convenience method to allow command line application authors to avoid some boilerplate code in their application.
* The annotated object needs to implement {@link Runnable}. Calling this method is equivalent to:
* <pre>
- * CommandLine cmd = new CommandLine(command);
+ * CommandLine cmd = new CommandLine(runnable);
+ * List<CommandLine> parsedCommands;
* try {
- * cmd.parse(args);
- * } catch (Exception ex) {
- * System.err.println(ex.getMessage());
+ * parsedCommands = cmd.parse(args);
+ * } catch (ParameterException ex) {
+ * out.println(ex.getMessage());
* cmd.usage(out, ansi);
- * return;
+ * return null;
+ * }
+ * if (CommandLine.printHelpIfRequested(parsedCommands, out, ansi)) {
+ * return null;
+ * }
+ * CommandLine last = parsedCommands.get(parsedCommands.size() - 1);
+ * try {
+ * Runnable subcommand = last.getCommand();
+ * subcommand.run();
+ * } catch (Exception ex) {
+ * throw new ExecutionException(last, "Error running " + last.getCommand(), ex);
* }
- * command.run();
* </pre>
- * Note that this method is not suitable for commands with subcommands.
- * @param command the command to run when {@linkplain #populateCommand(Object, String...) parsing} succeeds.
+ * <p>
+ * If the specified Runnable command has subcommands, the {@linkplain RunLast last} subcommand specified on the
+ * command line is executed.
+ * Commands with subcommands may be interested in calling the {@link #parseWithHandler(IParseResultHandler, PrintStream, String...) parseWithHandler}
+ * method with a {@link RunAll} handler or a custom handler.
+ * </p><p>
+ * From picocli v2.0, this method prints usage help or version help if {@linkplain #printHelpIfRequested(List, PrintStream, Help.Ansi) requested},
+ * and any exceptions thrown by the {@code Runnable} are caught and rethrown wrapped in an {@code ExecutionException}.
+ * </p>
+ * @param runnable the command to run when {@linkplain #parse(String...) parsing} succeeds.
* @param out the printStream to print to
* @param ansi whether the usage message should include ANSI escape codes or not
* @param args the command line arguments to parse
* @param <R> the annotated object must implement Runnable
- * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws InitializationException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation
+ * @throws ExecutionException if the Runnable throws an exception
+ * @see #parseWithHandlers(IParseResultHandler, PrintStream, Help.Ansi, IExceptionHandler, String...)
+ * @see RunLast
*/
- public static <R extends Runnable> void run(R command, PrintStream out, Help.Ansi ansi, String... args) {
- CommandLine cmd = new CommandLine(command); // validate command outside of try-catch
- try {
- cmd.parse(args);
- } catch (Exception ex) {
- out.println(ex.getMessage());
- cmd.usage(out, ansi);
- return;
- }
- command.run();
+ public static <R extends Runnable> void run(R runnable, PrintStream out, Help.Ansi ansi, String... args) {
+ CommandLine cmd = new CommandLine(runnable); // validate command outside of try-catch
+ cmd.parseWithHandlers(new RunLast(), out, ansi, new DefaultExceptionHandler(), args);
}
/**
@@ -564,16 +1002,35 @@ public class CommandLine {
return this;
}
- /** Returns the String that separates option names from option values when parsing command line options. {@code '='} by default.
+ /** Returns the String that separates option names from option values when parsing command line options. {@value Help#DEFAULT_SEPARATOR} by default.
* @return the String the parser uses to separate option names from option values */
public String getSeparator() {
return interpreter.separator;
}
/** Sets the String the parser uses to separate option names from option values to the specified value.
- * @param separator the String that separates option names from option values */
- public void setSeparator(String separator) {
+ * The separator may also be set declaratively with the {@link CommandLine.Command#separator()} annotation attribute.
+ * @param separator the String that separates option names from option values
+ * @return this {@code CommandLine} object, to allow method chaining */
+ public CommandLine setSeparator(String separator) {
interpreter.separator = Assert.notNull(separator, "separator");
+ return this;
+ }
+
+ /** Returns the command name (also called program name) displayed in the usage help synopsis. {@value Help#DEFAULT_COMMAND_NAME} by default.
+ * @return the command name (also called program name) displayed in the usage */
+ public String getCommandName() {
+ return commandName;
+ }
+
+ /** Sets the command name (also called program name) displayed in the usage help synopsis to the specified value.
+ * Note that this method only modifies the usage help message, it does not impact parsing behaviour.
+ * The command name may also be set declaratively with the {@link CommandLine.Command#name()} annotation attribute.
+ * @param commandName command name (also called program name) displayed in the usage help synopsis
+ * @return this {@code CommandLine} object, to allow method chaining */
+ public CommandLine setCommandName(String commandName) {
+ this.commandName = Assert.notNull(commandName, "commandName");
+ return this;
}
private static boolean empty(String str) { return str == null || str.trim().length() == 0; }
private static boolean empty(Object[] array) { return array == null || array.length == 0; }
@@ -581,6 +1038,35 @@ public class CommandLine {
private static String str(String[] arr, int i) { return (arr == null || arr.length == 0) ? "" : arr[i]; }
private static boolean isBoolean(Class<?> type) { return type == Boolean.class || type == Boolean.TYPE; }
private static CommandLine toCommandLine(Object obj) { return obj instanceof CommandLine ? (CommandLine) obj : new CommandLine(obj);}
+ private static boolean isMultiValue(Field field) { return isMultiValue(field.getType()); }
+ private static boolean isMultiValue(Class<?> cls) { return cls.isArray() || Collection.class.isAssignableFrom(cls) || Map.class.isAssignableFrom(cls); }
+ private static Class<?>[] getTypeAttribute(Field field) {
+ Class<?>[] explicit = field.isAnnotationPresent(Parameters.class) ? field.getAnnotation(Parameters.class).type() : field.getAnnotation(Option.class).type();
+ if (explicit.length > 0) { return explicit; }
+ if (field.getType().isArray()) { return new Class<?>[] { field.getType().getComponentType() }; }
+ if (isMultiValue(field)) {
+ Type type = field.getGenericType(); // e.g. Map<Long, ? extends Number>
+ if (type instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ Type[] paramTypes = parameterizedType.getActualTypeArguments(); // e.g. ? extends Number
+ Class<?>[] result = new Class<?>[paramTypes.length];
+ for (int i = 0; i < paramTypes.length; i++) {
+ if (paramTypes[i] instanceof Class) { result[i] = (Class<?>) paramTypes[i]; continue; } // e.g. Long
+ if (paramTypes[i] instanceof WildcardType) { // e.g. ? extends Number
+ WildcardType wildcardType = (WildcardType) paramTypes[i];
+ Type[] lower = wildcardType.getLowerBounds(); // e.g. []
+ if (lower.length > 0 && lower[0] instanceof Class) { result[i] = (Class<?>) lower[0]; continue; }
+ Type[] upper = wildcardType.getUpperBounds(); // e.g. Number
+ if (upper.length > 0 && upper[0] instanceof Class) { result[i] = (Class<?>) upper[0]; continue; }
+ }
+ Arrays.fill(result, String.class); return result; // too convoluted generic type, giving up
+ }
+ return result; // we inferred all types from ParameterizedType
+ }
+ return new Class<?>[] {String.class, String.class}; // field is multi-value but not ParameterizedType
+ }
+ return new Class<?>[] {field.getType()}; // not a multi-value field
+ }
/**
* <p>
* Annotate fields in your class with {@code @Option} and picocli will initialize these fields when matching
@@ -600,10 +1086,10 @@ public class CommandLine {
* @Option(names = { "-v", "--verbose"}, description = "Verbosely list files processed")
* private boolean verbose;
*
- * @Option(names = { "-h", "--help", "-?", "-help"}, help = true, description = "Display this help and exit")
+ * @Option(names = { "-h", "--help", "-?", "-help"}, usageHelp = true, description = "Display this help and exit")
* private boolean help;
*
- * @Option(names = { "-V", "--version"}, help = true, description = "Display version information and exit")
+ * @Option(names = { "-V", "--version"}, versionHelp = true, description = "Display version information and exit")
* private boolean version;
* }
* </pre>
@@ -681,6 +1167,7 @@ public class CommandLine {
* and take the appropriate action.
* </p>
* @return whether this option disables validation of the other arguments
+ * @deprecated Use {@link #usageHelp()} and {@link #versionHelp()} instead. See {@link #printHelpIfRequested(List, PrintStream, CommandLine.Help.Ansi)}
*/
boolean help() default false;
@@ -698,6 +1185,7 @@ public class CommandLine {
* and take the appropriate action.
* </p>
* @return whether this option allows the user to request usage help
+ * @since 0.9.8
*/
boolean usageHelp() default false;
@@ -715,6 +1203,7 @@ public class CommandLine {
* and take the appropriate action.
* </p>
* @return whether this option allows the user to request version information
+ * @since 0.9.8
*/
boolean versionHelp() default false;
@@ -783,22 +1272,34 @@ public class CommandLine {
*/
String paramLabel() default "";
- /**
- * <p>
- * Specify a {@code type} if the annotated field is a {@code Collection} that should hold objects other than Strings.
+ /** <p>
+ * Optionally specify a {@code type} to control exactly what Class the option parameter should be converted
+ * to. This may be useful when the field type is an interface or an abstract class. For example, a field can
+ * be declared to have type {@code java.lang.Number}, and annotating {@code @Option(type=Short.class)}
+ * ensures that the option parameter value is converted to a {@code Short} before setting the field value.
* </p><p>
- * If the field's type is a {@code Collection}, the generic type parameter of the collection is erased and
- * cannot be determined at runtime. Specify a {@code type} attribute to store values other than String in
- * the Collection. Picocli will use the {@link ITypeConverter}
- * that is {@linkplain #registerConverter(Class, ITypeConverter) registered} for that type to convert
- * the raw String values before they are added to the collection.
+ * For array fields whose <em>component</em> type is an interface or abstract class, specify the concrete <em>component</em> type.
+ * For example, a field with type {@code Number[]} may be annotated with {@code @Option(type=Short.class)}
+ * to ensure that option parameter values are converted to {@code Short} before adding an element to the array.
* </p><p>
- * When the field's type is an array, the {@code type} attribute is ignored: the values will be converted
- * to the array component type and the array will be replaced with a new instance containing both the old and
- * the new values. </p>
- * @return the type to convert the raw String values to before adding them to the Collection
+ * Picocli will use the {@link ITypeConverter} that is
+ * {@linkplain #registerConverter(Class, ITypeConverter) registered} for the specified type to convert
+ * the raw String values before modifying the field value.
+ * </p><p>
+ * Prior to 2.0, the {@code type} attribute was necessary for {@code Collection} and {@code Map} fields,
+ * but starting from 2.0 picocli will infer the component type from the generic type's type arguments.
+ * For example, for a field of type {@code Map<TimeUnit, Long>} picocli will know the option parameter
+ * should be split up in key=value pairs, where the key should be converted to a {@code java.util.concurrent.TimeUnit}
+ * enum value, and the value should be converted to a {@code Long}. No {@code @Option(type=...)} type attribute
+ * is required for this. For generic types with wildcards, picocli will take the specified upper or lower bound
+ * as the Class to convert to, unless the {@code @Option} annotation specifies an explicit {@code type} attribute.
+ * </p><p>
+ * If the field type is a raw collection or a raw map, and you want it to contain other values than Strings,
+ * or if the generic type's type arguments are interfaces or abstract classes, you may
+ * specify a {@code type} attribute to control the Class that the option parameter should be converted to.
+ * @return the type(s) to convert the raw String values
*/
- Class<?> type() default String.class;
+ Class<?>[] type() default {};
/**
* Specify a regular expression to use to split option parameter values before applying them to the field.
@@ -881,20 +1382,33 @@ public class CommandLine {
/**
* <p>
- * Specify a {@code type} if the annotated field is a {@code Collection} that should hold objects other than Strings.
+ * Optionally specify a {@code type} to control exactly what Class the positional parameter should be converted
+ * to. This may be useful when the field type is an interface or an abstract class. For example, a field can
+ * be declared to have type {@code java.lang.Number}, and annotating {@code @Parameters(type=Short.class)}
+ * ensures that the positional parameter value is converted to a {@code Short} before setting the field value.
+ * </p><p>
+ * For array fields whose <em>component</em> type is an interface or abstract class, specify the concrete <em>component</em> type.
+ * For example, a field with type {@code Number[]} may be annotated with {@code @Parameters(type=Short.class)}
+ * to ensure that positional parameter values are converted to {@code Short} before adding an element to the array.
* </p><p>
- * If the field's type is a {@code Collection}, the generic type parameter of the collection is erased and
- * cannot be determined at runtime. Specify a {@code type} attribute to store values other than String in
- * the Collection. Picocli will use the {@link ITypeConverter}
- * that is {@linkplain #registerConverter(Class, ITypeConverter) registered} for that type to convert
- * the raw String values before they are added to the collection.
+ * Picocli will use the {@link ITypeConverter} that is
+ * {@linkplain #registerConverter(Class, ITypeConverter) registered} for the specified type to convert
+ * the raw String values before modifying the field value.
* </p><p>
- * When the field's type is an array, the {@code type} attribute is ignored: the values will be converted
- * to the array component type and the array will be replaced with a new instance containing both the old and
- * the new values. </p>
- * @return the type to convert the raw String values to before adding them to the Collection
+ * Prior to 2.0, the {@code type} attribute was necessary for {@code Collection} and {@code Map} fields,
+ * but starting from 2.0 picocli will infer the component type from the generic type's type arguments.
+ * For example, for a field of type {@code Map<TimeUnit, Long>} picocli will know the positional parameter
+ * should be split up in key=value pairs, where the key should be converted to a {@code java.util.concurrent.TimeUnit}
+ * enum value, and the value should be converted to a {@code Long}. No {@code @Parameters(type=...)} type attribute
+ * is required for this. For generic types with wildcards, picocli will take the specified upper or lower bound
+ * as the Class to convert to, unless the {@code @Parameters} annotation specifies an explicit {@code type} attribute.
+ * </p><p>
+ * If the field type is a raw collection or a raw map, and you want it to contain other values than Strings,
+ * or if the generic type's type arguments are interfaces or abstract classes, you may
+ * specify a {@code type} attribute to control the Class that the positional parameter should be converted to.
+ * @return the type(s) to convert the raw String values
*/
- Class<?> type() default String.class;
+ Class<?>[] type() default {};
/**
* Specify a regular expression to use to split positional parameter values before applying them to the field.
@@ -936,7 +1450,7 @@ public class CommandLine {
* <li>[footer]</li>
* </ul> */
@Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.TYPE)
+ @Target({ElementType.TYPE, ElementType.LOCAL_VARIABLE, ElementType.PACKAGE})
public @interface Command {
/** Program name to show in the synopsis. If omitted, {@code "<main class>"} is used.
* For {@linkplain #subcommands() declaratively added} subcommands, this attribute is also used
@@ -1174,19 +1688,41 @@ public class CommandLine {
: new Range(0, 0, false, true, "0");
}
static Range adjustForType(Range result, Field field) {
- return result.isUnspecified ? defaultArity(field.getType()) : result;
+ return result.isUnspecified ? defaultArity(field) : result;
}
- /** Returns a new {@code Range} based on the specified type: booleans have arity 0, arrays or Collections have
+ /** Returns the default arity {@code Range}: for {@link Option options} this is 0 for booleans and 1 for
+ * other types, for {@link Parameters parameters} booleans have arity 0, arrays or Collections have
* arity "0..*", and other types have arity 1.
+ * @param field the field whose default arity to return
+ * @return a new {@code Range} indicating the default arity of the specified field
+ * @since 2.0 */
+ public static Range defaultArity(Field field) {
+ Class<?> type = field.getType();
+ if (field.isAnnotationPresent(Option.class)) {
+ return defaultArity(type);
+ }
+ if (isMultiValue(type)) {
+ return Range.valueOf("0..1");
+ }
+ return Range.valueOf("1");// for single-valued fields (incl. boolean positional parameters)
+ }
+ /** Returns the default arity {@code Range} for {@link Option options}: booleans have arity 0, other types have arity 1.
* @param type the type whose default arity to return
* @return a new {@code Range} indicating the default arity of the specified type */
public static Range defaultArity(Class<?> type) {
- if (isBoolean(type)) {
- return Range.valueOf("0");
- } else if (type.isArray() || Collection.class.isAssignableFrom(type)) {
- return Range.valueOf("0..*");
- }
- return Range.valueOf("1");// for single-valued fields
+ return isBoolean(type) ? Range.valueOf("0") : Range.valueOf("1");
+ }
+ private int size() { return 1 + max - min; }
+ static Range parameterCapacity(Field field) {
+ Range arity = parameterArity(field);
+ if (!isMultiValue(field)) { return arity; }
+ Range index = parameterIndex(field);
+ if (arity.max == 0) { return arity; }
+ if (index.size() == 1) { return arity; }
+ if (index.isVariable) { return Range.valueOf(arity.min + "..*"); }
+ if (arity.size() == 1) { return Range.valueOf(arity.min * index.size() + ""); }
+ if (arity.isVariable) { return Range.valueOf(arity.min * index.size() + "..*"); }
+ return Range.valueOf(arity.min * index.size() + ".." + arity.max * index.size());
}
/** Leniently parses the specified String as an {@code Range} value and return the result. A range string can
* be a fixed integer value or a range of the form {@code MIN_VALUE + ".." + MAX_VALUE}. If the
@@ -1231,6 +1767,13 @@ public class CommandLine {
* @return a new Range object with the specified {@code max} value */
public Range max(int newMax) { return new Range(Math.min(min, newMax), newMax, isVariable, isUnspecified, originalValue); }
+ /**
+ * Returns {@code true} if this Range includes the specified value, {@code false} otherwise.
+ * @param value the value to check
+ * @return {@code true} if the specified value is not less than the minimum and not greater than the maximum of this Range
+ */
+ public boolean contains(int value) { return min <= value && max >= value; }
+
public boolean equals(Object object) {
if (!(object instanceof Range)) { return false; }
Range other = (Range) object;
@@ -1247,11 +1790,11 @@ public class CommandLine {
return (result == 0) ? max - other.max : result;
}
}
- private static void init(Class<?> cls,
- List<Field> requiredFields,
- Map<String, Field> optionName2Field,
- Map<Character, Field> singleCharOption2Field,
- List<Field> positionalParametersFields) {
+ static void init(Class<?> cls,
+ List<Field> requiredFields,
+ Map<String, Field> optionName2Field,
+ Map<Character, Field> singleCharOption2Field,
+ List<Field> positionalParametersFields) {
Field[] declaredFields = cls.getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
@@ -1276,7 +1819,7 @@ public class CommandLine {
}
if (field.isAnnotationPresent(Parameters.class)) {
if (field.isAnnotationPresent(Option.class)) {
- throw new ParameterException("A field can be either @Option or @Parameters, but '"
+ throw new DuplicateOptionAnnotationsException("A field can be either @Option or @Parameters, but '"
+ field.getName() + "' is both.");
}
positionalParametersFields.add(field);
@@ -1315,7 +1858,8 @@ public class CommandLine {
private final List<Field> positionalParametersFields = new ArrayList<Field>();
private final Object command;
private boolean isHelpRequested;
- private String separator = "=";
+ private String separator = Help.DEFAULT_SEPARATOR;
+ private int position;
Interpreter(Object command) {
converterRegistry.put(Path.class, new BuiltIn.PathConverter());
@@ -1350,9 +1894,10 @@ public class CommandLine {
converterRegistry.put(Pattern.class, new BuiltIn.PatternConverter());
converterRegistry.put(UUID.class, new BuiltIn.UUIDConverter());
- this.command = Assert.notNull(command, "command");
- Class<?> cls = command.getClass();
- String declaredSeparator = null;
+ this.command = Assert.notNull(command, "command");
+ Class<?> cls = command.getClass();
+ String declaredName = null;
+ String declaredSeparator = null;
boolean hasCommandAnnotation = false;
while (cls != null) {
init(cls, requiredFields, optionName2Field, singleCharOption2Field, positionalParametersFields);
@@ -1360,12 +1905,13 @@ public class CommandLine {
hasCommandAnnotation = true;
Command cmd = cls.getAnnotation(Command.class);
declaredSeparator = (declaredSeparator == null) ? cmd.separator() : declaredSeparator;
+ declaredName = (declaredName == null) ? cmd.name() : declaredName;
CommandLine.this.versionLines.addAll(Arrays.asList(cmd.version()));
for (Class<?> sub : cmd.subcommands()) {
Command subCommand = sub.getAnnotation(Command.class);
if (subCommand == null || Help.DEFAULT_COMMAND_NAME.equals(subCommand.name())) {
- throw new IllegalArgumentException("Subcommand " + sub.getName() +
+ throw new InitializationException("Subcommand " + sub.getName() +
" is missing the mandatory @Command annotation with a 'name' attribute");
}
try {
@@ -1375,11 +1921,11 @@ public class CommandLine {
commandLine.parent = CommandLine.this;
commands.put(subCommand.name(), commandLine);
}
- catch (IllegalArgumentException ex) { throw ex; }
- catch (NoSuchMethodException ex) { throw new IllegalArgumentException("Cannot instantiate subcommand " +
+ catch (InitializationException ex) { throw ex; }
+ catch (NoSuchMethodException ex) { throw new InitializationException("Cannot instantiate subcommand " +
sub.getName() + ": the class has no constructor", ex); }
catch (Exception ex) {
- throw new IllegalStateException("Could not instantiate and add subcommand " +
+ throw new InitializationException("Could not instantiate and add subcommand " +
sub.getName() + ": " + ex, ex);
}
}
@@ -1387,11 +1933,12 @@ public class CommandLine {
cls = cls.getSuperclass();
}
separator = declaredSeparator != null ? declaredSeparator : separator;
+ CommandLine.this.commandName = declaredName != null ? declaredName : CommandLine.this.commandName;
Collections.sort(positionalParametersFields, new PositionalParametersSorter());
validatePositionalParameters(positionalParametersFields);
if (positionalParametersFields.isEmpty() && optionName2Field.isEmpty() && !hasCommandAnnotation) {
- throw new IllegalArgumentException(command + " (" + command.getClass() +
+ throw new InitializationException(command + " (" + command.getClass() +
") is not a command: it has no @Command, @Option or @Parameters annotations");
}
}
@@ -1404,6 +1951,7 @@ public class CommandLine {
*/
List<CommandLine> parse(String... args) {
Assert.notNull(args, "argument array");
+ if (tracer.isInfo()) {tracer.info("Parsing %d command line args %s%n", args.length, Arrays.toString(args));}
Stack<String> arguments = new Stack<String>();
for (int i = args.length - 1; i >= 0; i--) {
arguments.push(args[i]);
@@ -1419,6 +1967,8 @@ public class CommandLine {
CommandLine.this.versionHelpRequested = false;
CommandLine.this.usageHelpRequested = false;
+ Class<?> cmdClass = this.command.getClass();
+ if (tracer.isDebug()) {tracer.debug("Initializing %s: %d options, %d positional parameters, %d required, %d subcommands.%n", cmdClass.getName(), new HashSet<Field>(optionName2Field.values()).size(), positionalParametersFields.size(), requiredFields.size(), commands.size());}
parsedCommands.add(CommandLine.this);
List<Field> required = new ArrayList<Field>(requiredFields);
Set<Field> initialized = new HashSet<Field>();
@@ -1428,20 +1978,23 @@ public class CommandLine {
} catch (ParameterException ex) {
throw ex;
} catch (Exception ex) {
- int offendingArgIndex = originalArgs.length - argumentStack.size();
+ int offendingArgIndex = originalArgs.length - argumentStack.size() - 1;
String arg = offendingArgIndex >= 0 && offendingArgIndex < originalArgs.length ? originalArgs[offendingArgIndex] : "?";
- throw ParameterException.create(ex, arg, argumentStack.size(), originalArgs);
+ throw ParameterException.create(CommandLine.this, ex, arg, offendingArgIndex, originalArgs);
}
if (!isAnyHelpRequested() && !required.isEmpty()) {
- if (required.get(0).isAnnotationPresent(Option.class)) {
- throw MissingParameterException.create(required);
- } else {
- try {
- processPositionalParameters0(required, true, new Stack<String>());
- } catch (ParameterException ex) { throw ex;
- } catch (Exception ex) { throw new IllegalStateException("Internal error: " + ex, ex); }
+ for (Field missing : required) {
+ if (missing.isAnnotationPresent(Option.class)) {
+ throw MissingParameterException.create(CommandLine.this, required, separator);
+ } else {
+ assertNoMissingParameters(missing, Range.parameterArity(missing).min, argumentStack);
+ }
}
}
+ if (!unmatchedArguments.isEmpty()) {
+ if (!isUnmatchedArgumentsAllowed()) { throw new UnmatchedArgumentException(CommandLine.this, unmatchedArguments); }
+ if (tracer.isWarn()) { tracer.warn("Unmatched arguments: %s%n", unmatchedArguments); }
+ }
}
private void processArguments(List<CommandLine> parsedCommands,
@@ -1460,19 +2013,22 @@ public class CommandLine {
while (!args.isEmpty()) {
String arg = args.pop();
+ if (tracer.isDebug()) {tracer.debug("Processing argument '%s'. Remainder=%s%n", arg, reverse((Stack<String>) args.clone()));}
// Double-dash separates options from positional arguments.
// If found, then interpret the remaining args as positional parameters.
if ("--".equals(arg)) {
- processPositionalParameters(required, args);
+ tracer.info("Found end-of-options delimiter '--'. Treating remainder as positional parameters.%n");
+ processRemainderAsPositionalParameters(required, initialized, args);
return; // we are done
}
// if we find another command, we are done with the current command
if (commands.containsKey(arg)) {
if (!isHelpRequested && !required.isEmpty()) { // ensure current command portion is valid
- throw MissingParameterException.create(required);
+ throw MissingParameterException.create(CommandLine.this, required, separator);
}
+ if (tracer.isDebug()) {tracer.debug("Found subcommand '%s' (%s)%n", arg, commands.get(arg).interpreter.command.getClass().getName());}
commands.get(arg).interpreter.parse(parsedCommands, args, originalArgs);
return; // remainder done by the command
}
@@ -1491,7 +2047,12 @@ public class CommandLine {
String optionParam = arg.substring(separatorIndex + separator.length());
args.push(optionParam);
arg = key;
+ if (tracer.isDebug()) {tracer.debug("Separated '%s' option from '%s' option parameter%n", key, optionParam);}
+ } else {
+ if (tracer.isDebug()) {tracer.debug("'%s' contains separator '%s' but '%s' is not a known option%n", arg, separator, key);}
}
+ } else {
+ if (tracer.isDebug()) {tracer.debug("'%s' cannot be separated into <option>%s<option-parameter>%n", arg, separator);}
}
if (optionName2Field.containsKey(arg)) {
processStandaloneOption(required, initialized, arg, args, paramAttachedToOption);
@@ -1499,56 +2060,66 @@ public class CommandLine {
// Compact (single-letter) options can be grouped with other options or with an argument.
// only single-letter options can be combined with other options or with an argument
else if (arg.length() > 2 && arg.startsWith("-")) {
+ if (tracer.isDebug()) {tracer.debug("Trying to process '%s' as clustered short options%n", arg, args);}
processClusteredShortOptions(required, initialized, arg, args);
}
// The argument could not be interpreted as an option.
// We take this to mean that the remainder are positional arguments
else {
args.push(arg);
- processPositionalParameters(required, args);
- return;
+ if (tracer.isDebug()) {tracer.debug("Could not find option '%s', deciding whether to treat as unmatched option or positional parameter...%n", arg);}
+ if (resemblesOption(arg)) { handleUnmatchedArguments(args.pop()); continue; } // #149
+ if (tracer.isDebug()) {tracer.debug("No option named '%s' found. Processing remainder as positional parameters%n", arg);}
+ processPositionalParameter(required, initialized, args);
}
}
}
-
- private void processPositionalParameters(Collection<Field> required, Stack<String> args) throws Exception {
- processPositionalParameters0(required, false, args);
- if (!args.empty()) {
- handleUnmatchedArguments(args);
- return;
- };
+ private boolean resemblesOption(String arg) {
+ int count = 0;
+ for (String optionName : optionName2Field.keySet()) {
+ for (int i = 0; i < arg.length(); i++) {
+ if (optionName.length() > i && arg.charAt(i) == optionName.charAt(i)) { count++; } else { break; }
+ }
+ }
+ boolean result = count > 0 && count * 10 >= optionName2Field.size() * 9; // at least one prefix char in common with 9 out of 10 options
+ if (tracer.isDebug()) {tracer.debug("%s %s an option: %d matching prefix chars out of %d option names%n", arg, (result ? "resembles" : "doesn't resemble"), count, optionName2Field.size());}
+ return result;
}
-
+ private void handleUnmatchedArguments(String arg) {Stack<String> args = new Stack<String>(); args.add(arg); handleUnmatchedArguments(args);}
private void handleUnmatchedArguments(Stack<String> args) {
- if (!isUnmatchedArgumentsAllowed()) { throw new UnmatchedArgumentException(args); }
while (!args.isEmpty()) { unmatchedArguments.add(args.pop()); } // addAll would give args in reverse order
}
- private void processPositionalParameters0(Collection<Field> required, boolean validateOnly, Stack<String> args) throws Exception {
- int max = -1;
+ private void processRemainderAsPositionalParameters(Collection<Field> required, Set<Field> initialized, Stack<String> args) throws Exception {
+ while (!args.empty()) {
+ processPositionalParameter(required, initialized, args);
+ }
+ }
+ private void processPositionalParameter(Collection<Field> required, Set<Field> initialized, Stack<String> args) throws Exception {
+ if (tracer.isDebug()) {tracer.debug("Processing next arg as a positional parameter at index=%d. Remainder=%s%n", position, reverse((Stack<String>) args.clone()));}
+ int consumed = 0;
for (Field positionalParam : positionalParametersFields) {
Range indexRange = Range.parameterIndex(positionalParam);
- max = Math.max(max, indexRange.max);
- @SuppressWarnings("unchecked")
- Stack<String> argsCopy = reverse((Stack<String>) args.clone());
- if (!indexRange.isVariable) {
- for (int i = argsCopy.size() - 1; i > indexRange.max; i--) {
- argsCopy.removeElementAt(i);
- }
+ if (!indexRange.contains(position)) {
+ continue;
}
- Collections.reverse(argsCopy);
- for (int i = 0; i < indexRange.min && !argsCopy.isEmpty(); i++) { argsCopy.pop(); }
+ @SuppressWarnings("unchecked")
+ Stack<String> argsCopy = (Stack<String>) args.clone();
Range arity = Range.parameterArity(positionalParam);
+ if (tracer.isDebug()) {tracer.debug("Position %d is in index range %s. Trying to assign args to %s, arity=%s%n", position, indexRange, positionalParam, arity);}
assertNoMissingParameters(positionalParam, arity.min, argsCopy);
- if (!validateOnly) {
- applyOption(positionalParam, Parameters.class, arity, false, argsCopy, null);
- required.remove(positionalParam);
- }
+ int originalSize = argsCopy.size();
+ applyOption(positionalParam, Parameters.class, arity, false, argsCopy, initialized, "args[" + indexRange + "]");
+ int count = originalSize - argsCopy.size();
+ if (count > 0) { required.remove(positionalParam); }
+ consumed = Math.max(consumed, count);
}
// remove processed args from the stack
- if (!validateOnly && !positionalParametersFields.isEmpty()) {
- int processedArgCount = Math.min(args.size(), max < Integer.MAX_VALUE ? max + 1 : Integer.MAX_VALUE);
- for (int i = 0; i < processedArgCount; i++) { args.pop(); }
+ for (int i = 0; i < consumed; i++) { args.pop(); }
+ position += consumed;
+ if (tracer.isDebug()) {tracer.debug("Consumed %d arguments, moving position to index %d.%n", consumed, position);}
+ if (consumed == 0 && !args.isEmpty()) {
+ handleUnmatchedArguments(args.pop());
}
}
@@ -1563,7 +2134,8 @@ public class CommandLine {
if (paramAttachedToKey) {
arity = arity.min(Math.max(1, arity.min)); // if key=value, minimum arity is at least 1
}
- applyOption(field, Option.class, arity, paramAttachedToKey, args, initialized);
+ if (tracer.isDebug()) {tracer.debug("Found option named '%s': field %s, arity=%s%n", arg, field, arity);}
+ applyOption(field, Option.class, arity, paramAttachedToKey, args, initialized, "option " + arg);
}
private void processClusteredShortOptions(Collection<Field> required,
@@ -1577,19 +2149,27 @@ public class CommandLine {
do {
if (cluster.length() > 0 && singleCharOption2Field.containsKey(cluster.charAt(0))) {
Field field = singleCharOption2Field.get(cluster.charAt(0));
+ Range arity = Range.optionArity(field);
+ String argDescription = "option " + prefix + cluster.charAt(0);
+ if (tracer.isDebug()) {tracer.debug("Found option '%s%s' in %s: field %s, arity=%s%n", prefix, cluster.charAt(0), arg, field, arity);}
required.remove(field);
cluster = cluster.length() > 0 ? cluster.substring(1) : "";
paramAttachedToOption = cluster.length() > 0;
- Range arity = Range.optionArity(field);
if (cluster.startsWith(separator)) {// attached with separator, like -f=FILE or -v=true
cluster = cluster.substring(separator.length());
arity = arity.min(Math.max(1, arity.min)); // if key=value, minimum arity is at least 1
}
+ if (arity.min > 0 && !empty(cluster)) {
+ if (tracer.isDebug()) {tracer.debug("Trying to process '%s' as option parameter%n", cluster);}
+ }
args.push(cluster); // interpret remainder as option parameter (CAUTION: may be empty string!)
// arity may be >= 1, or
// arity <= 0 && !cluster.startsWith(separator)
// e.g., boolean @Option("-v", arity=0, varargs=true); arg "-rvTRUE", remainder cluster="TRUE"
- int consumed = applyOption(field, Option.class, arity, paramAttachedToOption, args, initialized);
+ if (!args.isEmpty() && args.peek().length() == 0 && !paramAttachedToOption) {
+ args.pop(); // throw out empty string we get at the end of a group of clustered short options
+ }
+ int consumed = applyOption(field, Option.class, arity, paramAttachedToOption, args, initialized, argDescription);
// only return if cluster (and maybe more) was consumed, otherwise continue do-while loop
if (consumed > 0) {
return;
@@ -1602,12 +2182,21 @@ public class CommandLine {
// We get here when the remainder of the cluster group is neither an option,
// nor a parameter that the last option could consume.
if (arg.endsWith(cluster)) {
- // remainder was part of a clustered group that could not be completely parsed
args.push(paramAttachedToOption ? prefix + cluster : cluster);
- handleUnmatchedArguments(args);
+ if (args.peek().equals(arg)) { // #149 be consistent between unmatched short and long options
+ if (tracer.isDebug()) {tracer.debug("Could not match any short options in %s, deciding whether to treat as unmatched option or positional parameter...%n", arg);}
+ if (resemblesOption(arg)) { handleUnmatchedArguments(args.pop()); return; } // #149
+ processPositionalParameter(required, initialized, args);
+ return;
+ }
+ // remainder was part of a clustered group that could not be completely parsed
+ if (tracer.isDebug()) {tracer.debug("No option found for %s in %s%n", cluster, arg);}
+ handleUnmatchedArguments(args.pop());
+ } else {
+ args.push(cluster);
+ if (tracer.isDebug()) {tracer.debug("%s is not an option parameter for %s%n", cluster, arg);}
+ processPositionalParameter(required, initialized, args);
}
- args.push(cluster);
- processPositionalParameters(required, args);
return;
}
} while (true);
@@ -1618,28 +2207,32 @@ public class CommandLine {
Range arity,
boolean valueAttachedToOption,
Stack<String> args,
- Set<Field> initialized) throws Exception {
+ Set<Field> initialized,
+ String argDescription) throws Exception {
updateHelpRequested(field);
- if (!args.isEmpty() && args.peek().length() == 0 && !valueAttachedToOption) {
- args.pop(); // throw out empty string we get at the end of a group of clustered short options
- }
int length = args.size();
assertNoMissingParameters(field, arity.min, args);
Class<?> cls = field.getType();
if (cls.isArray()) {
- return applyValuesToArrayField(field, annotation, arity, args, cls);
+ return applyValuesToArrayField(field, annotation, arity, args, cls, argDescription);
}
if (Collection.class.isAssignableFrom(cls)) {
- return applyValuesToCollectionField(field, annotation, arity, args, cls);
+ return applyValuesToCollectionField(field, annotation, arity, args, cls, argDescription);
+ }
+ if (Map.class.isAssignableFrom(cls)) {
+ return applyValuesToMapField(field, annotation, arity, args, cls, argDescription);
}
- return applyValueToSingleValuedField(field, arity, args, cls, initialized);
+ cls = getTypeAttribute(field)[0]; // field may be interface/abstract type, use annotation to get concrete type
+ return applyValueToSingleValuedField(field, arity, args, cls, initialized, argDescription);
}
+
private int applyValueToSingleValuedField(Field field,
Range arity,
Stack<String> args,
Class<?> cls,
- Set<Field> initialized) throws Exception {
+ Set<Field> initialized,
+ String argDescription) throws Exception {
boolean noMoreValues = args.isEmpty();
String value = args.isEmpty() ? null : trim(args.pop()); // unquote the value
int result = arity.min; // the number or args we need to consume
@@ -1661,28 +2254,117 @@ public class CommandLine {
if (noMoreValues && value == null) {
return 0;
}
+ ITypeConverter<?> converter = getTypeConverter(cls, field);
+ Object newValue = tryConvert(field, -1, converter, value, cls);
+ Object oldValue = field.get(command);
+ TraceLevel level = TraceLevel.INFO;
+ String traceMessage = "Setting %s field '%s.%s' to '%5$s' (was '%4$s') for %6$s%n";
if (initialized != null) {
- if (initialized.contains(field) && !isOverwrittenOptionsAllowed()) {
- throw new OverwrittenOptionException(optionDescription("", field, 0) + " should be specified only once");
+ if (initialized.contains(field)) {
+ if (!isOverwrittenOptionsAllowed()) {
+ throw new OverwrittenOptionException(CommandLine.this, optionDescription("", field, 0) + " should be
<TRUNCATED>