org.apache.logging.log4j.core.tools.picocli.CommandLine Maven / Gradle / Ivy
Show all versions of log4j-core Show documentation
/*
* 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;
/**
*
* CommandLine interpreter that uses reflection to initialize an annotated domain object with values obtained from the
* command line arguments.
*
Example
* 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;
* }
*
*
* Use {@code CommandLine} to initialize a domain object as follows:
*
* 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);
* }
* }
*
* Invoke the above program with some command line arguments. The below are all equivalent:
*
*
* --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
*
*
*
* Copied and modified from picocli.
*
*
* @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 unmatchedArguments = new ArrayList();
private CommandLine parent;
private boolean usageHelpRequested;
private boolean versionHelpRequested;
private List versionLines = new ArrayList();
/**
* 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:
*
* 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())
* //...
* ;
*
*
* The specified object can be an annotated object or a
* {@code CommandLine} instance with its own nested subcommands. For example:
*
* 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())
* )
* );
*
* 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.
*
* @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 getSubcommands() {
return new LinkedHashMap(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.
* The specified setting will be registered with this {@code CommandLine} and the full hierarchy of its
* subcommands and nested sub-subcommands at the moment this method is called. Subcommands added
* later will have the default setting. To ensure a setting is applied to all
* subcommands, call the setter last, after adding subcommands.
* @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.
* The specified setting will be registered with this {@code CommandLine} and the full hierarchy of its
* subcommands and nested sub-subcommands at the moment this method is called. Subcommands added
* later will have the default setting. To ensure a setting is applied to all
* subcommands, call the setter last, after adding subcommands.
* @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 getUnmatchedArguments() {
return unmatchedArguments;
}
/**
*
* Convenience method that initializes the specified annotated object from the specified command line arguments.
*
* This is equivalent to
*
* CommandLine cli = new CommandLine(command);
* cli.parse(args);
* return command;
*
*
* @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 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 populateCommand(T command, String... args) {
CommandLine cli = toCommandLine(command);
cli.parse(args);
return command;
}
/**
*
* 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.
*
*
* @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 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:
*
* 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);
*
* 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.
*
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.
* @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
* markup for colors and style.
* @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 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 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:
*
* CommandLine cmd = new CommandLine(command);
* try {
* cmd.parse(args);
* } catch (Exception ex) {
* System.err.println(ex.getMessage());
* cmd.usage(out, ansi);
* return;
* }
* command.run();
*
* 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 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 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.
*
* Java 8 lambdas make it easy to register custom type converters:
*
*
* 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));
*
* Built-in type converters are pre-registered for the following java 1.5 types:
*
*
* - all primitive types
* - all primitive wrapper types: Boolean, Byte, Character, Double, Float, Integer, Long, Short
* - any enum
* - java.io.File
* - java.math.BigDecimal
* - java.math.BigInteger
* - java.net.InetAddress
* - java.net.URI
* - java.net.URL
* - java.nio.charset.Charset
* - java.sql.Time
* - java.util.Date
* - java.util.UUID
* - java.util.regex.Pattern
* - StringBuilder
* - CharSequence
* - String
*
* The specified converter will be registered with this {@code CommandLine} and the full hierarchy of its
* subcommands and nested sub-subcommands at the moment the converter is registered. 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.
*
* @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 the target type
* @return this CommandLine object, to allow method chaining
* @see #addSubcommand(String, Object)
*/
public CommandLine registerConverter(Class cls, ITypeConverter 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);}
/**
*
* Annotate fields in your class with {@code @Option} and picocli will initialize these fields when matching
* arguments are specified on the command line.
*
* For example:
*
* 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;
* }
*
*
* A field cannot be annotated with both {@code @Parameters} and {@code @Option} or a
* {@code ParameterException} is thrown.
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Option {
/**
* One or more option names. At least one option name is required.
*
* 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.
*
* *nix
*
* In Unix and Linux, options have a short (single-character) name, a long name or both.
* Short options
* (POSIX
* style are single-character and are preceded by the {@code '-'} character, e.g., {@code `-v'}.
* GNU-style long
* (or mnemonic) options start with two dashes in a row, e.g., {@code `--file'}.
*
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:
*
* -xvfFILE
* -xvf FILE
* -xvf=FILE
* -xv --file FILE
* -xv --file=FILE
* -x -v --file FILE
* -x -v --file=FILE
*
* DOS
*
* 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:
*
* DIR /S /A:D /T:C
*
* PowerShell
*
* 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.
*
* @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.
*
* 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).
*
*
* 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.
*
* @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}.
*
* This attribute is useful for special options like help ({@code -h} and {@code --help} on unix,
* {@code -?} and {@code -Help} on Windows).
*
*
* 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.
*
* @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}.
*
* This attribute is useful for special options like version ({@code -V} and {@code --version} on unix,
* {@code -Version} on Windows).
*
*
* 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.
*
* @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.
*
* 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.
*
* 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.
*
* For example, if an option has 2 required parameters and any number of optional parameters,
* specify {@code @Option(names = "-example", arity = "2..*")}.
*
* A note on boolean options
*
* 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:
* @Option(names = "-v", arity = "1") boolean verbose;
*
* Because this boolean field is defined with arity 1, the user must specify either {@code -v false}
* or {@code -v true}
* on the command line, or a {@link MissingParameterException} is thrown by the {@link #parse(String...)}
* method.
*
* To make the boolean parameter possible but optional, define the field with {@code arity = "0..1"}.
* For example:
* @Option(names="-v", arity="0..1") boolean verbose;
* This will accept any of the below without throwing an exception:
*
* -v
* -v true
* -v false
*
* @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:
* 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;
* }
* By default, the above gives a usage help message like the following:
* 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.
*
* @return name of the option parameter used in the usage help message
*/
String paramLabel() default "";
/**
*
* Specify a {@code type} if the annotated field is a {@code Collection} that should hold objects other than Strings.
*
* 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.
*
* 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.
* @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;
}
/**
*
* 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).
*
* 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.
*
* For example:
*
* 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;
* }
*
* A field cannot be annotated with both {@code @Parameters} and {@code @Option} or a {@code ParameterException}
* is thrown.
*/
@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.
* 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.
* @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:
* class Example {
* @Parameters(paramLabel="FILE", description="path of the input FILE(s)")
* private File[] inputFiles;
* }
* By default, the above gives a usage help message like the following:
* Usage: <main class> [FILE...]
* [FILE...] path of the input FILE(s)
*
* @return name of the positional parameter used in the usage help message
*/
String paramLabel() default "";
/**
*
* Specify a {@code type} if the annotated field is a {@code Collection} that should hold objects other than Strings.
*
* 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.
*
* 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.
* @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;
}
/**
* Annotate your class with {@code @Command} when you want more control over the format of the generated help
* message.
*
* @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;
* }
*
* The structure of a help message looks like this:
*
* - [header]
* - [synopsis]: {@code Usage:
[OPTIONS] [FILE...]}
* - [description]
* - [parameter list]: {@code [FILE...] Any number of input files}
* - [option list]: {@code -h, --help prints this help message and exits}
* - [footer]
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Command {
/** Program name to show in the synopsis. If omitted, {@code ""} 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 "";
/** 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:
*
* @Command(subcommands = {
* GitStatus.class,
* GitCommit.class,
* GitBranch.class })
* public class Git { ... }
*
* CommandLine commandLine = new CommandLine(new Git());
*
is equivalent to this:
*
* // 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());
*
* @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 " [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 {};
}
/**
*
* 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.
*
* 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.
*
* Java 8 lambdas make it easy to register custom type converters:
*
*
* 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));
*
* Built-in type converters are pre-registered for the following java 1.5 types:
*
*
* - all primitive types
* - all primitive wrapper types: Boolean, Byte, Character, Double, Float, Integer, Long, Short
* - any enum
* - java.io.File
* - java.math.BigDecimal
* - java.math.BigInteger
* - java.net.InetAddress
* - java.net.URI
* - java.net.URL
* - java.nio.charset.Charset
* - java.sql.Time
* - java.util.Date
* - java.util.UUID
* - java.util.regex.Pattern
* - StringBuilder
* - CharSequence
* - String
*
* @param the type of the object that is the result of the conversion
*/
public interface ITypeConverter {
/**
* 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 {
/** 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 requiredFields,
Map optionName2Field,
Map singleCharOption2Field,
List 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 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 Stack reverse(Stack stack) {
Collections.reverse(stack);
return stack;
}
/**
* Helper class responsible for processing command line arguments.
*/
private class Interpreter {
private final Map commands = new LinkedHashMap();
private final Map, ITypeConverter> converterRegistry = new HashMap, ITypeConverter>();
private final Map optionName2Field = new HashMap();
private final Map singleCharOption2Field = new HashMap();
private final List requiredFields = new ArrayList();
private final List positionalParametersFields = new ArrayList();
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 parse(String... args) {
Assert.notNull(args, "argument array");
Stack arguments = new Stack();
for (int i = args.length - 1; i >= 0; i--) {
arguments.push(args[i]);
}
List result = new ArrayList();
parse(result, arguments, args);
return result;
}
private void parse(List parsedCommands, Stack 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 required = new ArrayList(requiredFields);
Set initialized = new HashSet();
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());
} catch (ParameterException ex) { throw ex;
} catch (Exception ex) { throw new IllegalStateException("Internal error: " + ex, ex); }
}
}
}
private void processArguments(List parsedCommands,
Stack args,
Collection required,
Set 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 required, Stack args) throws Exception {
processPositionalParameters0(required, false, args);
if (!args.empty()) {
handleUnmatchedArguments(args);
return;
};
}
private void handleUnmatchedArguments(Stack 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 required, boolean validateOnly, Stack args) throws Exception {
int max = -1;
for (Field positionalParam : positionalParametersFields) {
Range indexRange = Range.parameterIndex(positionalParam);
max = Math.max(max, indexRange.max);
@SuppressWarnings("unchecked")
Stack argsCopy = reverse((Stack) 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 required,
Set initialized,
String arg,
Stack 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 required,
Set initialized,
String arg,
Stack 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 args,
Set initialized) 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);
}
if (Collection.class.isAssignableFrom(cls)) {
return applyValuesToCollectionField(field, annotation, arity, args, cls);
}
return applyValueToSingleValuedField(field, arity, args, cls, initialized);
}
private int applyValueToSingleValuedField(Field field,
Range arity,
Stack args,
Class cls,
Set initialized) 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
// special logic for booleans: BooleanConverter accepts only "true" or "false".
if ((cls == Boolean.class || cls == Boolean.TYPE) && arity.min <= 0) {
// boolean option with arity = 0..1 or 0..*: value MAY be a param
if (arity.max > 0 && ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
result = 1; // if it is a varargs we only consume 1 argument if it is a boolean value
} else {
if (value != null) {
args.push(value); // we don't consume the value
}
Boolean currentValue = (Boolean) field.get(command);
value = String.valueOf(currentValue == null ? true : !currentValue); // #147 toggle existing boolean value
}
}
if (noMoreValues && value == null) {
return 0;
}
if (initialized != null) {
if (initialized.contains(field) && !isOverwrittenOptionsAllowed()) {
throw new OverwrittenOptionException(optionDescription("", field, 0) + " should be specified only once");
}
initialized.add(field);
}
ITypeConverter converter = getTypeConverter(cls);
Object objValue = tryConvert(field, -1, converter, value, cls);
field.set(command, objValue);
return result;
}
private int applyValuesToArrayField(Field field,
Class annotation,
Range arity,
Stack args,
Class cls) throws Exception {
Class type = cls.getComponentType();
ITypeConverter converter = getTypeConverter(type);
List