http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/e2f9d5ed/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 new file mode 100644 index 0000000..615f097 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/tools/picocli/CommandLine.java @@ -0,0 +1,3900 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache license, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the license for the specific language governing permissions and + * limitations under the license. + */ +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; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Time; +import java.text.BreakIterator; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +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.regex.Pattern; + +import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi; +import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi.IStyle; +import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi.Style; +import org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Ansi.Text; + +import static java.util.Locale.ENGLISH; +import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Column.Overflow.SPAN; +import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Column.Overflow.TRUNCATE; +import static org.apache.logging.log4j.core.tools.picocli.CommandLine.Help.Column.Overflow.WRAP; + +/** + * <p> + * CommandLine interpreter that uses reflection to initialize an annotated domain object with values obtained from the + * command line arguments. + * </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.") + * public class Encrypt { + * + * @Parameters(type = File.class, description = "Any number of input files") + * private List<File> files = new ArrayList<File>(); + * + * @Option(names = { "-o", "--out" }, description = "Output file (default: print to console)") + * private File outputFile; + * + * @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") + * private boolean help; + * } + * </pre> + * <p> + * Use {@code CommandLine} to initialize a domain object as follows: + * </p><pre> + * public static void main(String... args) { + * try { + * Encrypt encrypt = CommandLine.populateCommand(new Encrypt(), args); + * if (encrypt.help) { + * CommandLine.usage(encrypt, System.out); + * } else { + * runProgram(encrypt); + * } + * } catch (ParameterException ex) { // command line arguments could not be parsed + * System.err.println(ex.getMessage()); + * CommandLine.usage(new Encrypt(), System.err); + * } + * } + * </pre><p> + * Invoke the above program with some command line arguments. The below are all equivalent: + * </p> + * <pre> + * --verbose --out=outfile in1 in2 + * --verbose --out outfile in1 in2 + * -v --out=outfile in1 in2 + * -v -o outfile in1 in2 + * -v -o=outfile in1 in2 + * -vo outfile in1 in2 + * -vo=outfile in1 in2 + * -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"; + + private final Interpreter interpreter; + private boolean overwrittenOptionsAllowed = false; + private boolean unmatchedArgumentsAllowed = false; + private List<String> unmatchedArguments = new ArrayList<String>(); + private CommandLine parent; + private boolean usageHelpRequested; + private boolean versionHelpRequested; + private List<String> versionLines = new ArrayList<String>(); + + /** + * Constructs a new {@code CommandLine} interpreter with the specified annotated object. + * 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 + */ + public CommandLine(Object command) { + interpreter = new Interpreter(command); + } + + /** Registers a subcommand with the specified name. For example: + * <pre> + * CommandLine commandLine = new CommandLine(new Git()) + * .addSubcommand("status", new GitStatus()) + * .addSubcommand("commit", new GitCommit(); + * .addSubcommand("add", new GitAdd()) + * .addSubcommand("branch", new GitBranch()) + * .addSubcommand("checkout", new GitCheckout()) + * //... + * ; + * </pre> + * + * <p>The specified object can be an annotated object or a + * {@code CommandLine} instance with its own nested subcommands. For example:</p> + * <pre> + * CommandLine commandLine = new CommandLine(new MainCommand()) + * .addSubcommand("cmd1", new ChildCommand1()) // subcommand + * .addSubcommand("cmd2", new ChildCommand2()) + * .addSubcommand("cmd3", new CommandLine(new ChildCommand3()) // subcommand with nested sub-subcommands + * .addSubcommand("cmd3sub1", new GrandChild3Command1()) + * .addSubcommand("cmd3sub2", new GrandChild3Command2()) + * .addSubcommand("cmd3sub3", new CommandLine(new GrandChild3Command3()) // deeper nesting + * .addSubcommand("cmd3sub3sub1", new GreatGrandChild3Command3_1()) + * .addSubcommand("cmd3sub3sub2", new GreatGrandChild3Command3_2()) + * ) + * ); + * </pre> + * <p>The default type converters are available on all subcommands and nested sub-subcommands, but custom type + * 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> + * + * @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. + * This may be a {@code CommandLine} instance with its own (nested) subcommands + * @return this CommandLine object, to allow method chaining + * @see #registerConverter(Class, ITypeConverter) + * @since 0.9.7 + */ + public CommandLine addSubcommand(String name, Object command) { + CommandLine commandLine = toCommandLine(command); + commandLine.parent = this; + interpreter.commands.put(name, commandLine); + return this; + } + /** Returns a map with the subcommands {@linkplain #addSubcommand(String, Object) registered} on this instance. + * @return a map with the registered subcommands + * @since 0.9.7 + */ + public Map<String, CommandLine> getSubcommands() { + return new LinkedHashMap<String, CommandLine>(interpreter.commands); + } + /** + * Returns the command that this is a subcommand of, or {@code null} if this is a top-level command. + * @return the command that this is a subcommand of, or {@code null} if this is a top-level command + * @see #addSubcommand(String, Object) + * @see Command#subcommands() + * @since 0.9.8 + */ + public CommandLine getParent() { + return parent; + } + + /** + * Returns the annotated object that this {@code CommandLine} instance was constructed with. + * @return the annotated object that this {@code CommandLine} instance was constructed with + * @since 0.9.7 + */ + public Object getCommand() { + return 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()} */ + 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()} */ + public boolean isVersionHelpRequested() { return versionHelpRequested; } + + /** Returns whether options for single-value fields can be specified multiple times on the command line. + * The default is {@code false} and a {@link OverwrittenOptionException} is thrown if this happens. + * When {@code true}, the last specified value is retained. + * @return {@code true} if options for single-value fields can be specified multiple times on the command line, {@code false} otherwise + * @since 0.9.7 + */ + public boolean isOverwrittenOptionsAllowed() { + return overwrittenOptionsAllowed; + } + + /** Sets whether options for single-value fields can be specified multiple times on the command line without a {@link OverwrittenOptionException} being thrown. + * <p>The specified setting will be registered with this {@code CommandLine} and the full hierarchy of its + * 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 + * @return this {@code CommandLine} object, to allow method chaining + * @since 0.9.7 + */ + public CommandLine setOverwrittenOptionsAllowed(boolean newValue) { + this.overwrittenOptionsAllowed = newValue; + for (CommandLine command : interpreter.commands.values()) { + command.setOverwrittenOptionsAllowed(newValue); + } + return this; + } + + /** Returns whether the end user may specify arguments on the command line that are not matched to any option or parameter fields. + * The default is {@code false} and a {@link UnmatchedArgumentException} is thrown if this happens. + * When {@code true}, the last unmatched arguments are available via the {@link #getUnmatchedArguments()} method. + * @return {@code true} if the end use may specify unmatched arguments on the command line, {@code false} otherwise + * @see #getUnmatchedArguments() + * @since 0.9.7 + */ + public boolean isUnmatchedArgumentsAllowed() { + return unmatchedArgumentsAllowed; + } + + /** Sets whether the end user may specify unmatched arguments on the command line without a {@link UnmatchedArgumentException} being thrown. + * <p>The specified setting will be registered with this {@code CommandLine} and the full hierarchy of its + * 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 + * @return this {@code CommandLine} object, to allow method chaining + * @since 0.9.7 + */ + public CommandLine setUnmatchedArgumentsAllowed(boolean newValue) { + this.unmatchedArgumentsAllowed = newValue; + for (CommandLine command : interpreter.commands.values()) { + command.setUnmatchedArgumentsAllowed(newValue); + } + return this; + } + + /** Returns the list of unmatched command line arguments, if any. + * @return the list of unmatched command line arguments or an empty list + * @see #isUnmatchedArgumentsAllowed() + * @since 0.9.7 + */ + public List<String> getUnmatchedArguments() { + return unmatchedArguments; + } + + /** + * <p> + * Convenience method that initializes the specified annotated object from the specified command line arguments. + * </p><p> + * This is equivalent to + * </p><pre> + * CommandLine cli = new CommandLine(command); + * cli.parse(args); + * return command; + * </pre> + * + * @param command the object to initialize. This object contains fields annotated with + * {@code @Option} or {@code @Parameters}. + * @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 ParameterException if the specified command line arguments are invalid + * @since 0.9.7 + */ + public static <T> T populateCommand(T command, String... args) { + CommandLine cli = toCommandLine(command); + cli.parse(args); + return command; + } + + /** + * <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. + * </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 + */ + public List<CommandLine> parse(String... args) { + return interpreter.parse(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} + * @param out the print stream to print the help message to + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation + */ + public static void usage(Object command, PrintStream out) { + toCommandLine(command).usage(out); + } + + /** + * Equivalent to {@code new CommandLine(command).usage(out, ansi)}. + * See {@link #usage(PrintStream, Help.Ansi)} for details. + * @param command the object annotated with {@link Command}, {@link Option} and {@link Parameters} + * @param out the print stream to print the help message to + * @param ansi whether the usage message should contain ANSI escape codes or not + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation + */ + public static void usage(Object command, PrintStream out, Help.Ansi ansi) { + toCommandLine(command).usage(out, ansi); + } + + /** + * Equivalent to {@code new CommandLine(command).usage(out, colorScheme)}. + * See {@link #usage(PrintStream, Help.ColorScheme)} for details. + * @param command the object annotated with {@link Command}, {@link Option} and {@link Parameters} + * @param out the print stream to print the help message to + * @param colorScheme the {@code ColorScheme} defining the styles for options, parameters and commands when ANSI is enabled + * @throws IllegalArgumentException if the specified command object does not have a {@link Command}, {@link Option} or {@link Parameters} annotation + */ + public static void usage(Object command, PrintStream out, Help.ColorScheme colorScheme) { + toCommandLine(command).usage(out, colorScheme); + } + + /** + * Delegates to {@link #usage(PrintStream, Help.Ansi)} with the {@linkplain Help.Ansi#AUTO platform default}. + * @param out the printStream to print to + * @see #usage(PrintStream, Help.ColorScheme) + */ + public void usage(PrintStream out) { + usage(out, Help.Ansi.AUTO); + } + + /** + * Delegates to {@link #usage(PrintStream, Help.ColorScheme)} with the {@linkplain Help#defaultColorScheme(CommandLine.Help.Ansi) default color scheme}. + * @param out the printStream to print to + * @param ansi whether the usage message should include ANSI escape codes or not + * @see #usage(PrintStream, Help.ColorScheme) + */ + public void usage(PrintStream out, Help.Ansi ansi) { + usage(out, Help.defaultColorScheme(ansi)); + } + /** + * Prints a usage help message for the annotated command class to the specified {@code PrintStream}. + * Delegates construction of the usage help message to the {@link Help} inner class and is equivalent to: + * <pre> + * Help help = new Help(command).addAllSubcommands(getSubcommands()); + * StringBuilder sb = new StringBuilder() + * .append(help.headerHeading()) + * .append(help.header()) + * .append(help.synopsisHeading()) //e.g. Usage: + * .append(help.synopsis()) //e.g. <main class> [OPTIONS] <command> [COMMAND-OPTIONS] [ARGUMENTS] + * .append(help.descriptionHeading()) //e.g. %nDescription:%n%n + * .append(help.description()) //e.g. {"Converts foos to bars.", "Use options to control conversion mode."} + * .append(help.parameterListHeading()) //e.g. %nPositional parameters:%n%n + * .append(help.parameterList()) //e.g. [FILE...] the files to convert + * .append(help.optionListHeading()) //e.g. %nOptions:%n%n + * .append(help.optionList()) //e.g. -h, --help displays this help and exits + * .append(help.commandListHeading()) //e.g. %nCommands:%n%n + * .append(help.commandList()) //e.g. add adds the frup to the frooble + * .append(help.footerHeading()) + * .append(help.footer()); + * out.print(sb); + * </pre> + * <p>Annotate your class with {@link Command} to control many aspects of the usage help message, including + * the program name, text of section headings and section contents, and some aspects of the auto-generated sections + * of the usage help message. + * <p>To customize the auto-generated sections of the usage help message, like how option details are displayed, + * instantiate a {@link Help} object and use a {@link Help.TextTable} with more of fewer columns, a custom + * {@linkplain Help.Layout layout}, and/or a custom option {@linkplain Help.IOptionRenderer renderer} + * for ultimate control over which aspects of an Option or Field are displayed where.</p> + * @param out the {@code PrintStream} to print the usage help message to + * @param colorScheme the {@code ColorScheme} defining the styles for options, parameters and commands when ANSI is enabled + */ + public void usage(PrintStream out, Help.ColorScheme colorScheme) { + Help help = new Help(interpreter.command, colorScheme).addAllSubcommands(getSubcommands()); + StringBuilder sb = new StringBuilder() + .append(help.headerHeading()) + .append(help.header()) + .append(help.synopsisHeading()) //e.g. Usage: + .append(help.synopsis(help.synopsisHeadingLength())) //e.g. <main class> [OPTIONS] <command> [COMMAND-OPTIONS] [ARGUMENTS] + .append(help.descriptionHeading()) //e.g. %nDescription:%n%n + .append(help.description()) //e.g. {"Converts foos to bars.", "Use options to control conversion mode."} + .append(help.parameterListHeading()) //e.g. %nPositional parameters:%n%n + .append(help.parameterList()) //e.g. [FILE...] the files to convert + .append(help.optionListHeading()) //e.g. %nOptions:%n%n + .append(help.optionList()) //e.g. -h, --help displays this help and exits + .append(help.commandListHeading()) //e.g. %nCommands:%n%n + .append(help.commandList()) //e.g. add adds the frup to the frooble + .append(help.footerHeading()) + .append(help.footer()); + out.print(sb); + } + + /** + * 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) + */ + public void printVersionHelp(PrintStream out) { printVersionHelp(out, Help.Ansi.AUTO); } + + /** + * Prints version information from the {@link Command#version()} annotation to the specified {@code PrintStream}. + * Each element of the array of version strings is printed on a separate line. Version strings 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 + * @see Command#version() + * @see Option#versionHelp() + * @see #isVersionHelpRequested() + */ + public void printVersionHelp(PrintStream out, Help.Ansi ansi) { + for (String versionInfo : versionLines) { + out.println(ansi.new Text(versionInfo)); + } + } + + /** + * 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. + * @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 + */ + public static <R extends Runnable> void run(R command, PrintStream out, String... args) { + run(command, 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); + * try { + * cmd.parse(args); + * } catch (Exception ex) { + * System.err.println(ex.getMessage()); + * cmd.usage(out, ansi); + * return; + * } + * 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. + * @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 + */ + 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(); + } + + /** + * Registers the specified type converter for the specified class. When initializing fields annotated with + * {@link Option}, the field's type is used as a lookup key to find the associated type converter, and this + * type converter converts the original command line argument string value to the correct type. + * <p> + * Java 8 lambdas make it easy to register custom type converters: + * </p> + * <pre> + * commandLine.registerConverter(java.nio.file.Path.class, s -> java.nio.file.Paths.get(s)); + * commandLine.registerConverter(java.time.Duration.class, s -> java.time.Duration.parse(s));</pre> + * <p> + * Built-in type converters are pre-registered for the following java 1.5 types: + * </p> + * <ul> + * <li>all primitive types</li> + * <li>all primitive wrapper types: Boolean, Byte, Character, Double, Float, Integer, Long, Short</li> + * <li>any enum</li> + * <li>java.io.File</li> + * <li>java.math.BigDecimal</li> + * <li>java.math.BigInteger</li> + * <li>java.net.InetAddress</li> + * <li>java.net.URI</li> + * <li>java.net.URL</li> + * <li>java.nio.charset.Charset</li> + * <li>java.sql.Time</li> + * <li>java.util.Date</li> + * <li>java.util.UUID</li> + * <li>java.util.regex.Pattern</li> + * <li>StringBuilder</li> + * <li>CharSequence</li> + * <li>String</li> + * </ul> + * <p>The specified converter will be registered with this {@code CommandLine} and the full hierarchy of its + * subcommands and nested sub-subcommands <em>at the moment the converter is registered</em>. Subcommands added + * later will not have this converter added automatically. To ensure a custom type converter is available to all + * subcommands, register the type converter last, after adding subcommands.</p> + * + * @param cls the target class to convert parameter string values to + * @param converter the class capable of converting string values to the specified target type + * @param <K> the target type + * @return this CommandLine object, to allow method chaining + * @see #addSubcommand(String, Object) + */ + public <K> CommandLine registerConverter(Class<K> cls, ITypeConverter<K> converter) { + interpreter.converterRegistry.put(Assert.notNull(cls, "class"), Assert.notNull(converter, "converter")); + for (CommandLine command : interpreter.commands.values()) { + command.registerConverter(cls, converter); + } + return this; + } + + /** Returns the String that separates option names from option values when parsing command line options. {@code '='} 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) { + interpreter.separator = Assert.notNull(separator, "separator"); + } + 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; } + private static boolean empty(Text txt) { return txt == null || txt.plain.toString().trim().length() == 0; } + 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);} + /** + * <p> + * Annotate fields in your class with {@code @Option} and picocli will initialize these fields when matching + * arguments are specified on the command line. + * </p><p> + * For example: + * </p> + * <pre>import static picocli.CommandLine.*; + * + * public class MyClass { + * @Parameters(type = File.class, description = "Any number of input files") + * private List<File> files = new ArrayList<File>(); + * + * @Option(names = { "-o", "--out" }, description = "Output file (default: print to console)") + * private File outputFile; + * + * @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") + * private boolean help; + * + * @Option(names = { "-V", "--version"}, help = true, description = "Display version information and exit") + * private boolean version; + * } + * </pre> + * <p> + * A field cannot be annotated with both {@code @Parameters} and {@code @Option} or a + * {@code ParameterException} is thrown. + * </p> + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Option { + /** + * One or more option names. At least one option name is required. + * <p> + * Different environments have different conventions for naming options, but usually options have a prefix + * that sets them apart from parameters. + * Picocli supports all of the below styles. The default separator is {@code '='}, but this can be configured. + * </p><p> + * <b>*nix</b> + * </p><p> + * In Unix and Linux, options have a short (single-character) name, a long name or both. + * Short options + * (<a href="http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html#tag_12_02">POSIX + * style</a> are single-character and are preceded by the {@code '-'} character, e.g., {@code `-v'}. + * <a href="https://www.gnu.org/software/tar/manual/html_node/Long-Options.html">GNU-style</a> long + * (or <em>mnemonic</em>) options start with two dashes in a row, e.g., {@code `--file'}. + * </p><p>Picocli supports the POSIX convention that short options can be grouped, with the last option + * optionally taking a parameter, which may be attached to the option name or separated by a space or + * a {@code '='} character. The below examples are all equivalent: + * </p><pre> + * -xvfFILE + * -xvf FILE + * -xvf=FILE + * -xv --file FILE + * -xv --file=FILE + * -x -v --file FILE + * -x -v --file=FILE + * </pre><p> + * <b>DOS</b> + * </p><p> + * DOS options mostly have upper case single-character names and start with a single slash {@code '/'} character. + * Option parameters are separated by a {@code ':'} character. Options cannot be grouped together but + * must be specified separately. For example: + * </p><pre> + * DIR /S /A:D /T:C + * </pre><p> + * <b>PowerShell</b> + * </p><p> + * Windows PowerShell options generally are a word preceded by a single {@code '-'} character, e.g., {@code `-Help'}. + * Option parameters are separated by a space or by a {@code ':'} character. + * </p> + * @return one or more option names + */ + String[] names(); + + /** + * Indicates whether this option is required. By default this is false. + * If an option is required, but a user invokes the program without specifying the required option, + * a {@link MissingParameterException} is thrown from the {@link #parse(String...)} method. + * @return whether this option is required + */ + boolean required() default false; + + /** + * Set {@code help=true} if this option should disable validation of the remaining arguments: + * If the {@code help} option is specified, no error message is generated for missing required options. + * <p> + * This attribute is useful for special options like help ({@code -h} and {@code --help} on unix, + * {@code -?} and {@code -Help} on Windows) or version ({@code -V} and {@code --version} on unix, + * {@code -Version} on Windows). + * </p> + * <p> + * Note that the {@link #parse(String...)} method will not print help documentation. It will only set + * the value of the annotated field. It is the responsibility of the caller to inspect the annotated fields + * and take the appropriate action. + * </p> + * @return whether this option disables validation of the other arguments + */ + boolean help() default false; + + /** + * Set {@code usageHelp=true} if this option allows the user to request usage help. If this option is + * specified on the command line, picocli will not validate the remaining arguments (so no "missing required + * option" errors) and the {@link CommandLine#isUsageHelpRequested()} method will return {@code true}. + * <p> + * This attribute is useful for special options like help ({@code -h} and {@code --help} on unix, + * {@code -?} and {@code -Help} on Windows). + * </p> + * <p> + * Note that the {@link #parse(String...)} method will not print usage help documentation. It will only set + * the value of the annotated field. It is the responsibility of the caller to inspect the annotated fields + * and take the appropriate action. + * </p> + * @return whether this option allows the user to request usage help + */ + boolean usageHelp() default false; + + /** + * Set {@code versionHelp=true} if this option allows the user to request version information. If this option is + * specified on the command line, picocli will not validate the remaining arguments (so no "missing required + * option" errors) and the {@link CommandLine#isVersionHelpRequested()} method will return {@code true}. + * <p> + * This attribute is useful for special options like version ({@code -V} and {@code --version} on unix, + * {@code -Version} on Windows). + * </p> + * <p> + * Note that the {@link #parse(String...)} method will not print version information. It will only set + * the value of the annotated field. It is the responsibility of the caller to inspect the annotated fields + * and take the appropriate action. + * </p> + * @return whether this option allows the user to request version information + */ + boolean versionHelp() default false; + + /** + * Description of this option, used when generating the usage documentation. + * @return the description of this option + */ + String[] description() default {}; + + /** + * Specifies the minimum number of required parameters and the maximum number of accepted parameters. + * If an option declares a positive arity, and the user specifies an insufficient number of parameters on the + * command line, a {@link MissingParameterException} is thrown by the {@link #parse(String...)} method. + * <p> + * In many cases picocli can deduce the number of required parameters from the field's type. + * By default, flags (boolean options) have arity zero, + * and single-valued type fields (String, int, Integer, double, Double, File, Date, etc) have arity one. + * Generally, fields with types that cannot hold multiple values can omit the {@code arity} attribute. + * </p><p> + * Fields used to capture options with arity two or higher should have a type that can hold multiple values, + * like arrays or Collections. See {@link #type()} for strongly-typed Collection fields. + * </p><p> + * For example, if an option has 2 required parameters and any number of optional parameters, + * specify {@code @Option(names = "-example", arity = "2..*")}. + * </p> + * <b>A note on boolean options</b> + * <p> + * By default picocli does not expect boolean options (also called "flags" or "switches") to have a parameter. + * You can make a boolean option take a required parameter by annotating your field with {@code arity="1"}. + * For example: </p> + * <pre>@Option(names = "-v", arity = "1") boolean verbose;</pre> + * <p> + * Because this boolean field is defined with arity 1, the user must specify either {@code <program> -v false} + * or {@code <program> -v true} + * on the command line, or a {@link MissingParameterException} is thrown by the {@link #parse(String...)} + * method. + * </p><p> + * To make the boolean parameter possible but optional, define the field with {@code arity = "0..1"}. + * For example: </p> + * <pre>@Option(names="-v", arity="0..1") boolean verbose;</pre> + * <p>This will accept any of the below without throwing an exception:</p> + * <pre> + * -v + * -v true + * -v false + * </pre> + * @return how many arguments this option requires + */ + String arity() default ""; + + /** + * Specify a {@code paramLabel} for the option parameter to be used in the usage help message. If omitted, + * picocli uses the field name in fish brackets ({@code '<'} and {@code '>'}) by default. Example: + * <pre>class Example { + * @Option(names = {"-o", "--output"}, paramLabel="FILE", description="path of the output file") + * private File out; + * @Option(names = {"-j", "--jobs"}, arity="0..1", description="Allow N jobs at once; infinite jobs with no arg.") + * private int maxJobs = -1; + * }</pre> + * <p>By default, the above gives a usage help message like the following:</p><pre> + * Usage: <main class> [OPTIONS] + * -o, --output FILE path of the output file + * -j, --jobs [<maxJobs>] Allow N jobs at once; infinite jobs with no arg. + * </pre> + * @return name of the option parameter used in the usage help message + */ + String paramLabel() default ""; + + /** + * <p> + * Specify a {@code type} if the annotated field is a {@code Collection} that should hold objects other than Strings. + * </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. + * </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 + */ + Class<?> type() default String.class; + + /** + * Specify a regular expression to use to split option parameter values before applying them to the field. + * All elements resulting from the split are added to the array or Collection. Ignored for single-value fields. + * @return a regular expression to split option parameter values or {@code ""} if the value should not be split + * @see String#split(String) + */ + String split() default ""; + + /** + * Set {@code hidden=true} if this option should not be included in the usage documentation. + * @return whether this option should be excluded from the usage message + */ + boolean hidden() default false; + } + /** + * <p> + * Fields annotated with {@code @Parameters} will be initialized with positional parameters. By specifying the + * {@link #index()} attribute you can pick which (or what range) of the positional parameters to apply. If no index + * is specified, the field will get all positional parameters (so it should be an array or a collection). + * </p><p> + * When parsing the command line arguments, picocli first tries to match arguments to {@link Option Options}. + * Positional parameters are the arguments that follow the options, or the arguments that follow a "--" (double + * dash) argument on the command line. + * </p><p> + * For example: + * </p> + * <pre>import static picocli.CommandLine.*; + * + * public class MyCalcParameters { + * @Parameters(type = BigDecimal.class, description = "Any number of input numbers") + * private List<BigDecimal> files = new ArrayList<BigDecimal>(); + * + * @Option(names = { "-h", "--help", "-?", "-help"}, help = true, description = "Display this help and exit") + * private boolean help; + * } + * </pre><p> + * A field cannot be annotated with both {@code @Parameters} and {@code @Option} or a {@code ParameterException} + * is thrown.</p> + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface Parameters { + /** Specify an index ("0", or "1", etc.) to pick which of the command line arguments should be assigned to this + * field. For array or Collection fields, you can also specify an index range ("0..3", or "2..*", etc.) to assign + * a subset of the command line arguments to this field. The default is "*", meaning all command line arguments. + * @return an index or range specifying which of the command line arguments should be assigned to this field + */ + String index() default "*"; + + /** Description of the parameter(s), used when generating the usage documentation. + * @return the description of the parameter(s) + */ + String[] description() default {}; + + /** + * Specifies the minimum number of required parameters and the maximum number of accepted parameters. If a + * positive arity is declared, and the user specifies an insufficient number of parameters on the command line, + * {@link MissingParameterException} is thrown by the {@link #parse(String...)} method. + * <p>The default depends on the type of the parameter: booleans require no parameters, arrays and Collections + * accept zero to any number of parameters, and any other type accepts one parameter.</p> + * @return the range of minimum and maximum parameters accepted by this command + */ + String arity() default ""; + + /** + * Specify a {@code paramLabel} for the parameter to be used in the usage help message. If omitted, + * picocli uses the field name in fish brackets ({@code '<'} and {@code '>'}) by default. Example: + * <pre>class Example { + * @Parameters(paramLabel="FILE", description="path of the input FILE(s)") + * private File[] inputFiles; + * }</pre> + * <p>By default, the above gives a usage help message like the following:</p><pre> + * Usage: <main class> [FILE...] + * [FILE...] path of the input FILE(s) + * </pre> + * @return name of the positional parameter used in the usage help message + */ + String paramLabel() default ""; + + /** + * <p> + * Specify a {@code type} if the annotated field is a {@code Collection} that should hold objects other than Strings. + * </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. + * </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 + */ + Class<?> type() default String.class; + + /** + * Specify a regular expression to use to split positional parameter values before applying them to the field. + * All elements resulting from the split are added to the array or Collection. Ignored for single-value fields. + * @return a regular expression to split operand values or {@code ""} if the value should not be split + * @see String#split(String) + */ + String split() default ""; + + /** + * Set {@code hidden=true} if this parameter should not be included in the usage message. + * @return whether this parameter should be excluded from the usage message + */ + boolean hidden() default false; + } + + /** + * <p>Annotate your class with {@code @Command} when you want more control over the format of the generated help + * message. + * </p><pre> + * @Command(name = "Encrypt", + * description = "Encrypt FILE(s), or standard input, to standard output or to the output file.", + * footer = "Copyright (c) 2017") + * public class Encrypt { + * @Parameters(paramLabel = "FILE", type = File.class, description = "Any number of input files") + * private List<File> files = new ArrayList<File>(); + * + * @Option(names = { "-o", "--out" }, description = "Output file (default: print to console)") + * private File outputFile; + * }</pre> + * <p> + * The structure of a help message looks like this: + * </p><ul> + * <li>[header]</li> + * <li>[synopsis]: {@code Usage: <commandName> [OPTIONS] [FILE...]}</li> + * <li>[description]</li> + * <li>[parameter list]: {@code [FILE...] Any number of input files}</li> + * <li>[option list]: {@code -h, --help prints this help message and exits}</li> + * <li>[footer]</li> + * </ul> */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + 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 + * by the parser to recognize subcommands in the command line arguments. + * @return the program name to show in the synopsis + * @see Help#commandName */ + String name() default "<main class>"; + + /** A list of classes to instantiate and register as subcommands. When registering subcommands declaratively + * like this, you don't need to call the {@link CommandLine#addSubcommand(String, Object)} method. For example, this: + * <pre> + * @Command(subcommands = { + * GitStatus.class, + * GitCommit.class, + * GitBranch.class }) + * public class Git { ... } + * + * CommandLine commandLine = new CommandLine(new Git()); + * </pre> is equivalent to this: + * <pre> + * // alternative: programmatically add subcommands. + * // NOTE: in this case there should be no `subcommands` attribute on the @Command annotation. + * @Command public class Git { ... } + * + * CommandLine commandLine = new CommandLine(new Git()) + * .addSubcommand("status", new GitStatus()) + * .addSubcommand("commit", new GitCommit()) + * .addSubcommand("branch", new GitBranch()); + * </pre> + * @return the declaratively registered subcommands of this command, or an empty array if none + * @see CommandLine#addSubcommand(String, Object) + * @since 0.9.8 + */ + Class<?>[] subcommands() default {}; + + /** String that separates options from option parameters. Default is {@code "="}. Spaces are also accepted. + * @return the string that separates options from option parameters, used both when parsing and when generating usage help + * @see Help#separator + * @see CommandLine#setSeparator(String) */ + String separator() default "="; + + /** Version information for this command, to print to the console when the user specifies an + * {@linkplain Option#versionHelp() option} to request version help. This is not part of the usage help message. + * + * @return a string or an array of strings with version information about this command. + * @since 0.9.8 + * @see CommandLine#printVersionHelp(PrintStream) + */ + String[] version() default {}; + + /** Set the heading preceding the header section. May contain embedded {@linkplain java.util.Formatter format specifiers}. + * @return the heading preceding the header section + * @see Help#headerHeading(Object...) */ + String headerHeading() default ""; + + /** Optional summary description of the command, shown before the synopsis. + * @return summary description of the command + * @see Help#header + * @see Help#header(Object...) */ + String[] header() default {}; + + /** Set the heading preceding the synopsis text. May contain embedded + * {@linkplain java.util.Formatter format specifiers}. The default heading is {@code "Usage: "} (without a line + * break between the heading and the synopsis text). + * @return the heading preceding the synopsis text + * @see Help#synopsisHeading(Object...) */ + String synopsisHeading() default "Usage: "; + + /** Specify {@code true} to generate an abbreviated synopsis like {@code "<main> [OPTIONS] [PARAMETERS...]"}. + * By default, a detailed synopsis with individual option names and parameters is generated. + * @return whether the synopsis should be abbreviated + * @see Help#abbreviateSynopsis + * @see Help#abbreviatedSynopsis() + * @see Help#detailedSynopsis(Comparator, boolean) */ + boolean abbreviateSynopsis() default false; + + /** Specify one or more custom synopsis lines to display instead of an auto-generated synopsis. + * @return custom synopsis text to replace the auto-generated synopsis + * @see Help#customSynopsis + * @see Help#customSynopsis(Object...) */ + String[] customSynopsis() default {}; + + /** Set the heading preceding the description section. May contain embedded {@linkplain java.util.Formatter format specifiers}. + * @return the heading preceding the description section + * @see Help#descriptionHeading(Object...) */ + String descriptionHeading() default ""; + + /** Optional text to display between the synopsis line(s) and the list of options. + * @return description of this command + * @see Help#description + * @see Help#description(Object...) */ + String[] description() default {}; + + /** Set the heading preceding the parameters list. May contain embedded {@linkplain java.util.Formatter format specifiers}. + * @return the heading preceding the parameters list + * @see Help#parameterListHeading(Object...) */ + String parameterListHeading() default ""; + + /** Set the heading preceding the options list. May contain embedded {@linkplain java.util.Formatter format specifiers}. + * @return the heading preceding the options list + * @see Help#optionListHeading(Object...) */ + String optionListHeading() default ""; + + /** Specify {@code false} to show Options in declaration order. The default is to sort alphabetically. + * @return whether options should be shown in alphabetic order. + * @see Help#sortOptions */ + boolean sortOptions() default true; + + /** Prefix required options with this character in the options list. The default is no marker: the synopsis + * indicates which options and parameters are required. + * @return the character to show in the options list to mark required options + * @see Help#requiredOptionMarker */ + char requiredOptionMarker() default ' '; + + /** Specify {@code true} to show default values in the description column of the options list (except for + * boolean options). False by default. + * @return whether the default values for options and parameters should be shown in the description column + * @see Help#showDefaultValues */ + boolean showDefaultValues() default false; + + /** Set the heading preceding the subcommands list. May contain embedded {@linkplain java.util.Formatter format specifiers}. + * The default heading is {@code "Commands:%n"} (with a line break at the end). + * @return the heading preceding the subcommands list + * @see Help#commandListHeading(Object...) */ + String commandListHeading() default "Commands:%n"; + + /** Set the heading preceding the footer section. May contain embedded {@linkplain java.util.Formatter format specifiers}. + * @return the heading preceding the footer section + * @see Help#footerHeading(Object...) */ + String footerHeading() default ""; + + /** Optional text to display after the list of options. + * @return text to display after the list of options + * @see Help#footer + * @see Help#footer(Object...) */ + String[] footer() default {}; + } + /** + * <p> + * When parsing command line arguments and initializing + * fields annotated with {@link Option @Option} or {@link Parameters @Parameters}, + * String values can be converted to any type for which a {@code ITypeConverter} is registered. + * </p><p> + * This interface defines the contract for classes that know how to convert a String into some domain object. + * Custom converters can be registered with the {@link #registerConverter(Class, ITypeConverter)} method. + * </p><p> + * Java 8 lambdas make it easy to register custom type converters: + * </p> + * <pre> + * commandLine.registerConverter(java.nio.file.Path.class, s -> java.nio.file.Paths.get(s)); + * commandLine.registerConverter(java.time.Duration.class, s -> java.time.Duration.parse(s));</pre> + * <p> + * Built-in type converters are pre-registered for the following java 1.5 types: + * </p> + * <ul> + * <li>all primitive types</li> + * <li>all primitive wrapper types: Boolean, Byte, Character, Double, Float, Integer, Long, Short</li> + * <li>any enum</li> + * <li>java.io.File</li> + * <li>java.math.BigDecimal</li> + * <li>java.math.BigInteger</li> + * <li>java.net.InetAddress</li> + * <li>java.net.URI</li> + * <li>java.net.URL</li> + * <li>java.nio.charset.Charset</li> + * <li>java.sql.Time</li> + * <li>java.util.Date</li> + * <li>java.util.UUID</li> + * <li>java.util.regex.Pattern</li> + * <li>StringBuilder</li> + * <li>CharSequence</li> + * <li>String</li> + * </ul> + * @param <K> the type of the object that is the result of the conversion + */ + public interface ITypeConverter<K> { + /** + * Converts the specified command line argument value to some domain object. + * @param value the command line argument String value + * @return the resulting domain object + * @throws Exception an exception detailing what went wrong during the conversion + */ + K convert(String value) throws Exception; + } + /** Describes the number of parameters required and accepted by an option or a positional parameter. + * @since 0.9.7 + */ + public static class Range implements Comparable<Range> { + /** Required number of parameters for an option or positional parameter. */ + public final int min; + /** Maximum accepted number of parameters for an option or positional parameter. */ + public final int max; + public final boolean isVariable; + private final boolean isUnspecified; + private final String originalValue; + + /** Constructs a new Range object with the specified parameters. + * @param min minimum number of required parameters + * @param max maximum number of allowed parameters (or Integer.MAX_VALUE if variable) + * @param variable {@code true} if any number or parameters is allowed, {@code false} otherwise + * @param unspecified {@code true} if no arity was specified on the option/parameter (value is based on type) + * @param originalValue the original value that was specified on the option or parameter + */ + public Range(int min, int max, boolean variable, boolean unspecified, String originalValue) { + this.min = min; + this.max = max; + this.isVariable = variable; + this.isUnspecified = unspecified; + this.originalValue = originalValue; + } + /** Returns a new {@code Range} based on the {@link Option#arity()} annotation on the specified field, + * or the field type's default arity if no arity was specified. + * @param field the field whose Option annotation to inspect + * @return a new {@code Range} based on the Option arity annotation on the specified field */ + public static Range optionArity(Field field) { + return field.isAnnotationPresent(Option.class) + ? adjustForType(Range.valueOf(field.getAnnotation(Option.class).arity()), field) + : new Range(0, 0, false, true, "0"); + } + /** Returns a new {@code Range} based on the {@link Parameters#arity()} annotation on the specified field, + * or the field type's default arity if no arity was specified. + * @param field the field whose Parameters annotation to inspect + * @return a new {@code Range} based on the Parameters arity annotation on the specified field */ + public static Range parameterArity(Field field) { + return field.isAnnotationPresent(Parameters.class) + ? adjustForType(Range.valueOf(field.getAnnotation(Parameters.class).arity()), field) + : new Range(0, 0, false, true, "0"); + } + /** Returns a new {@code Range} based on the {@link Parameters#index()} annotation on the specified field. + * @param field the field whose Parameters annotation to inspect + * @return a new {@code Range} based on the Parameters index annotation on the specified field */ + public static Range parameterIndex(Field field) { + return field.isAnnotationPresent(Parameters.class) + ? Range.valueOf(field.getAnnotation(Parameters.class).index()) + : new Range(0, 0, false, true, "0"); + } + static Range adjustForType(Range result, Field field) { + return result.isUnspecified ? defaultArity(field.getType()) : result; + } + /** Returns a new {@code Range} based on the specified type: booleans have arity 0, arrays or Collections have + * arity "0..*", and 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 + } + /** 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 + * {@code MIN_VALUE} string is not numeric, the minimum is zero. If the {@code MAX_VALUE} is not numeric, the + * range is taken to be variable and the maximum is {@code Integer.MAX_VALUE}. + * @param range the value range string to parse + * @return a new {@code Range} value */ + public static Range valueOf(String range) { + range = range.trim(); + boolean unspecified = range.length() == 0 || range.startsWith(".."); // || range.endsWith(".."); + int min = -1, max = -1; + boolean variable = false; + int dots = -1; + if ((dots = range.indexOf("..")) >= 0) { + min = parseInt(range.substring(0, dots), 0); + max = parseInt(range.substring(dots + 2), Integer.MAX_VALUE); + variable = max == Integer.MAX_VALUE; + } else { + max = parseInt(range, Integer.MAX_VALUE); + variable = max == Integer.MAX_VALUE; + min = variable ? 0 : max; + } + Range result = new Range(min, max, variable, unspecified, range); + return result; + } + private static int parseInt(String str, int defaultValue) { + try { + return Integer.parseInt(str); + } catch (Exception ex) { + return defaultValue; + } + } + /** Returns a new Range object with the {@code min} value replaced by the specified value. + * The {@code max} of the returned Range is guaranteed not to be less than the new {@code min} value. + * @param newMin the {@code min} value of the returned Range object + * @return a new Range object with the specified {@code min} value */ + public Range min(int newMin) { return new Range(newMin, Math.max(newMin, max), isVariable, isUnspecified, originalValue); } + + /** Returns a new Range object with the {@code max} value replaced by the specified value. + * The {@code min} of the returned Range is guaranteed not to be greater than the new {@code max} value. + * @param newMax the {@code max} value of the returned Range object + * @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); } + + public boolean equals(Object object) { + if (!(object instanceof Range)) { return false; } + Range other = (Range) object; + return other.max == this.max && other.min == this.min && other.isVariable == this.isVariable; + } + public int hashCode() { + return ((17 * 37 + max) * 37 + min) * 37 + (isVariable ? 1 : 0); + } + public String toString() { + return min == max ? String.valueOf(min) : min + ".." + (isVariable ? "*" : max); + } + public int compareTo(Range other) { + int result = min - other.min; + 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) { + Field[] declaredFields = cls.getDeclaredFields(); + for (Field field : declaredFields) { + field.setAccessible(true); + if (field.isAnnotationPresent(Option.class)) { + Option option = field.getAnnotation(Option.class); + if (option.required()) { + requiredFields.add(field); + } + for (String name : option.names()) { // cannot be null or empty + Field existing = optionName2Field.put(name, field); + if (existing != null && existing != field) { + throw DuplicateOptionAnnotationsException.create(name, field, existing); + } + if (name.length() == 2 && name.startsWith("-")) { + char flag = name.charAt(1); + Field existing2 = singleCharOption2Field.put(flag, field); + if (existing2 != null && existing2 != field) { + throw DuplicateOptionAnnotationsException.create(name, field, existing2); + } + } + } + } + if (field.isAnnotationPresent(Parameters.class)) { + if (field.isAnnotationPresent(Option.class)) { + throw new ParameterException("A field can be either @Option or @Parameters, but '" + + field.getName() + "' is both."); + } + positionalParametersFields.add(field); + Range arity = Range.parameterArity(field); + if (arity.min > 0) { + requiredFields.add(field); + } + } + } + } + static void validatePositionalParameters(List<Field> positionalParametersFields) { + int min = 0; + for (Field field : positionalParametersFields) { + Range index = Range.parameterIndex(field); + if (index.min > min) { + throw new ParameterIndexGapException("Missing field annotated with @Parameter(index=" + min + + "). Nearest field '" + field.getName() + "' has index=" + index.min); + } + min = Math.max(min, index.max); + min = min == Integer.MAX_VALUE ? min : min + 1; + } + } + private static <T> Stack<T> reverse(Stack<T> stack) { + Collections.reverse(stack); + return stack; + } + /** + * Helper class responsible for processing command line arguments. + */ + private class Interpreter { + private final Map<String, CommandLine> commands = new LinkedHashMap<String, CommandLine>(); + private final Map<Class<?>, ITypeConverter<?>> converterRegistry = new HashMap<Class<?>, ITypeConverter<?>>(); + private final Map<String, Field> optionName2Field = new HashMap<String, Field>(); + private final Map<Character, Field> singleCharOption2Field = new HashMap<Character, Field>(); + private final List<Field> requiredFields = new ArrayList<Field>(); + private final List<Field> positionalParametersFields = new ArrayList<Field>(); + private final Object command; + private boolean isHelpRequested; + private String separator = "="; + + Interpreter(Object command) { + converterRegistry.put(Path.class, new BuiltIn.PathConverter()); + converterRegistry.put(String.class, new BuiltIn.StringConverter()); + converterRegistry.put(StringBuilder.class, new BuiltIn.StringBuilderConverter()); + converterRegistry.put(CharSequence.class, new BuiltIn.CharSequenceConverter()); + converterRegistry.put(Byte.class, new BuiltIn.ByteConverter()); + converterRegistry.put(Byte.TYPE, new BuiltIn.ByteConverter()); + converterRegistry.put(Boolean.class, new BuiltIn.BooleanConverter()); + converterRegistry.put(Boolean.TYPE, new BuiltIn.BooleanConverter()); + converterRegistry.put(Character.class, new BuiltIn.CharacterConverter()); + converterRegistry.put(Character.TYPE, new BuiltIn.CharacterConverter()); + converterRegistry.put(Short.class, new BuiltIn.ShortConverter()); + converterRegistry.put(Short.TYPE, new BuiltIn.ShortConverter()); + converterRegistry.put(Integer.class, new BuiltIn.IntegerConverter()); + converterRegistry.put(Integer.TYPE, new BuiltIn.IntegerConverter()); + converterRegistry.put(Long.class, new BuiltIn.LongConverter()); + converterRegistry.put(Long.TYPE, new BuiltIn.LongConverter()); + converterRegistry.put(Float.class, new BuiltIn.FloatConverter()); + converterRegistry.put(Float.TYPE, new BuiltIn.FloatConverter()); + converterRegistry.put(Double.class, new BuiltIn.DoubleConverter()); + converterRegistry.put(Double.TYPE, new BuiltIn.DoubleConverter()); + converterRegistry.put(File.class, new BuiltIn.FileConverter()); + converterRegistry.put(URI.class, new BuiltIn.URIConverter()); + converterRegistry.put(URL.class, new BuiltIn.URLConverter()); + converterRegistry.put(Date.class, new BuiltIn.ISO8601DateConverter()); + converterRegistry.put(Time.class, new BuiltIn.ISO8601TimeConverter()); + converterRegistry.put(BigDecimal.class, new BuiltIn.BigDecimalConverter()); + converterRegistry.put(BigInteger.class, new BuiltIn.BigIntegerConverter()); + converterRegistry.put(Charset.class, new BuiltIn.CharsetConverter()); + converterRegistry.put(InetAddress.class, new BuiltIn.InetAddressConverter()); + 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; + boolean hasCommandAnnotation = false; + while (cls != null) { + init(cls, requiredFields, optionName2Field, singleCharOption2Field, positionalParametersFields); + if (cls.isAnnotationPresent(Command.class)) { + hasCommandAnnotation = true; + Command cmd = cls.getAnnotation(Command.class); + declaredSeparator = (declaredSeparator == null) ? cmd.separator() : declaredSeparator; + 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() + + " is missing the mandatory @Command annotation with a 'name' attribute"); + } + try { + Constructor<?> constructor = sub.getDeclaredConstructor(); + constructor.setAccessible(true); + CommandLine commandLine = toCommandLine(constructor.newInstance()); + commandLine.parent = CommandLine.this; + commands.put(subCommand.name(), commandLine); + } + catch (IllegalArgumentException ex) { throw ex; } + catch (NoSuchMethodException ex) { throw new IllegalArgumentException("Cannot instantiate subcommand " + + sub.getName() + ": the class has no constructor", ex); } + catch (Exception ex) { + throw new IllegalStateException("Could not instantiate and add subcommand " + + sub.getName() + ": " + ex, ex); + } + } + } + cls = cls.getSuperclass(); + } + separator = declaredSeparator != null ? declaredSeparator : separator; + Collections.sort(positionalParametersFields, new PositionalParametersSorter()); + validatePositionalParameters(positionalParametersFields); + + if (positionalParametersFields.isEmpty() && optionName2Field.isEmpty() && !hasCommandAnnotation) { + throw new IllegalArgumentException(command + " (" + command.getClass() + + ") is not a command: it has no @Command, @Option or @Parameters annotations"); + } + } + + /** + * Entry point into parsing command line arguments. + * @param args the command line arguments + * @return a list with all commands and subcommands initialized by this method + * @throws ParameterException if the specified command line arguments are invalid + */ + List<CommandLine> parse(String... args) { + Assert.notNull(args, "argument array"); + Stack<String> arguments = new Stack<String>(); + for (int i = args.length - 1; i >= 0; i--) { + arguments.push(args[i]); + } + List<CommandLine> result = new ArrayList<CommandLine>(); + parse(result, arguments, args); + return result; + } + + private void parse(List<CommandLine> parsedCommands, Stack<String> argumentStack, String[] originalArgs) { + // first reset any state in case this CommandLine instance is being reused + isHelpRequested = false; + CommandLine.this.versionHelpRequested = false; + CommandLine.this.usageHelpRequested = false; + + parsedCommands.add(CommandLine.this); + List<Field> required = new ArrayList<Field>(requiredFields); + Set<Field> initialized = new HashSet<Field>(); + Collections.sort(required, new PositionalParametersSorter()); + try { + processArguments(parsedCommands, argumentStack, required, initialized, originalArgs); + } catch (ParameterException ex) { + throw ex; + } catch (Exception ex) { + int offendingArgIndex = originalArgs.length - argumentStack.size(); + String arg = offendingArgIndex >= 0 && offendingArgIndex < originalArgs.length ? originalArgs[offendingArgIndex] : "?"; + throw ParameterException.create(ex, arg, argumentStack.size(), 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); } + } + } + } + + private void processArguments(List<CommandLine> parsedCommands, + Stack<String> args, + Collection<Field> required, + Set<Field> initialized, + String[] originalArgs) throws Exception { + // arg must be one of: + // 1. the "--" double dash separating options from positional arguments + // 1. a stand-alone flag, like "-v" or "--verbose": no value required, must map to boolean or Boolean field + // 2. a short option followed by an argument, like "-f file" or "-ffile": may map to any type of field + // 3. a long option followed by an argument, like "-file out.txt" or "-file=out.txt" + // 3. one or more remaining arguments without any associated options. Must be the last in the list. + // 4. a combination of stand-alone options, like "-vxr". Equivalent to "-v -x -r", "-v true -x true -r true" + // 5. a combination of stand-alone options and one option with an argument, like "-vxrffile" + + while (!args.isEmpty()) { + String arg = args.pop(); + + // Double-dash separates options from positional arguments. + // If found, then interpret the remaining args as positional parameters. + if ("--".equals(arg)) { + processPositionalParameters(required, 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); + } + commands.get(arg).interpreter.parse(parsedCommands, args, originalArgs); + return; // remainder done by the command + } + + // First try to interpret the argument as a single option (as opposed to a compact group of options). + // A single option may be without option parameters, like "-v" or "--verbose" (a boolean value), + // or an option may have one or more option parameters. + // A parameter may be attached to the option. + boolean paramAttachedToOption = false; + int separatorIndex = arg.indexOf(separator); + if (separatorIndex > 0) { + String key = arg.substring(0, separatorIndex); + // be greedy. Consume the whole arg as an option if possible. + if (optionName2Field.containsKey(key) && !optionName2Field.containsKey(arg)) { + paramAttachedToOption = true; + String optionParam = arg.substring(separatorIndex + separator.length()); + args.push(optionParam); + arg = key; + } + } + if (optionName2Field.containsKey(arg)) { + processStandaloneOption(required, initialized, arg, args, paramAttachedToOption); + } + // 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("-")) { + 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; + } + } + } + + private void processPositionalParameters(Collection<Field> required, Stack<String> args) throws Exception { + processPositionalParameters0(required, false, args); + if (!args.empty()) { + handleUnmatchedArguments(args); + return; + }; + } + + 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; + 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); + } + } + Collections.reverse(argsCopy); + for (int i = 0; i < indexRange.min && !argsCopy.isEmpty(); i++) { argsCopy.pop(); } + Range arity = Range.parameterArity(positionalParam); + assertNoMissingParameters(positionalParam, arity.min, argsCopy); + if (!validateOnly) { + applyOption(positionalParam, Parameters.class, arity, false, argsCopy, null); + required.remove(positionalParam); + } + } + // 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(); } + } + } + + private void processStandaloneOption(Collection<Field> required, + Set<Field> initialized, + String arg, + Stack<String> args, + boolean paramAttachedToKey) throws Exception { + Field field = optionName2Field.get(arg); + required.remove(field); + Range arity = Range.optionArity(field); + 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); + } + + private void processClusteredShortOptions(Collection<Field> required, + Set<Field> initialized, + String arg, + Stack<String> args) + throws Exception { + String prefix = arg.substring(0, 1); + String cluster = arg.substring(1); + boolean paramAttachedToOption = true; + do { + if (cluster.length() > 0 && singleCharOption2Field.containsKey(cluster.charAt(0))) { + Field field = singleCharOption2Field.get(cluster.charAt(0)); + 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 + } + 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); + // only return if cluster (and maybe more) was consumed, otherwise continue do-while loop + if (consumed > 0) { + return; + } + cluster = args.pop(); + } else { // cluster is empty || cluster.charAt(0) is not a short option key + if (cluster.length() == 0) { // we finished parsing a group of short options like -rxv + return; // return normally and parse the next arg + } + // 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); + } + args.push(cluster); + processPositionalParameters(required, args); + return; + } + } while (true); + } + + private int applyOption(Field field, + Class<?> annotation, + Range arity, + boolean valueAttachedToOption, + Stack<String> args, + Set<Field> initialized) throws Exception { + updateHelpRequested(field); + if (!args.isEmpty() && args.peek().length(
<TRUNCATED>
