+
+package 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.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 picocli.CommandLine.Help.Ansi.Text;
+
+import static java.util.Locale.ENGLISH;
+import static picocli.CommandLine.Help.Ansi.*;
+import static picocli.CommandLine.Help.Column.Overflow.*;
+
+
+public class CommandLine {
+
+ public static final String VERSION = "1.0.0-SNAPSHOT";
+
+ 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>();
+
+
+ public CommandLine(Object command) {
+ interpreter = new Interpreter(command);
+ }
+
+
+ public CommandLine addSubcommand(String name, Object command) {
+ CommandLine commandLine = toCommandLine(command);
+ commandLine.parent = this;
+ interpreter.commands.put(name, commandLine);
+ return this;
+ }
+
+ public Map<String, CommandLine> getSubcommands() {
+ return new LinkedHashMap<String, CommandLine>(interpreter.commands);
+ }
+
+ public CommandLine getParent() {
+ return parent;
+ }
+
+
+ public Object getCommand() {
+ return interpreter.command;
+ }
+
+
+ public boolean isUsageHelpRequested() { return usageHelpRequested; }
+
+
+ public boolean isVersionHelpRequested() { return versionHelpRequested; }
+
+
+ public boolean isOverwrittenOptionsAllowed() {
+ return overwrittenOptionsAllowed;
+ }
+
+
+ public CommandLine setOverwrittenOptionsAllowed(boolean newValue) {
+ this.overwrittenOptionsAllowed = newValue;
+ for (CommandLine command : interpreter.commands.values()) {
+ command.setOverwrittenOptionsAllowed(newValue);
+ }
+ return this;
+ }
+
+
+ public boolean isUnmatchedArgumentsAllowed() {
+ return unmatchedArgumentsAllowed;
+ }
+
+
+ public CommandLine setUnmatchedArgumentsAllowed(boolean newValue) {
+ this.unmatchedArgumentsAllowed = newValue;
+ for (CommandLine command : interpreter.commands.values()) {
+ command.setUnmatchedArgumentsAllowed(newValue);
+ }
+ return this;
+ }
+
+
+ public List<String> getUnmatchedArguments() {
+ return unmatchedArguments;
+ }
+
+
+ public static <T> T populateCommand(T command, String... args) {
+ CommandLine cli = toCommandLine(command);
+ cli.parse(args);
+ return command;
+ }
+
+
+ public List<CommandLine> parse(String... args) {
+ return interpreter.parse(args);
+ }
+
+
+ public static void usage(Object command, PrintStream out) {
+ toCommandLine(command).usage(out);
+ }
+
+
+ public static void usage(Object command, PrintStream out, Help.Ansi ansi) {
+ toCommandLine(command).usage(out, ansi);
+ }
+
+
+ public static void usage(Object command, PrintStream out, Help.ColorScheme colorScheme) {
+ toCommandLine(command).usage(out, colorScheme);
+ }
+
+
+ public void usage(PrintStream out) {
+ usage(out, Help.Ansi.AUTO);
+ }
+
+
+ public void usage(PrintStream out, Help.Ansi ansi) {
+ usage(out, Help.defaultColorScheme(ansi));
+ }
+
+ 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())
+ .append(help.synopsis(help.synopsisHeadingLength()))
+ .append(help.descriptionHeading())
+ .append(help.description())
+ .append(help.parameterListHeading())
+ .append(help.parameterList())
+ .append(help.optionListHeading())
+ .append(help.optionList())
+ .append(help.commandListHeading())
+ .append(help.commandList())
+ .append(help.footerHeading())
+ .append(help.footer());
+ out.print(sb);
+ }
+
+
+ public void printVersionHelp(PrintStream out) { printVersionHelp(out, Help.Ansi.AUTO); }
+
+
+ public void printVersionHelp(PrintStream out, Help.Ansi ansi) {
+ for (String versionInfo : versionLines) {
+ out.println(ansi.new Text(versionInfo));
+ }
+ }
+
+
+ public static <R extends Runnable> void run(R command, PrintStream out, String... args) {
+ run(command, out, AUTO, args);
+ }
+
+ public static <R extends Runnable> void run(R command, PrintStream out, Help.Ansi ansi, String... args) {
+ CommandLine cmd = new CommandLine(command);
+ try {
+ cmd.parse(args);
+ } catch (Exception ex) {
+ out.println(ex.getMessage());
+ cmd.usage(out, ansi);
+ return;
+ }
+ command.run();
+ }
+
+
+ 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;
+ }
+
+
+ public String getSeparator() {
+ return interpreter.separator;
+ }
+
+
+ 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);}
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface Option {
+
+ String[] names();
+
+
+ boolean required() default false;
+
+
+ boolean help() default false;
+
+
+ boolean usageHelp() default false;
+
+
+ boolean versionHelp() default false;
+
+
+ String[] description() default {};
+
+
+ String arity() default "";
+
+
+ String paramLabel() default "";
+
+
+ Class<?> type() default String.class;
+
+
+ String split() default "";
+
+
+ boolean hidden() default false;
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface Parameters {
+
+ String index() default "*";
+
+
+ String[] description() default {};
+
+
+ String arity() default "";
+
+
+ String paramLabel() default "";
+
+
+ Class<?> type() default String.class;
+
+
+ String split() default "";
+
+
+ boolean hidden() default false;
+ }
+
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ public @interface Command {
+
+ String name() default "<main class>";
+
+
+ Class<?>[] subcommands() default {};
+
+
+ String separator() default "=";
+
+
+ String[] version() default {};
+
+
+ String headerHeading() default "";
+
+
+ String[] header() default {};
+
+
+ String synopsisHeading() default "Usage: ";
+
+
+ boolean abbreviateSynopsis() default false;
+
+
+ String[] customSynopsis() default {};
+
+
+ String descriptionHeading() default "";
+
+
+ String[] description() default {};
+
+
+ String parameterListHeading() default "";
+
+
+ String optionListHeading() default "";
+
+
+ boolean sortOptions() default true;
+
+
+ char requiredOptionMarker() default ' ';
+
+
+ boolean showDefaultValues() default false;
+
+
+ String commandListHeading() default "Commands:%n";
+
+
+ String footerHeading() default "";
+
+
+ String[] footer() default {};
+ }
+
+ public interface ITypeConverter<K> {
+
+ K convert(String value) throws Exception;
+ }
+
+ public static class Range implements Comparable<Range> {
+
+ public final int min;
+
+ public final int max;
+ public final boolean isVariable;
+ private final boolean isUnspecified;
+ private final String originalValue;
+
+
+ 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;
+ }
+
+ 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");
+ }
+
+ 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");
+ }
+
+ 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;
+ }
+
+ 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");
+ }
+
+ public static Range valueOf(String range) {
+ range = range.trim();
+ boolean unspecified = range.length() == 0 || range.startsWith("..");
+ 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;
+ }
+ }
+
+ public Range min(int newMin) { return new Range(newMin, Math.max(newMin, max), isVariable, isUnspecified, originalValue); }
+
+
+ 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;
+ }
+ }
+ 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()) {
+ 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;
+ }
+
+ 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(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");
+ }
+ }
+
+
+ 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) {
+
+ 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 {
+
+
+
+
+
+
+
+
+
+ while (!args.isEmpty()) {
+ String arg = args.pop();
+
+
+
+ if ("--".equals(arg)) {
+ processPositionalParameters(required, args);
+ return;
+ }
+
+
+ if (commands.containsKey(arg)) {
+ if (!isHelpRequested && !required.isEmpty()) {
+ throw MissingParameterException.create(required);
+ }
+ commands.get(arg).interpreter.parse(parsedCommands, args, originalArgs);
+ return;
+ }
+
+
+
+
+
+ boolean paramAttachedToOption = false;
+ int separatorIndex = arg.indexOf(separator);
+ if (separatorIndex > 0) {
+ String key = arg.substring(0, separatorIndex);
+
+ 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);
+ }
+
+
+ else if (arg.length() > 2 && arg.startsWith("-")) {
+ processClusteredShortOptions(required, initialized, arg, args);
+ }
+
+
+ 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()); }
+ }
+
+ 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);
+ }
+ }
+
+ 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));
+ }
+ 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)) {
+ cluster = cluster.substring(separator.length());
+ arity = arity.min(Math.max(1, arity.min));
+ }
+ args.push(cluster);
+
+
+
+ int consumed = applyOption(field, Option.class, arity, paramAttachedToOption, args, initialized);
+
+ if (consumed > 0) {
+ return;
+ }
+ cluster = args.pop();
+ } else {
+ if (cluster.length() == 0) {
+ return;
+ }
+
+
+ if (arg.endsWith(cluster)) {
+
+ 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() == 0 && !valueAttachedToOption) {
+ args.pop();
+ }
+ 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<String> args,
+ Class<?> cls,
+ Set<Field> initialized) throws Exception {
+ boolean noMoreValues = args.isEmpty();
+ String value = args.isEmpty() ? null : trim(args.pop());
+ int result = arity.min;
+
+
+ if ((cls == Boolean.class || cls == Boolean.TYPE) && arity.min <= 0) {
+
+
+ if (arity.max > 0 && ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
+ result = 1;
+ } else {
+ if (value != null) {
+ args.push(value);
+ }
+ Boolean currentValue = (Boolean) field.get(command);
+ value = String.valueOf(currentValue == null ? true : !currentValue);
+ }
+ }
+ 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<String> args,
+ Class<?> cls) throws Exception {
+ Class<?> type = cls.getComponentType();
+ ITypeConverter<?> converter = getTypeConverter(type);
+ List<Object> converted = consumeArguments(field, annotation, arity, args, converter, cls);
+ Object existing = field.get(command);
+ int length = existing == null ? 0 : Array.getLength(existing);
+ List<Object> newValues = new ArrayList<Object>();
+ for (int i = 0; i < length; i++) {
+ newValues.add(Array.get(existing, i));
+ }
+ for (Object obj : converted) {
+ if (obj instanceof Collection<?>) {
+ newValues.addAll((Collection<?>) obj);
+ } else {
+ newValues.add(obj);
+ }
+ }
+ Object array = Array.newInstance(type, newValues.size());
+ field.set(command, array);
+ for (int i = 0; i < newValues.size(); i++) {
+ Array.set(array, i, newValues.get(i));
+ }
+ return converted.size();
+ }
+
+ @SuppressWarnings("unchecked")
+ private int applyValuesToCollectionField(Field field,
+ Class<?> annotation,
+ Range arity,
+ Stack<String> args,
+ Class<?> cls) throws Exception {
+ Collection<Object> collection = (Collection<Object>) field.get(command);
+ Class<?> type = getTypeAttribute(field);
+ ITypeConverter<?> converter = getTypeConverter(type);
+ List<Object> converted = consumeArguments(field, annotation, arity, args, converter, type);
+ if (collection == null) {
+ collection = createCollection(cls);
+ field.set(command, collection);
+ }
+ for (Object element : converted) {
+ if (element instanceof Collection<?>) {
+ collection.addAll((Collection<?>) element);
+ } else {
+ collection.add(element);
+ }
+ }
+ return converted.size();
+ }
+
+ private List<Object> consumeArguments(Field field,
+ Class<?> annotation,
+ Range arity,
+ Stack<String> args,
+ ITypeConverter<?> converter,
+ Class<?> type) throws Exception {
+ List<Object> result = new ArrayList<Object>();
+ int index = 0;
+
+
+ for (int i = 0; result.size() < arity.min; i++) {
+ index = consumeOneArgument(field, arity, args, converter, type, result, index);
+ }
+
+ while (result.size() < arity.max && !args.isEmpty()) {
+ if (annotation != Parameters.class) {
+ if (commands.containsKey(args.peek()) || isOption(args.peek())) {
+ return result;
+ }
+ }
+ index = consumeOneArgument(field, arity, args, converter, type, result, index);
+ }
+ return result;
+ }
+
+ private int consumeOneArgument(Field field,
+ Range arity,
+ Stack<String> args,
+ ITypeConverter<?> converter,
+ Class<?> type,
+ List<Object> result, int index) throws Exception {
+ String[] values = split(trim(args.pop()), field);
+
+
+ int max = Math.min(arity.max - result.size(), values.length);
+ for (int j = 0; j < max; j++) {
+ result.add(tryConvert(field, index, converter, values[j], type));
+ }
+
+
+ for (int j = values.length - 1; j >= max; j--) {
+ args.push(values[j]);
+ }
+ index++;
+ return index;
+ }
+
+ private String[] split(String value, Field field) {
+ if (field.isAnnotationPresent(Option.class)) {
+ String regex = field.getAnnotation(Option.class).split();
+ return regex.length() == 0 ? new String[] {value} : value.split(regex);
+ }
+ if (field.isAnnotationPresent(Parameters.class)) {
+ String regex = field.getAnnotation(Parameters.class).split();
+ return regex.length() == 0 ? new String[] {value} : value.split(regex);
+ }
+ return new String[] {value};
+ }
+
+
+ private boolean isOption(String arg) {
+ if ("--".equals(arg)) {
+ return true;
+ }
+
+ if (optionName2Field.containsKey(arg)) {
+ return true;
+ }
+ int separatorIndex = arg.indexOf(separator);
+ if (separatorIndex > 0) {
+ if (optionName2Field.containsKey(arg.substring(0, separatorIndex))) {
+ return true;
+ }
+ }
+ return (arg.length() > 2 && arg.startsWith("-") && singleCharOption2Field.containsKey(arg.charAt(1)));
+ }
+ private Object tryConvert(Field field, int index, ITypeConverter<?> converter, String value, Class<?> type)
+ throws Exception {
+ try {
+ return converter.convert(value);
+ } catch (ParameterException ex) {
+ throw new ParameterException(ex.getMessage() + optionDescription(" for ", field, index));
+ } catch (Exception other) {
+ String desc = optionDescription(" for ", field, index) + ": " + other;
+ throw new ParameterException("Could not convert '" + value + "' to " + type.getSimpleName() + desc, other);
+ }
+ }
+
+ private String optionDescription(String prefix, Field field, int index) {
+ Help.IParamLabelRenderer labelRenderer = Help.createMinimalParamLabelRenderer();
+ String desc = "";
+ if (field.isAnnotationPresent(Option.class)) {
+ desc = prefix + "option '" + field.getAnnotation(Option.class).names()[0] + "'";
+ if (index >= 0) {
+ Range arity = Range.optionArity(field);
+ if (arity.max > 1) {
+ desc += " at index " + index;
+ }
+ desc += " (" + labelRenderer.renderParameterLabel(field, Help.Ansi.OFF, Collections.<IStyle>emptyList()) + ")";
+ }
+ } else if (field.isAnnotationPresent(Parameters.class)) {
+ Range indexRange = Range.parameterIndex(field);
+ Text label = labelRenderer.renderParameterLabel(field, Help.Ansi.OFF, Collections.<IStyle>emptyList());
+ desc = prefix + "positional parameter at index " + indexRange + " (" + label + ")";
+ }
+ return desc;
+ }
+
+ private Class<?> getTypeAttribute(Field field) {
+ if (field.isAnnotationPresent(Parameters.class)) {
+ return field.getAnnotation(Parameters.class).type();
+ } else if (field.isAnnotationPresent(Option.class)) {
+ return field.getAnnotation(Option.class).type();
+ }
+ throw new IllegalStateException(field + " has neither @Parameters nor @Option annotation");
+ }
+
+ private boolean isAnyHelpRequested() { return isHelpRequested || versionHelpRequested || usageHelpRequested; }
+
+ private void updateHelpRequested(Field field) {
+ if (field.isAnnotationPresent(Option.class)) {
+ isHelpRequested |= field.getAnnotation(Option.class).help();
+ CommandLine.this.versionHelpRequested |= field.getAnnotation(Option.class).versionHelp();
+ CommandLine.this.usageHelpRequested |= field.getAnnotation(Option.class).usageHelp();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Collection<Object> createCollection(Class<?> collectionClass) throws Exception {
+ if (collectionClass.isInterface()) {
+ if (List.class.isAssignableFrom(collectionClass)) {
+ return new ArrayList<Object>();
+ } else if (SortedSet.class.isAssignableFrom(collectionClass)) {
+ return new TreeSet<Object>();
+ } else if (Set.class.isAssignableFrom(collectionClass)) {
+ return new HashSet<Object>();
+ } else if (Queue.class.isAssignableFrom(collectionClass)) {
+ return new LinkedList<Object>();
+ }
+ return new ArrayList<Object>();
+ }
+
+ return (Collection<Object>) collectionClass.newInstance();
+ }
+
+ private ITypeConverter<?> getTypeConverter(final Class<?> type) {
+ ITypeConverter<?> result = converterRegistry.get(type);
+ if (result != null) {
+ return result;
+ }
+ if (type.isEnum()) {
+ return new ITypeConverter<Object>() {
+ @SuppressWarnings("unchecked")
+ public Object convert(String value) throws Exception {
+ return Enum.valueOf((Class<Enum>) type, value);
+ }
+ };
+ }
+ throw new MissingTypeConverterException("No TypeConverter registered for " + type.getName());
+ }
+
+ private void assertNoMissingParameters(Field field, int arity, Stack<String> args) {
+ if (arity > args.size()) {
+ int actualSize = 0;
+ @SuppressWarnings("unchecked")
+ Stack<String> copy = (Stack<String>) args.clone();
+ while (!copy.isEmpty()) {
+ actualSize += split(copy.pop(), field).length;
+ if (actualSize >= arity) { return; }
+ }
+ if (arity == 1) {
+ if (field.isAnnotationPresent(Option.class)) {
+ throw new MissingParameterException("Missing required parameter for " +
+ optionDescription("", field, 0));
+ }
+ Range indexRange = Range.parameterIndex(field);
+ Help.IParamLabelRenderer labelRenderer = Help.createMinimalParamLabelRenderer();
+ String sep = "";
+ String names = "";
+ int count = 0;
+ for (int i = indexRange.min; i < positionalParametersFields.size(); i++) {
+ if (Range.parameterArity(positionalParametersFields.get(i)).min > 0) {
+ names += sep + labelRenderer.renderParameterLabel(positionalParametersFields.get(i),
+ Help.Ansi.OFF, Collections.<IStyle>emptyList());
+ sep = ", ";
+ count++;
+ }
+ }
+ String msg = "Missing required parameter";
+ Range paramArity = Range.parameterArity(field);
+ if (paramArity.isVariable) {
+ msg += "s at positions " + indexRange + ": ";
+ } else {
+ msg += (count > 1 ? "s: " : ": ");
+ }
+ throw new MissingParameterException(msg + names);
+ }
+ throw new MissingParameterException(optionDescription("", field, 0) +
+ " requires at least " + arity + " values, but only " + args.size() + " were specified.");
+ }
+ }
+ private String trim(String value) {
+ return unquote(value);
+ }
+
+ private String unquote(String value) {
+ return value == null
+ ? null
+ : (value.length() > 1 && value.startsWith("\"") && value.endsWith("\""))
+ ? value.substring(1, value.length() - 1)
+ : value;
+ }
+ }
+ private static class PositionalParametersSorter implements Comparator<Field> {
+ public int compare(Field o1, Field o2) {
+ int result = Range.parameterIndex(o1).compareTo(Range.parameterIndex(o2));
+ return (result == 0) ? Range.parameterArity(o1).compareTo(Range.parameterArity(o2)) : result;
+ }
+ }
+
+ private static class BuiltIn {
+ static class StringConverter implements ITypeConverter<String> {
+ public String convert(String value) { return value; }
+ }
+ static class StringBuilderConverter implements ITypeConverter<StringBuilder> {
+ public StringBuilder convert(String value) { return new StringBuilder(value); }
+ }
+ static class CharSequenceConverter implements ITypeConverter<CharSequence> {
+ public String convert(String value) { return value; }
+ }
+
+ static class ByteConverter implements ITypeConverter<Byte> {
+ public Byte convert(String value) { return Byte.valueOf(value); }
+ }
+
+ static class BooleanConverter implements ITypeConverter<Boolean> {
+ public Boolean convert(String value) {
+ if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
+ return Boolean.parseBoolean(value);
+ } else {
+ throw new ParameterException("'" + value + "' is not a boolean");
+ }
+ }
+ }
+ static class CharacterConverter implements ITypeConverter<Character> {
+ public Character convert(String value) {
+ if (value.length() > 1) {
+ throw new ParameterException("'" + value + "' is not a single character");
+ }
+ return value.charAt(0);
+ }
+ }
+
+ static class ShortConverter implements ITypeConverter<Short> {
+ public Short convert(String value) { return Short.valueOf(value); }
+ }
+
+ static class IntegerConverter implements ITypeConverter<Integer> {
+ public Integer convert(String value) { return Integer.valueOf(value); }
+ }
+
+ static class LongConverter implements ITypeConverter<Long> {
+ public Long convert(String value) { return Long.valueOf(value); }
+ }
+ static class FloatConverter implements ITypeConverter<Float> {
+ public Float convert(String value) { return Float.valueOf(value); }
+ }
+ static class DoubleConverter implements ITypeConverter<Double> {
+ public Double convert(String value) { return Double.valueOf(value); }
+ }
+ static class FileConverter implements ITypeConverter<File> {
+ public File convert(String value) { return new File(value); }
+ }
+ static class URLConverter implements ITypeConverter<URL> {
+ public URL convert(String value) throws MalformedURLException { return new URL(value); }
+ }
+ static class URIConverter implements ITypeConverter<URI> {
+ public URI convert(String value) throws URISyntaxException { return new URI(value); }
+ }
+
+ static class ISO8601DateConverter implements ITypeConverter<Date> {
+ public Date convert(String value) {
+ try {
+ return new SimpleDateFormat("yyyy-MM-dd").parse(value);
+ } catch (ParseException e) {
+ throw new ParameterException("'" + value + "' is not a yyyy-MM-dd date");
+ }
+ }
+ }
+
+ static class ISO8601TimeConverter implements ITypeConverter<Time> {
+ public Time convert(String value) {
+ try {
+ if (value.length() <= 5) {
+ return new Time(new SimpleDateFormat("HH:mm").parse(value).getTime());
+ } else if (value.length() <= 8) {
+ return new Time(new SimpleDateFormat("HH:mm:ss").parse(value).getTime());
+ } else if (value.length() <= 12) {
+ try {
+ return new Time(new SimpleDateFormat("HH:mm:ss.SSS").parse(value).getTime());
+ } catch (ParseException e2) {
+ return new Time(new SimpleDateFormat("HH:mm:ss,SSS").parse(value).getTime());
+ }
+ }
+ } catch (ParseException ignored) {
+
+ }
+ throw new ParameterException("'" + value + "' is not a HH:mm[:ss[.SSS]] time");
+ }
+ }
+ static class BigDecimalConverter implements ITypeConverter<BigDecimal> {
+ public BigDecimal convert(String value) { return new BigDecimal(value); }
+ }
+ static class BigIntegerConverter implements ITypeConverter<BigInteger> {
+ public BigInteger convert(String value) { return new BigInteger(value); }
+ }
+ static class CharsetConverter implements ITypeConverter<Charset> {
+ public Charset convert(String s) { return Charset.forName(s); }
+ }
+
+ static class InetAddressConverter implements ITypeConverter<InetAddress> {
+ public InetAddress convert(String s) throws Exception { return InetAddress.getByName(s); }
+ }
+ static class PatternConverter implements ITypeConverter<Pattern> {
+ public Pattern convert(String s) { return Pattern.compile(s); }
+ }
+ static class UUIDConverter implements ITypeConverter<UUID> {
+ public UUID convert(String s) throws Exception { return UUID.fromString(s); }
+ }
+ private BuiltIn() {}
+ }
+
+
+ public static class Help {
+
+ protected static final String DEFAULT_COMMAND_NAME = "<main class>";
+
+ private final static int usageHelpWidth = 80;
+ private final static int optionsColumnWidth = 2 + 2 + 1 + 24;
+ private final Object command;
+ private final Map<String, Help> commands = new LinkedHashMap<String, Help>();
+ final ColorScheme colorScheme;
+
+
+ public final List<Field> optionFields;
+
+
+ public final List<Field> positionalParametersFields;
+
+
+ public String separator;
+
+
+ public String commandName = DEFAULT_COMMAND_NAME;
+
+
+ public String[] description = {};
+
+
+ public String[] customSynopsis = {};
+
+
+ public String[] header = {};
+
+
+ public String[] footer = {};
+
+
+ public IParamLabelRenderer parameterLabelRenderer;
+
+
+ public Boolean abbreviateSynopsis;
+
+
+ public Boolean sortOptions;
+
+
+ public Boolean showDefaultValues;
+
+
+ public Character requiredOptionMarker;
+
+
+ public String headerHeading;
+
+
+ public String synopsisHeading;
+
+
+ public String descriptionHeading;
+
+
+ public String parameterListHeading;
+
+
+ public String optionListHeading;
+
+
+ public String commandListHeading;
+
+
+ public String footerHeading;
+
+
+ public Help(Object command) {
+ this(command, Ansi.AUTO);
+ }
+
+
+ public Help(Object command, Ansi ansi) {
+ this(command, defaultColorScheme(ansi));
+ }
+
+
+ public Help(Object command, ColorScheme colorScheme) {
+ this.command = Assert.notNull(command, "command");
+ this.colorScheme = Assert.notNull(colorScheme, "colorScheme").applySystemProperties();
+ List<Field> options = new ArrayList<Field>();
+ List<Field> operands = new ArrayList<Field>();
+ Class<?> cls = command.getClass();
+ while (cls != null) {
+ for (Field field : cls.getDeclaredFields()) {
+ field.setAccessible(true);
+ if (field.isAnnotationPresent(Option.class)) {
+ Option option = field.getAnnotation(Option.class);
+ if (!option.hidden()) {
+
+ options.add(field);
+ }
+ }
+ if (field.isAnnotationPresent(Parameters.class)) {
+ operands.add(field);
+ }
+ }
+
+ if (cls.isAnnotationPresent(Command.class)) {
+ Command cmd = cls.getAnnotation(Command.class);
+ if (DEFAULT_COMMAND_NAME.equals(commandName)) {
+ commandName = cmd.name();
+ }
+ separator = (separator == null) ? cmd.separator() : separator;
+ abbreviateSynopsis = (abbreviateSynopsis == null) ? cmd.abbreviateSynopsis() : abbreviateSynopsis;
+ sortOptions = (sortOptions == null) ? cmd.sortOptions() : sortOptions;
+ requiredOptionMarker = (requiredOptionMarker == null) ? cmd.requiredOptionMarker() : requiredOptionMarker;
+ showDefaultValues = (showDefaultValues == null) ? cmd.showDefaultValues() : showDefaultValues;
+ customSynopsis = empty(customSynopsis) ? cmd.customSynopsis() : customSynopsis;
+ description = empty(description) ? cmd.description() : description;
+ header = empty(header) ? cmd.header() : header;
+ footer = empty(footer) ? cmd.footer() : footer;
+ headerHeading = empty(headerHeading) ? cmd.headerHeading() : headerHeading;
+ synopsisHeading = empty(synopsisHeading) || "Usage: ".equals(synopsisHeading) ? cmd.synopsisHeading() : synopsisHeading;
+ descriptionHeading = empty(descriptionHeading) ? cmd.descriptionHeading() : descriptionHeading;
+ parameterListHeading = empty(parameterListHeading) ? cmd.parameterListHeading() : parameterListHeading;
+ optionListHeading = empty(optionListHeading) ? cmd.optionListHeading() : optionListHeading;
+ commandListHeading = empty(commandListHeading) || "Commands:%n".equals(commandListHeading) ? cmd.commandListHeading() : commandListHeading;
+ footerHeading = empty(footerHeading) ? cmd.footerHeading() : footerHeading;
+ }
+ cls = cls.getSuperclass();
+ }
+ sortOptions = (sortOptions == null) ? true : sortOptions;
+ abbreviateSynopsis = (abbreviateSynopsis == null) ? false : abbreviateSynopsis;
+ requiredOptionMarker = (requiredOptionMarker == null) ? ' ' : requiredOptionMarker;
+ showDefaultValues = (showDefaultValues == null) ? false : showDefaultValues;
+ synopsisHeading = (synopsisHeading == null) ? "Usage: " : synopsisHeading;
+ commandListHeading = (commandListHeading == null) ? "Commands:%n" : commandListHeading;
+ separator = (separator == null) ? "=" : separator;
+ parameterLabelRenderer = new DefaultParamLabelRenderer(separator);
+ Collections.sort(operands, new PositionalParametersSorter());
+ positionalParametersFields = Collections.unmodifiableList(operands);
+ optionFields = Collections.unmodifiableList(options);
+ }
+
+
+ public Help addAllSubcommands(Map<String, CommandLine> commands) {
+ if (commands != null) {
+ for (Map.Entry<String, CommandLine> entry : commands.entrySet()) {
+ addSubcommand(entry.getKey(), entry.getValue().getCommand());
+ }
+ }
+ return this;
+ }
+
+
+ public Help addSubcommand(String commandName, Object command) {
+ commands.put(commandName, new Help(command));
+ return this;
+ }
+
+
+ public String synopsis() { return synopsis(0); }
+
+
+ public String synopsis(int synopsisHeadingLength) {
+ if (!empty(customSynopsis)) { return customSynopsis(); }
+ return abbreviateSynopsis ? abbreviatedSynopsis()
+ : detailedSynopsis(synopsisHeadingLength, createShortOptionArityAndNameComparator(), true);
+ }
+
+
+ public String abbreviatedSynopsis() {
+ StringBuilder sb = new StringBuilder();
+ if (!optionFields.isEmpty()) {
+ sb.append(" [OPTIONS]");
+ }
+
+ for (Field positionalParam : positionalParametersFields) {
+ if (!positionalParam.getAnnotation(Parameters.class).hidden()) {
+ sb.append(' ').append(parameterLabelRenderer.renderParameterLabel(positionalParam, ansi(), colorScheme.parameterStyles));
+ }
+ }
+ return colorScheme.commandText(commandName).toString()
+ + (sb.toString()) + System.getProperty("line.separator");
+ }
+
+ public String detailedSynopsis(Comparator<Field> optionSort, boolean clusterBooleanOptions) {
+ return detailedSynopsis(0, optionSort, clusterBooleanOptions);
+ }
+
+
+ public String detailedSynopsis(int synopsisHeadingLength, Comparator<Field> optionSort, boolean clusterBooleanOptions) {
+ Text optionText = ansi().new Text(0);
+ List<Field> fields = new ArrayList<Field>(optionFields);
+ if (optionSort != null) {
+ Collections.sort(fields, optionSort);
+ }
+ if (clusterBooleanOptions) {
+ List<Field> booleanOptions = new ArrayList<Field>();
+ StringBuilder clusteredRequired = new StringBuilder("-");
+ StringBuilder clusteredOptional = new StringBuilder("-");
+ for (Field field : fields) {
+ if (field.getType() == boolean.class || field.getType() == Boolean.class) {
+ Option option = field.getAnnotation(Option.class);
+ String shortestName = ShortestFirst.sort(option.names())[0];
+ if (shortestName.length() == 2 && shortestName.startsWith("-")) {
+ booleanOptions.add(field);
+ if (option.required()) {
+ clusteredRequired.append(shortestName.substring(1));
+ } else {
+ clusteredOptional.append(shortestName.substring(1));
+ }
+ }
+ }
+ }
+ fields.removeAll(booleanOptions);
+ if (clusteredRequired.length() > 1) {
+ optionText = optionText.append(" ").append(colorScheme.optionText(clusteredRequired.toString()));
+ }
+ if (clusteredOptional.length() > 1) {
+ optionText = optionText.append(" [").append(colorScheme.optionText(clusteredOptional.toString())).append("]");
+ }
+ }
+ for (Field field : fields) {
+ Option option = field.getAnnotation(Option.class);
+ if (!option.hidden()) {
+
+ optionText = optionText.append(option.required() ? " " : " [");
+
+ String optionNames = ShortestFirst.sort(option.names())[0];
+ optionText = optionText.append(colorScheme.optionText(optionNames));
+
+ Text optionParamText = parameterLabelRenderer.renderParameterLabel(field, colorScheme.ansi(), colorScheme.optionParamStyles);
+ optionText = optionText.append(optionParamText);
+ if (!option.required()) {
+ optionText = optionText.append("]");
+ }
+ }
+ }
+ for (Field positionalParam : positionalParametersFields) {
+ if (!positionalParam.getAnnotation(Parameters.class).hidden()) {
+ optionText = optionText.append(" ");
+ Text label = parameterLabelRenderer.renderParameterLabel(positionalParam, colorScheme.ansi(), colorScheme.parameterStyles);
+ optionText = optionText.append(label);
+ }
+ }
+
+ int firstColumnLength = commandName.length() + synopsisHeadingLength;
+
+
+ TextTable textTable = new TextTable(ansi(), firstColumnLength, usageHelpWidth - firstColumnLength);
+ textTable.indentWrappedLines = 1;
+
+
+ Text PADDING = Ansi.OFF.new Text(spaces(synopsisHeadingLength));
+ textTable.addRowValues(new Text[] {PADDING.append(colorScheme.commandText(commandName)), optionText});
+ return textTable.toString().substring(synopsisHeadingLength);
+ }
+
+ public int synopsisHeadingLength() {
+ String[] lines = Ansi.OFF.new Text(synopsisHeading).toString().split("\\r?\\n|\\r|%n", -1);
+ return lines[lines.length - 1].length();
+ }
+
+ public String optionList() {
+ Comparator<Field> sortOrder = sortOptions == null || sortOptions.booleanValue()
+ ? createShortOptionNameComparator()
+ : null;
+ return optionList(createDefaultLayout(), sortOrder, createDefaultParamLabelRenderer());
+ }
+
+
+ public String optionList(Layout layout, Comparator<Field> optionSort, IParamLabelRenderer valueLabelRenderer) {
+ List<Field> fields = new ArrayList<Field>(optionFields);
+ if (optionSort != null) {
+ Collections.sort(fields, optionSort);
+ }
+ layout.addOptions(fields, valueLabelRenderer);
+ return layout.toString();
+ }
+
+
+ public String parameterList() {
+ return parameterList(createDefaultLayout(), createMinimalParamLabelRenderer());
+ }
+
+ public String parameterList(Layout layout, IParamLabelRenderer paramLabelRenderer) {
+ layout.addPositionalParameters(positionalParametersFields, paramLabelRenderer);
+ return layout.toString();
+ }
+
+
+ public static StringBuilder join(Ansi ansi, String[] values, StringBuilder sb, Object... params) {
+ if (values != null) {
+ TextTable table = new TextTable(ansi, usageHelpWidth);
+ table.indentWrappedLines = 0;
+ for (String summaryLine : values) {
+ table.addRowValues(ansi.new Text(String.format(summaryLine, params)));
+ }
+ table.toString(sb);
+ }
+ return sb;
+ }
+
+ public String customSynopsis(Object... params) {
+ return join(ansi(), customSynopsis, new StringBuilder(), params).toString();
+ }
+
+ public String description(Object... params) {
+ return join(ansi(), description, new StringBuilder(), params).toString();
+ }
+
+ public String header(Object... params) {
+ return join(ansi(), header, new StringBuilder(), params).toString();
+ }
+
+ public String footer(Object... params) {
+ return join(ansi(), footer, new StringBuilder(), params).toString();
+ }
+
+
+ public String headerHeading(Object... params) {
+ return ansi().new Text(format(headerHeading, params)).toString();
+ }
+
+
+ public String synopsisHeading(Object... params) {
+ return ansi().new Text(format(synopsisHeading, params)).toString();
+ }
+
+
+ public String descriptionHeading(Object... params) {
+ return empty(descriptionHeading) ? "" : ansi().new Text(format(descriptionHeading, params)).toString();
+ }
+
+
+ public String parameterListHeading(Object... params) {
+ return positionalParametersFields.isEmpty() ? "" : ansi().new Text(format(parameterListHeading, params)).toString();
+ }
+
+
+ public String optionListHeading(Object... params) {
+ return optionFields.isEmpty() ? "" : ansi().new Text(format(optionListHeading, params)).toString();
+ }
+
+
+ public String commandListHeading(Object... params) {
+ return commands.isEmpty() ? "" : ansi().new Text(format(commandListHeading, params)).toString();
+ }
+
+
+ public String footerHeading(Object... params) {
+ return ansi().new Text(format(footerHeading, params)).toString();
+ }
+ private String format(String formatString, Object[] params) {
+ return formatString == null ? "" : String.format(formatString, params);
+ }
+
+ public String commandList() {
+ if (commands.isEmpty()) { return ""; }
+ int commandLength = maxLength(commands.keySet());
+ Help.TextTable textTable = new Help.TextTable(ansi(),
+ new Help.Column(commandLength + 2, 2, Help.Column.Overflow.SPAN),
+ new Help.Column(usageHelpWidth - (commandLength + 2), 2, Help.Column.Overflow.WRAP));
+
+ for (Map.Entry<String, Help> entry : commands.entrySet()) {
+ Help command = entry.getValue();
+ String header = command.header != null && command.header.length > 0 ? command.header[0]
+ : (command.description != null && command.description.length > 0 ? command.description[0] : "");
+ textTable.addRowValues(colorScheme.commandText(entry.getKey()), ansi().new Text(header));
+ }
+ return textTable.toString();
+ }
+ private static int maxLength(Collection<String> any) {
+ List<String> strings = new ArrayList<String>(any);
+ Collections.sort(strings, Collections.reverseOrder(Help.shortestFirst()));
+ return strings.get(0).length();
+ }
+ private static String join(String[] names, int offset, int length, String separator) {
+ if (names == null) { return ""; }
+ StringBuilder result = new StringBuilder();
+ for (int i = offset; i < offset + length; i++) {
+ result.append((i > offset) ? separator : "").append(names[i]);
+ }
+ return result.toString();
+ }
+ private static String spaces(int length) {
+ char[] buff = new char[length];
+ Arrays.fill(buff, ' ');
+ return new String(buff);
+ }
+
+
+ public Layout createDefaultLayout() {
+ return new Layout(colorScheme, new TextTable(colorScheme.ansi()), createDefaultOptionRenderer(), createDefaultParameterRenderer());
+ }
+
+ public IOptionRenderer createDefaultOptionRenderer() {
+ DefaultOptionRenderer result = new DefaultOptionRenderer();
+ result.requiredMarker = String.valueOf(requiredOptionMarker);
+ if (showDefaultValues != null && showDefaultValues.booleanValue()) {
+ result.command = this.command;
+ }
+ return result;
+ }
+
+ public static IOptionRenderer createMinimalOptionRenderer() {
+ return new MinimalOptionRenderer();
+ }
+
+
+ public IParameterRenderer createDefaultParameterRenderer() {
+ DefaultParameterRenderer result = new DefaultParameterRenderer();
+ result.requiredMarker = String.valueOf(requiredOptionMarker);
+ return result;
+ }
+
+ public static IParameterRenderer createMinimalParameterRenderer() {
+ return new MinimalParameterRenderer();
+ }
+
+
+ public static IParamLabelRenderer createMinimalParamLabelRenderer() {
+ return new IParamLabelRenderer() {
+ public Text renderParameterLabel(Field field, Ansi ansi, List<IStyle> styles) {
+ String paramLabel = null;
+ Parameters parameters = field.getAnnotation(Parameters.class);
+ if (parameters != null) {
+ paramLabel = parameters.paramLabel();
+ } else {
+ paramLabel = field.isAnnotationPresent(Option.class) ? field.getAnnotation(Option.class).paramLabel() : null;
+ }
+ String text = paramLabel == null || paramLabel.length() == 0 ? field.getName() : paramLabel;
+ return ansi.apply(text, styles);
+ }
+ public String separator() { return ""; }
+ };
+ }
+
+ public IParamLabelRenderer createDefaultParamLabelRenderer() {
+ return new DefaultParamLabelRenderer(separator);
+ }
+
+ public static Comparator<Field> createShortOptionNameComparator() {
+ return new SortByShortestOptionNameAlphabetically();
+ }
+
+ public static Comparator<Field> createShortOptionArityAndNameComparator() {
+ return new SortByOptionArityAndNameAlphabetically();
+ }
+
+ public static Comparator<String> shortestFirst() {
+ return new ShortestFirst();
+ }
+
+
+ public Ansi ansi() {
+ return colorScheme.ansi;
+ }
+
+
+ public interface IOptionRenderer {
+
+ Text[][] render(Option option, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme);
+ }
+
+ static class DefaultOptionRenderer implements IOptionRenderer {
+ public String requiredMarker = " ";
+ public Object command;
+ public Text[][] render(Option option, Field field, IParamLabelRenderer paramLabelRenderer, ColorScheme scheme) {
+ String[] names = ShortestFirst.sort(option.names());
+ int shortOptionCount = names[0].length() == 2 ? 1 : 0;
+ String shortOption = shortOptionCount > 0 ? names[0] : "";
+ Text paramLabelText = paramLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.optionParamStyles);
+ String longOption = join(names, shortOptionCount, names.length - shortOptionCount, ", ");
+ String sep = shortOptionCount > 0 && names.length > 1 ? "," : "";
+
+
+ if (paramLabelText.length > 0 && longOption.length() == 0) {
+ sep = paramLabelRenderer.separator();
+ paramLabelText = paramLabelText.substring(sep.length());
+ }
+ Text longOptionText = scheme.optionText(longOption);
+ longOptionText = longOptionText.append(paramLabelText);
+ String requiredOption = option.required() ? requiredMarker : "";
+
+ boolean showDefault = command != null && !option.help() && !isBoolean(field.getType());
+ Object defaultValue = null;
+ try {
+ defaultValue = field.get(command);
+ if (defaultValue != null && field.getType().isArray()) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < Array.getLength(defaultValue); i++) {
+ sb.append(i > 0 ? ", " : "").append(Array.get(defaultValue, i));
+ }
+ defaultValue = sb.insert(0, "[").append("]").toString();
+ }
+ } catch (Exception ex) {
+ showDefault = false;
+ }
+ final int descriptionCount = Math.max(1, option.description().length);
+ final int ROW_COUNT = showDefault ? descriptionCount + 1 : descriptionCount;
+ final int COLUMN_COUNT = 5;
+ Text EMPTY = Ansi.EMPTY_TEXT;
+ Text[][] result = new Text[ROW_COUNT][COLUMN_COUNT];
+ result[0] = new Text[] { scheme.optionText(requiredOption), scheme.optionText(shortOption),
+ scheme.ansi().new Text(sep), longOptionText, scheme.ansi().new Text(str(option.description(), 0)) };
+ for (int i = 1; i < option.description().length; i++) {
+ result[i] = new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, scheme.ansi().new Text(option.description()[i]) };
+ }
+ if (showDefault) {
+ Arrays.fill(result[result.length - 1], EMPTY);
+ int row = empty(result[ROW_COUNT - 2][COLUMN_COUNT - 1]) ? ROW_COUNT - 2 : ROW_COUNT - 1;
+ result[row][COLUMN_COUNT - 1] = scheme.ansi().new Text(" Default: " + defaultValue);
+ }
+ return result;
+ }
+ }
+
+ static class MinimalOptionRenderer implements IOptionRenderer {
+ public Text[][] render(Option option, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) {
+ Text optionText = scheme.optionText(option.names()[0]);
+ Text paramLabelText = parameterLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.optionParamStyles);
+ optionText = optionText.append(paramLabelText);
+ return new Text[][] {{ optionText,
+ scheme.ansi().new Text(option.description().length == 0 ? "" : option.description()[0]) }};
+ }
+ }
+
+ static class MinimalParameterRenderer implements IParameterRenderer {
+ public Text[][] render(Parameters param, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme) {
+ return new Text[][] {{ parameterLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.parameterStyles),
+ scheme.ansi().new Text(param.description().length == 0 ? "" : param.description()[0]) }};
+ }
+ }
+
+ public interface IParameterRenderer {
+
+ Text[][] render(Parameters parameters, Field field, IParamLabelRenderer parameterLabelRenderer, ColorScheme scheme);
+ }
+
+ static class DefaultParameterRenderer implements IParameterRenderer {
+ public String requiredMarker = " ";
+ public Text[][] render(Parameters params, Field field, IParamLabelRenderer paramLabelRenderer, ColorScheme scheme) {
+ Text label = paramLabelRenderer.renderParameterLabel(field, scheme.ansi(), scheme.parameterStyles);
+ Text requiredParameter = scheme.parameterText(Range.parameterArity(field).min > 0 ? requiredMarker : "");
+
+ final int COLUMN_COUNT = 5;
+ final Text EMPTY = Ansi.EMPTY_TEXT;
+ Text[][] result = new Text[Math.max(1, params.description().length)][COLUMN_COUNT];
+ result[0] = new Text[] { requiredParameter, EMPTY, EMPTY, label, scheme.ansi().new Text(str(params.description(), 0)) };
+ for (int i = 1; i < params.description().length; i++) {
+ result[i] = new Text[] { EMPTY, EMPTY, EMPTY, EMPTY, scheme.ansi().new Text(params.description()[i]) };
+ }
+ return result;
+ }
+ }
+
+ public interface IParamLabelRenderer {
+
+
+ Text renderParameterLabel(Field field, Ansi ansi, List<IStyle> styles);
+
+
+ String separator();
+ }
+
+ static class DefaultParamLabelRenderer implements IParamLabelRenderer {
+
+ public final String separator;
+
+ public DefaultParamLabelRenderer(String separator) {
+ this.separator = Assert.notNull(separator, "separator");
+ }
+ public String separator() { return separator; }
+ public Text renderParameterLabel(Field field, Ansi ansi, List<IStyle> styles) {
+ boolean isOptionParameter = field.isAnnotationPresent(Option.class);
+ Range arity = isOptionParameter ? Range.optionArity(field) : Range.parameterArity(field);
+ Text result = ansi.new Text("");
+ String sep = isOptionParameter ? separator : "";
+ if (arity.min > 0) {
+ for (int i = 0; i < arity.min; i++) {
+ result = result.append(sep).append(ansi.apply(renderParameterName(field), styles));
+ sep = " ";
+ }
+ }
+ if (arity.max > arity.min) {
+ sep = result.length == 0 ? (isOptionParameter ? separator : "") : " ";
+ int max = arity.isVariable ? 1 : arity.max - arity.min;
+ for (int i = 0; i < max; i++) {
+ if (sep.trim().length() == 0) {
+ result = result.append(sep + "[").append(ansi.apply(renderParameterName(field), styles));
+ } else {
+ result = result.append("[" + sep).append(ansi.apply(renderParameterName(field), styles));
+ }
+ sep = " ";
+ }
+ if (arity.isVariable) {
+ result = result.append("...");
+ }
+ for (int i = 0; i < max; i++) { result = result.append("]"); }
+ }
+ return result;
+ }
+ private String renderParameterName(Field field) {
+ String result = null;
+ if (field.isAnnotationPresent(Option.class)) {
+ result = field.getAnnotation(Option.class).paramLabel();
+ } else if (field.isAnnotationPresent(Parameters.class)) {
+ result = field.getAnnotation(Parameters.class).paramLabel();
+ }
+ if (result != null && result.trim().length() > 0) {
+ return result.trim();
+ }
+ return "<" + field.getName() + ">";
+ }
+ }
+
+ public static class Layout {
+ protected final ColorScheme colorScheme;
+ protected final TextTable table;
+ protected IOptionRenderer optionRenderer;
+ protected IParameterRenderer parameterRenderer;
+
+
+ public Layout(ColorScheme colorScheme) { this(colorScheme, new TextTable(colorScheme.ansi())); }
+
+
+ public Layout(ColorScheme colorScheme, TextTable textTable) {
+ this(colorScheme, textTable, new DefaultOptionRenderer(), new DefaultParameterRenderer());
+ }
+
+ public Layout(ColorScheme colorScheme, TextTable textTable, IOptionRenderer optionRenderer, IParameterRenderer parameterRenderer) {
+ this.colorScheme = Assert.notNull(colorScheme, "colorScheme");
+ this.table = Assert.notNull(textTable, "textTable");
+ this.optionRenderer = Assert.notNull(optionRenderer, "optionRenderer");
+ this.parameterRenderer = Assert.notNull(parameterRenderer, "parameterRenderer");
+ }
+
+ public void layout(Field field, Text[][] cellValues) {
+ for (Text[] oneRow : cellValues) {
+ table.addRowValues(oneRow);
+ }
+ }
+
+ public void addOptions(List<Field> fields, IParamLabelRenderer paramLabelRenderer) {
+ for (Field field : fields) {
+ Option option = field.getAnnotation(Option.class);
+ if (!option.hidden()) {
+ addOption(field, paramLabelRenderer);
+ }
+ }
+ }
+
+ public void addOption(Field field, IParamLabelRenderer paramLabelRenderer) {
+ Option option = field.getAnnotation(Option.class);
+ Text[][] values = optionRenderer.render(option, field, paramLabelRenderer, colorScheme);
+ layout(field, values);
+ }
+
+ public void addPositionalParameters(List<Field> fields, IParamLabelRenderer paramLabelRenderer) {
+ for (Field field : fields) {
+ Parameters parameters = field.getAnnotation(Parameters.class);
+ if (!parameters.hidden()) {
+ addPositionalParameter(field, paramLabelRenderer);
+ }
+ }
+ }
+
+ public void addPositionalParameter(Field field, IParamLabelRenderer paramLabelRenderer) {
+ Parameters option = field.getAnnotation(Parameters.class);
+ Text[][] values = parameterRenderer.render(option, field, paramLabelRenderer, colorScheme);
+ layout(field, values);
+ }
+
+ @Override public String toString() {
+ return table.toString();
+ }
+ }
+
+ static class ShortestFirst implements Comparator<String> {
+ public int compare(String o1, String o2) {
+ return o1.length() - o2.length();
+ }
+
+ public static String[] sort(String[] names) {
+ Arrays.sort(names, new ShortestFirst());
+ return names;
+ }
+ }
+
+ static class SortByShortestOptionNameAlphabetically implements Comparator<Field> {
+ public int compare(Field f1, Field f2) {
+ Option o1 = f1.getAnnotation(Option.class);
+ Option o2 = f2.getAnnotation(Option.class);
+ if (o1 == null) { return 1; } else if (o2 == null) { return -1; }
+ String[] names1 = ShortestFirst.sort(o1.names());
+ String[] names2 = ShortestFirst.sort(o2.names());
+ int result = names1[0].toUpperCase().compareTo(names2[0].toUpperCase());
+ result = result == 0 ? -names1[0].compareTo(names2[0]) : result;
+ return o1.help() == o2.help() ? result : o2.help() ? -1 : 1;
+ }
+ }
+
+ static class SortByOptionArityAndNameAlphabetically extends SortByShortestOptionNameAlphabetically {
+ public int compare(Field f1, Field f2) {
+ Option o1 = f1.getAnnotation(Option.class);
+ Option o2 = f2.getAnnotation(Option.class);
+ Range arity1 = Range.optionArity(f1);
+ Range arity2 = Range.optionArity(f2);
+ int result = arity1.max - arity2.max;
+ if (result == 0) {
+ result = arity1.min - arity2.min;
+ }
+ return result == 0 ? super.compare(f1, f2) : result;
+ }
+ }
+
+ public static class TextTable {
+
+ public final Column[] columns;
+
+
+ protected final List<Text> columnValues = new ArrayList<Text>();
+
+
+ public int indentWrappedLines = 2;
+
+ private final Ansi ansi;
+
+
+ public TextTable(Ansi ansi) {
+
+ this(ansi, new Column[] {
+ new Column(2, 0, TRUNCATE),
+ new Column(2, 0, TRUNCATE),
+ new Column(1, 0, TRUNCATE),
+ new Column(optionsColumnWidth - 2 - 2 - 1 , 1, SPAN),
+ new Column(usageHelpWidth - optionsColumnWidth, 1, WRAP)
+ });
+ }
+
+
+ public TextTable(Ansi ansi, int... columnWidths) {
+ this.ansi = Assert.notNull(ansi, "ansi");
+ columns = new Column[columnWidths.length];
+ for (int i = 0; i < columnWidths.length; i++) {
+ columns[i] = new Column(columnWidths[i], 0, i == columnWidths.length - 1 ? SPAN: WRAP);
+ }
+ }
+
+ public TextTable(Ansi ansi, Column... columns) {
+ this.ansi = Assert.notNull(ansi, "ansi");
+ this.columns = Assert.notNull(columns, "columns");
+ if (columns.length == 0) { throw new IllegalArgumentException("At least one column is required"); }
+ }
+
+ public Text cellAt(int row, int col) { return columnValues.get(col + (row * columns.length)); }
+
+
+ public int rowCount() { return columnValues.size() / columns.length; }
+
+
+ public void addEmptyRow() {
+ for (int i = 0; i < columns.length; i++) {
+ columnValues.add(ansi.new Text(columns[i].width));
+ }
+ }
+
+
+ public void addRowValues(String... values) {
+ Text[] array = new Text[values.length];
+ for (int i = 0; i < array.length; i++) {
+ array[i] = values[i] == null ? Ansi.EMPTY_TEXT : ansi.new Text(values[i]);
+ }
+ addRowValues(array);
+ }
+
+ public void addRowValues(Text... values) {
+ if (values.length > columns.length) {
+ throw new IllegalArgumentException(values.length + " values don't fit in " +
+ columns.length + " columns");
+ }
+ addEmptyRow();
+ for (int col = 0; col < values.length; col++) {
+ int row = rowCount() - 1;
+ Point cell = putValue(row, col, values[col]);
+
+
+ if ((cell.y != row || cell.x != col) && col != values.length - 1) {
+ addEmptyRow();
+ }
+ }
+ }
+
+ public Point putValue(int row, int col, Text value) {
+ if (row > rowCount() - 1) {
+ throw new IllegalArgumentException("Cannot write to row " + row + ": rowCount=" + rowCount());
+ }
+ if (value == null || value.plain.length() == 0) { return new Point(col, row); }
+ Column column = columns[col];
+ int indent = column.indent;
+ switch (column.overflow) {
+ case TRUNCATE:
+ copy(value, cellAt(row, col), indent);
+ return new Point(col, row);
+ case SPAN:
+ int startColumn = col;
+ do {
+ boolean lastColumn = col == columns.length - 1;
+ int charsWritten = lastColumn
+ ? copy(BreakIterator.getLineInstance(), value, cellAt(row, col), indent)
+ : copy(value, cellAt(row, col), indent);
+ value = value.substring(charsWritten);
+ indent = 0;
+ if (value.length > 0) {
+ ++col;
+ }
+ if (value.length > 0 && col >= columns.length) {
+ addEmptyRow();
+ row++;
+ col = startColumn;
+ indent = column.indent + indentWrappedLines;
+ }
+ } while (value.length > 0);
+ return new Point(col, row);
+ case WRAP:
+ BreakIterator lineBreakIterator = BreakIterator.getLineInstance();
+ do {
+ int charsWritten = copy(lineBreakIterator, value, cellAt(row, col), indent);
+ value = value.substring(charsWritten);
+ indent = column.indent + indentWrappedLines;
+ if (value.length > 0) {
+ ++row;
+ addEmptyRow();
+ }
+ } while (value.length > 0);
+ return new Point(col, row);
+ }
+ throw new IllegalStateException(column.overflow.toString());
+ }
+ private static int length(Text str) {
+ return str.length;
+ }
+ private char[] spaces(int length) { char[] result = new char[length]; Arrays.fill(result, ' '); return result; }
+
+ private int copy(BreakIterator line, Text text, Text columnValue, int offset) {
+
+ line.setText(text.plainString().replace("-", "\u00ff"));
+ int done = 0;
+ for (int start = line.first(), end = line.next(); end != BreakIterator.DONE; start = end, end = line.next()) {
+ Text word = text.substring(start, end);
+ if (columnValue.maxLength >= offset + done + length(word)) {
+ done += copy(word, columnValue, offset + done);
+ } else {
+ break;
+ }
+ }
+ if (done == 0 && length(text) > columnValue.maxLength) {
+
+ done = copy(text, columnValue, offset);
+ }
+ return done;
+ }
+ private static int copy(Text value, Text destination, int offset) {
+ int length = Math.min(value.length, destination.maxLength - offset);
+ value.getStyledChars(value.from, length, destination, offset);
+ return length;
+ }
+
+
+ public StringBuilder toString(StringBuilder text) {
+ int columnCount = this.columns.length;
+ StringBuilder row = new StringBuilder(usageHelpWidth);
+ for (int i = 0; i < columnValues.size(); i++) {
+ Text column = columnValues.get(i);
+ row.append(column.toString());
+ row.append(new String(spaces(columns[i % columnCount].width - column.length)));
+ if (i % columnCount == columnCount - 1) {
+ int lastChar = row.length() - 1;
+ while (lastChar >= 0 && row.charAt(lastChar) == ' ') {lastChar--;}
+ row.setLength(lastChar + 1);
+ text.append(row.toString()).append(System.getProperty("line.separator"));
+ row.setLength(0);
+ }
+ }
+
+ return text;
+ }
+ public String toString() { return toString(new StringBuilder()).toString(); }
+ }
+
+ public static class Column {
+
+
+ public enum Overflow { TRUNCATE, SPAN, WRAP }
+
+
+ public final int width;
+
+
+ public final int indent;
+
+
+ public final Overflow overflow;
+ public Column(int width, int indent, Overflow overflow) {
+ this.width = width;
+ this.indent = indent;
+ this.overflow = Assert.notNull(overflow, "overflow");
+ }
+ }
+
+
+ public static class ColorScheme {
+ public final List<IStyle> commandStyles = new ArrayList<IStyle>();
+ public final List<IStyle> optionStyles = new ArrayList<IStyle>();
+ public final List<IStyle> parameterStyles = new ArrayList<IStyle>();
+ public final List<IStyle> optionParamStyles = new ArrayList<IStyle>();
+ private final Ansi ansi;
+
+
+ public ColorScheme() { this(Ansi.AUTO); }
+
+
+ public ColorScheme(Ansi ansi) {this.ansi = Assert.notNull(ansi, "ansi"); }
+
+
+ public ColorScheme commands(IStyle... styles) { return addAll(commandStyles, styles); }
+
+ public ColorScheme options(IStyle... styles) { return addAll(optionStyles, styles);}
+
+ public ColorScheme parameters(IStyle... styles) { return addAll(parameterStyles, styles);}
+
+ public ColorScheme optionParams(IStyle... styles) { return addAll(optionParamStyles, styles);}
+
+ public Ansi.Text commandText(String command) { return ansi().apply(command, commandStyles); }
+
+ public Ansi.Text optionText(String option) { return ansi().apply(option, optionStyles); }
+
+ public Ansi.Text parameterText(String parameter) { return ansi().apply(parameter, parameterStyles); }
+
+ public Ansi.Text optionParamText(String optionParam) { return ansi().apply(optionParam, optionParamStyles); }
+
+
+ public ColorScheme applySystemProperties() {
+ replace(commandStyles, System.getProperty("picocli.color.commands"));
+ replace(optionStyles, System.getProperty("picocli.color.options"));
+ replace(parameterStyles, System.getProperty("picocli.color.parameters"));
+ replace(optionParamStyles, System.getProperty("picocli.color.optionParams"));
+ return this;
+ }
+ private void replace(List<IStyle> styles, String property) {
+ if (property != null) {
+ styles.clear();
+ addAll(styles, Style.parse(property));
+ }
+ }
+ private ColorScheme addAll(List<IStyle> styles, IStyle... add) {
+ styles.addAll(Arrays.asList(add));
+ return this;
+ }
+
+ public Ansi ansi() {
+ return ansi;
+ }
+ }
+
+
+ public static ColorScheme defaultColorScheme(Ansi ansi) {
+ return new ColorScheme(ansi)
+ .commands(Style.bold)
+ .options(Style.fg_yellow)
+ .parameters(Style.fg_yellow)
+ .optionParams(Style.italic);
+ }
+
+
+ public enum Ansi {
+
+ AUTO,
+
+ ON,
+
+ OFF;
+ static Text EMPTY_TEXT = OFF.new Text(0);
+ static final boolean isWindows = System.getProperty("os.name").startsWith("Windows");
+ static final boolean isXterm = System.getenv("TERM") != null && System.getenv("TERM").startsWith("xterm");
+ static final boolean ISATTY = calcTTY();
+
+
+ static final boolean calcTTY() {
+ if (isWindows && isXterm) { return true; }
+ try { return System.class.getDeclaredMethod("console").invoke(null) != null; }
+ catch (Throwable reflectionFailed) { return true; }
+ }
+ private static boolean ansiPossible() { return ISATTY && (!isWindows || isXterm); }
+
+
+ public boolean enabled() {
+ if (this == ON) { return true; }
+ if (this == OFF) { return false; }
+ return (System.getProperty("picocli.ansi") == null ? ansiPossible() : Boolean.getBoolean("picocli.ansi"));
+ }
+
+
+ public interface IStyle {
+
+
+ String CSI = "\u001B[";
+
+
+ String on();
+
+
+ String off();
+ }
+
+
+ public enum Style implements IStyle {
+ reset(0, 0), bold(1, 21), faint(2, 22), italic(3, 23), underline(4, 24), blink(5, 25), reverse(7, 27),
+ fg_black(30, 39), fg_red(31, 39), fg_green(32, 39), fg_yellow(33, 39), fg_blue(34, 39), fg_magenta(35, 39), fg_cyan(36, 39), fg_white(37, 39),
+ bg_black(40, 49), bg_red(41, 49), bg_green(42, 49), bg_yellow(43, 49), bg_blue(44, 49), bg_magenta(45, 49), bg_cyan(46, 49), bg_white(47, 49),
+ ;
+ private final int startCode;
+ private final int endCode;
+
+ Style(int startCode, int endCode) {this.startCode = startCode; this.endCode = endCode; }
+ public String on() { return CSI + startCode + "m"; }
+ public String off() { return CSI + endCode + "m"; }
+
+
+ public static String on(IStyle... styles) {
+ StringBuilder result = new StringBuilder();
+ for (IStyle style : styles) {
+ result.append(style.on());
+ }
+ return result.toString();
+ }
+
+ public static String off(IStyle... styles) {
+ StringBuilder result = new StringBuilder();
+ for (IStyle style : styles) {
+ result.append(style.off());
+ }
+ return result.toString();
+ }
+
+ public static IStyle fg(String str) {
+ try { return Style.valueOf(str.toLowerCase(ENGLISH)); } catch (Exception ignored) {}
+ try { return Style.valueOf("fg_" + str.toLowerCase(ENGLISH)); } catch (Exception ignored) {}
+ return new Palette256Color(true, str);
+ }
+
+ public static IStyle bg(String str) {
+ try { return Style.valueOf(str.toLowerCase(ENGLISH)); } catch (Exception ignored) {}
+ try { return Style.valueOf("bg_" + str.toLowerCase(ENGLISH)); } catch (Exception ignored) {}
+ return new Palette256Color(false, str);
+ }
+
+ public static IStyle[] parse(String commaSeparatedCodes) {
+ String[] codes = commaSeparatedCodes.split(",");
+ IStyle[] styles = new IStyle[codes.length];
+ for(int i = 0; i < codes.length; ++i) {
+ if (codes[i].toLowerCase(ENGLISH).startsWith("fg(")) {
+ int end = codes[i].indexOf(')');
+ styles[i] = Style.fg(codes[i].substring(3, end < 0 ? codes[i].length() : end));
+ } else if (codes[i].toLowerCase(ENGLISH).startsWith("bg(")) {
+ int end = codes[i].indexOf(')');
+ styles[i] = Style.bg(codes[i].substring(3, end < 0 ? codes[i].length() : end));
+ } else {
+ styles[i] = Style.fg(codes[i]);
+ }
+ }
+ return styles;
+ }
+ }
+
+
+ static class Palette256Color implements IStyle {
+ private final int fgbg;
+ private final int color;
+
+ Palette256Color(boolean foreground, String color) {
+ this.fgbg = foreground ? 38 : 48;
+ String[] rgb = color.split(";");
+ if (rgb.length == 3) {
+ this.color = 16 + 36 * Integer.decode(rgb[0]) + 6 * Integer.decode(rgb[1]) + Integer.decode(rgb[2]);
+ } else {
+ this.color = Integer.decode(color);
+ }
+ }
+ public String on() { return String.format(CSI + "%d;5;%dm", fgbg, color); }
+ public String off() { return CSI + (fgbg + 1) + "m"; }
+ }
+
+
+
+ public Text apply(String plainText, List<IStyle> styles) {
+ if (plainText.length() == 0) { return new Text(0); }
+ Text result = new Text(plainText.length());
+ IStyle[] all = styles.toArray(new IStyle[styles.size()]);
+ result.indexToStyle.put(result.plain.length(), Style.on(all));
+ result.plain.append(plainText);
+ result.length = result.plain.length();
+ reverse(all);
+ result.indexToStyle.put(result.plain.length(), Style.off(all) + Style.reset.off());
+ return result;
+ }
+
+ private static void reverse(Object[] all) {
+ for (int i = 0; i < all.length / 2; i++) {
+ Object temp = all[i];
+ all[i] = all[all.length - i - 1];
+ all[all.length - i - 1] = temp;
+ }
+ }
+
+ public class Text implements Cloneable {
+ private final int maxLength;
+ private int from;
+ private int length;
+ private StringBuilder plain = new StringBuilder();
+ private SortedMap<Integer, String> indexToStyle = new TreeMap<Integer, String>();
+
+
+ public Text(int maxLength) { this.maxLength = maxLength; }
+
+
+ public Text(String input) {
+ maxLength = -1;
+ plain.setLength(0);
+ int i = 0;
+
+ while (true) {
+ int j = input.indexOf("@|", i);
+ if (j == -1) {
+ if (i == 0) {
+ plain.append(input);
+ length = plain.length();
+ return;
+ }
+ plain.append(input.substring(i, input.length()));
+ length = plain.length();
+ return;
+ }
+ plain.append(input.substring(i, j));
+ int k = input.indexOf("|@", j);
+ if (k == -1) {
+ plain.append(input);
+ length = plain.length();
+ return;
+ }
+
+ j += 2;
+ String spec = input.substring(j, k);
+ String[] items = spec.split(" ", 2);
+ if (items.length == 1) {
+ plain.append(input);
+ length = plain.length();
+ return;
+ }
+
+ IStyle[] styles = Style.parse(items[0]);
+ putStyle(plain.length(), Style.on(styles));
+ plain.append(items[1]);
+ reverse(styles);
+ putStyle(plain.length(), Style.off(styles));
+ putStyle(plain.length(), Style.reset.off());
+ i = k + 2;
+ }
+ }
+ private void putStyle(int index, String style) {
+ String existing = indexToStyle.put(index, style);
+ if (existing != null) { indexToStyle.put(index, existing + style); }
+ }
+ public Object clone() {
+ try { return super.clone(); } catch (CloneNotSupportedException e) { throw new IllegalStateException(e); }
+ }
+
+
+ public Text substring(int start) {
+ return substring(start, length);
+ }
+
+
+ public Text substring(int start, int end) {
+ Text result = (Text) clone();
+ result.from = from + start;
+ result.length = end - start;
+ return result;
+ }
+
+ public Text append(String string) {
+ return append(new Text(string));
+ }
+
+
+ public Text append(Text other) {
+ Text result = (Text) clone();
+ result.plain = new StringBuilder(plain.toString().substring(from, from + length));
+ result.from = 0;
+ result.indexToStyle = new TreeMap<Integer, String>();
+ for (Integer index : indexToStyle.keySet()) {
+ result.indexToStyle.put(index - from, indexToStyle.get(index));
+ }
+ result.plain.append(other.plain.toString().substring(other.from, other.from + other.length));
+ for (Integer otherIndex : other.indexToStyle.keySet()) {
+ int index = result.length + otherIndex - other.from;
+ String replaced = result.indexToStyle.put(index, other.indexToStyle.get(otherIndex));
+ if (replaced != null) {
+ result.indexToStyle.put(index, replaced + other.indexToStyle.get(otherIndex));
+ }
+ }
+ result.length = result.plain.length();
+ return result;
+ }
+
+
+ public void getStyledChars(int from, int length, Text destination, int offset) {
+ if (destination.length < offset) {
+ for (int i = destination.length; i < offset; i++) {
+ destination.plain.append(' ');
+ }
+ destination.length = offset;
+ }
+ for (Integer index : indexToStyle.keySet()) {
+ destination.indexToStyle.put(index - from + destination.length, indexToStyle.get(index));
+ }
+ destination.plain.append(plain.toString().substring(from, from + length));
+ destination.length = destination.plain.length();
+ }
+
+ public String plainString() { return plain.toString().substring(from, from + length); }
+
+ public boolean equals(Object obj) { return toString().equals(String.valueOf(obj)); }
+ public int hashCode() { return toString().hashCode(); }
+
+
+ public String toString() {
+ if (!Ansi.this.enabled()) {
+ return plain.toString().substring(from, from + length);
+ }
+ if (length == 0) { return ""; }
+ StringBuilder sb = new StringBuilder(plain.length() + 20 * indexToStyle.size());
+ Integer startStyle = null;
+ Integer endStyle = -1;
+ for (Integer index : indexToStyle.keySet()) {
+ if (index <= from) {
+ startStyle = startStyle == null ? index : null;
+ endStyle = endStyle == null ? index : null;
+ }
+ if (index >= from) {break;}
+ }
+ if (startStyle != null) {
+ sb.append(indexToStyle.get(startStyle));
+ endStyle = startStyle;
+ }
+ int end = Math.min(from + length, plain.length());
+ for (int i = from; i < end; i++) {
+ String style = indexToStyle.get(i);
+ if (style != null) {
+ if (endStyle != null && endStyle != i) {
+ sb.append(style);
+ startStyle = startStyle == null ? i : null;
+ }
+ endStyle = i;
+ }
+ sb.append(plain.charAt(i));
+ }
+ if (startStyle != null) {
+ SortedMap<Integer, String> tailMap = indexToStyle.tailMap(startStyle + 1);
+ if (!tailMap.isEmpty()) {
+ sb.append(indexToStyle.get(tailMap.firstKey()));
+ } else {
+ sb.append(Style.reset.off());
+ }
+ }
+ return sb.toString();
+ }
+ }
+ }
+ }
+
+
+ private static final class Assert {
+
+ static <T> T notNull(T object, String description) {
+ if (object == null) {
+ throw new NullPointerException(description);
+ }
+ return object;
+ }
+ private Assert() {}
+ }
+
+
+ public static class ParameterException extends RuntimeException {
+ private static final long serialVersionUID = 1477112829129763139L;
+ public ParameterException(String msg) {
+ super(msg);
+ }
+
+ public ParameterException(String msg, Exception ex) {
+ super(msg, ex);
+ }
+
+ private static ParameterException create(Exception ex, String arg, int i, String[] args) {
+ String next = args.length < i + 1 ? "" : " " + args[i + 1];
+ String msg = ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage()
+ + " while processing option[" + i + "] '" + arg + next + "': " + ex.toString();
+ return new ParameterException(msg, ex);
+ }
+ }
+
+ public static class MissingParameterException extends ParameterException {
+ private static final long serialVersionUID = 5075678535706338753L;
+ public MissingParameterException(String msg) {
+ super(msg);
+ }
+
+ private static MissingParameterException create(Collection<Field> missing) {
+ if (missing.size() == 1) {
+ return new MissingParameterException("Missing required option '"
+ + missing.iterator().next().getName() + "'");
+ }
+ List<String> names = new ArrayList<String>(missing.size());
+ for (Field field : missing) {
+ names.add(field.getName());
+ }
+ return new MissingParameterException("Missing required options " + names.toString());
+ }
+ }
+
+
+ public static class DuplicateOptionAnnotationsException extends ParameterException {
+ private static final long serialVersionUID = -3355128012575075641L;
+ public DuplicateOptionAnnotationsException(String msg) {
+ super(msg);
+ }
+
+ private static DuplicateOptionAnnotationsException create(String name, Field field1, Field field2) {
+ return new DuplicateOptionAnnotationsException("Option name '" + name + "' is used by both " +
+ field1.getDeclaringClass().getName() + "." + field1.getName() + " and " +
+ field2.getDeclaringClass().getName() + "." + field2.getName());
+ }
+ }
+
+ public static class ParameterIndexGapException extends ParameterException {
+ private static final long serialVersionUID = -1520981133257618319L;
+ public ParameterIndexGapException(String msg) { super(msg); }
+ }
+
+ public static class UnmatchedArgumentException extends ParameterException {
+ private static final long serialVersionUID = -8700426380701452440L;
+ public UnmatchedArgumentException(String msg) { super(msg); }
+ public UnmatchedArgumentException(Stack<String> args) {
+ this("Unmatched argument" + (args.size() == 1 ? " " : "s ") + reverse(args));
+ }
+ }
+
+ public static class OverwrittenOptionException extends ParameterException {
+ private static final long serialVersionUID = 1338029208271055776L;
+ public OverwrittenOptionException(String msg) { super(msg); }
+ }
+
+ public static class MissingTypeConverterException extends ParameterException {
+ private static final long serialVersionUID = -6050931703233083760L;
+ public MissingTypeConverterException(String msg) {
+ super(msg);
+ }
+ }
+}
+