All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.carrotsearch.console.jcommander.JCommander Maven / Gradle / Ivy

There is a newer version: 1.1.4
Show newest version
/*
 * console-tools
 *
 * Copyright (C) 2019, Carrot Search s.c.
 * All rights reserved.
 */
package com.carrotsearch.console.jcommander;

import com.carrotsearch.console.jcommander.FuzzyMap.IKey;
import com.carrotsearch.console.jcommander.converters.IParameterSplitter;
import com.carrotsearch.console.jcommander.converters.NoConverter;
import com.carrotsearch.console.jcommander.converters.StringConverter;
import com.carrotsearch.console.jcommander.internal.Console;
import com.carrotsearch.console.jcommander.internal.DefaultConsole;
import com.carrotsearch.console.jcommander.internal.DefaultConverterFactory;
import com.carrotsearch.console.jcommander.internal.JDK6Console;
import com.carrotsearch.console.jcommander.internal.Nullable;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

/**
 * The main class for JCommander. It's responsible for parsing the object that contains all the
 * annotated fields, parse the command line and assign the fields with the correct values and a few
 * other helper methods, such as usage().
 *
 * 

The object(s) you pass in the constructor are expected to have one or more \@Parameter * annotations on them. You can pass either a single object, an array of objects or an instance of * Iterable. In the case of an array or Iterable, JCommander will collect the \@Parameter * annotations from all the objects passed in parameter. * * @author Cedric Beust */ @SuppressWarnings("all") public class JCommander { public static final String DEBUG_PROPERTY = "jcommander.debug"; /** A map to look up parameter description per option name. */ private Map m_descriptions; /** The objects that contain fields annotated with @Parameter. */ private List m_objects = new ArrayList(); private boolean m_firstTimeMainParameter = true; /** * This field/method will contain whatever command line parameter is not an option. It is expected * to be a List. */ private Parameterized m_mainParameter = null; /** The object on which we found the main parameter field. */ private Object m_mainParameterObject; /** The annotation found on the main parameter field. */ private Parameter m_mainParameterAnnotation; private ParameterDescription m_mainParameterDescription; /** * A set of all the parameterizeds that are required. During the reflection phase, this field * receives all the fields that are annotated with required=true and during the parsing phase, all * the fields that are assigned a value are removed from it. At the end of the parsing phase, if * it's not empty, then some required fields did not receive a value and an exception is thrown. */ private Map m_requiredFields = new HashMap(); /** A map of all the parameterized fields/methods. */ private Map m_fields = new HashMap(); private ResourceBundle m_bundle; /** A default provider returns default values for the parameters. */ private IDefaultProvider m_defaultProvider; /** List of commands and their instance. */ private Map m_commands = new LinkedHashMap(); /** Alias database for reverse lookup */ private Map aliasMap = new LinkedHashMap(); /** The name of the command after the parsing has run. */ private String m_parsedCommand; /** The name of command or alias as it was passed to the command line */ private String m_parsedAlias; private ProgramName m_programName; private Comparator m_parameterDescriptionComparator = new Comparator() { @Override public int compare(ParameterDescription p0, ParameterDescription p1) { return p0.getLongestName().compareTo(p1.getLongestName()); } }; private int m_columnSize = 79; private boolean m_helpWasSpecified; private List m_unknownArgs = new ArrayList(); private boolean m_acceptUnknownOptions = false; private boolean m_allowParameterOverwriting = false; private static Console m_console; /** The factories used to look up string converters. */ private static LinkedList CONVERTER_FACTORIES = new LinkedList(); static { CONVERTER_FACTORIES.addFirst(new DefaultConverterFactory()); } ; /** Creates a new un-configured JCommander object. */ public JCommander() {} /** @param object The arg object expected to contain {@link Parameter} annotations. */ public JCommander(Object object) { addObject(object); createDescriptions(); } /** * @param object The arg object expected to contain {@link Parameter} annotations. * @param bundle The bundle to use for the descriptions. Can be null. */ public JCommander(Object object, @Nullable ResourceBundle bundle) { addObject(object); setDescriptionsBundle(bundle); } /** * @param object The arg object expected to contain {@link Parameter} annotations. * @param bundle The bundle to use for the descriptions. Can be null. * @param args The arguments to parse (optional). */ public JCommander(Object object, ResourceBundle bundle, String... args) { addObject(object); setDescriptionsBundle(bundle); parse(args); } /** * @param object The arg object expected to contain {@link Parameter} annotations. * @param args The arguments to parse (optional). */ public JCommander(Object object, String... args) { addObject(object); parse(args); } public static Console getConsole() { if (m_console == null) { try { Method consoleMethod = System.class.getDeclaredMethod("console", new Class[0]); Object console = consoleMethod.invoke(null, new Object[0]); m_console = new JDK6Console(console); } catch (Throwable t) { m_console = new DefaultConsole(); } } return m_console; } /** * Adds the provided arg object to the set of objects that this commander will parse arguments * into. * * @param object The arg object expected to contain {@link Parameter} annotations. If object * is an array or is {@link Iterable}, the child objects will be added instead. */ // declared final since this is invoked from constructors public final void addObject(Object object) { if (object instanceof Iterable) { // Iterable for (Object o : (Iterable) object) { m_objects.add(o); } } else if (object.getClass().isArray()) { // Array for (Object o : (Object[]) object) { m_objects.add(o); } } else { // Single object m_objects.add(object); } } /** * Sets the {@link ResourceBundle} to use for looking up descriptions. Set this to null * to use description text directly. */ // declared final since this is invoked from constructors public final void setDescriptionsBundle(ResourceBundle bundle) { m_bundle = bundle; } /** Parse and validate the command line parameters. */ public void parse(String... args) { parse(true /* validate */, args); } /** Parse the command line parameters without validating them. */ public void parseWithoutValidation(String... args) { parse(false /* no validation */, args); } private void parse(boolean validate, String... args) { StringBuilder sb = new StringBuilder("Parsing \""); sb.append(join(args).append("\"\n with:").append(join(m_objects.toArray()))); p(sb.toString()); if (m_descriptions == null) createDescriptions(); initializeDefaultValues(); parseValues(expandArgs(args), validate); if (validate) validateOptions(); } private StringBuilder join(Object[] args) { StringBuilder result = new StringBuilder(); for (int i = 0; i < args.length; i++) { if (i > 0) result.append(" "); result.append(args[i]); } return result; } private void initializeDefaultValues() { if (m_defaultProvider != null) { for (ParameterDescription pd : m_descriptions.values()) { initializeDefaultValue(pd); } for (Map.Entry entry : m_commands.entrySet()) { entry.getValue().initializeDefaultValues(); } } } /** Make sure that all the required parameters have received a value. */ private void validateOptions() { // No validation if we found a help parameter if (m_helpWasSpecified) { return; } if (!m_requiredFields.isEmpty()) { StringBuilder missingFields = new StringBuilder(); for (ParameterDescription pd : m_requiredFields.values()) { missingFields.append(pd.getNames()).append(" "); } throw new ParameterException( "The following " + pluralize(m_requiredFields.size(), "option is required: ", "options are required: ") + missingFields); } if (m_mainParameterDescription != null) { if (m_mainParameterDescription.getParameter().required() && !m_mainParameterDescription.isAssigned()) { throw new ParameterException( "Main parameters are required (\"" + m_mainParameterDescription.getDescription() + "\")"); } } } private static String pluralize(int quantity, String singular, String plural) { return quantity == 1 ? singular : plural; } /** * Expand the command line parameters to take @ parameters into account. When @ is encountered, * the content of the file that follows is inserted in the command line. * * @param originalArgv the original command line parameters * @return the new and enriched command line parameters */ private String[] expandArgs(String[] originalArgv) { List vResult1 = new ArrayList(); // // Expand @ // for (String arg : originalArgv) { if (arg.startsWith("@")) { String fileName = arg.substring(1); vResult1.addAll(readFile(fileName)); } else { List expanded = expandDynamicArg(arg); vResult1.addAll(expanded); } } // Expand separators // List vResult2 = new ArrayList(); for (int i = 0; i < vResult1.size(); i++) { String arg = vResult1.get(i); String[] v1 = vResult1.toArray(new String[0]); if (isOption(v1, arg)) { String sep = getSeparatorFor(v1, arg); if (!" ".equals(sep)) { String[] sp = arg.split("[" + sep + "]", 2); for (String ssp : sp) { vResult2.add(ssp); } } else { vResult2.add(arg); } } else { vResult2.add(arg); } } return vResult2.toArray(new String[vResult2.size()]); } private List expandDynamicArg(String arg) { for (ParameterDescription pd : m_descriptions.values()) { if (pd.isDynamicParameter()) { for (String name : pd.getParameter().names()) { if (arg.startsWith(name) && !arg.equals(name)) { return Arrays.asList(name, arg.substring(name.length())); } } } } return Arrays.asList(arg); } private boolean isOption(String[] args, String arg) { String prefixes = getOptionPrefixes(args, arg); return arg.length() > 0 && prefixes.indexOf(arg.charAt(0)) >= 0; } private ParameterDescription getPrefixDescriptionFor(String arg) { for (Map.Entry es : m_descriptions.entrySet()) { if (arg.startsWith(es.getKey().getName())) return es.getValue(); } return null; } /** * If arg is an option, we can look it up directly, but if it's a value, we need to find the * description for the option that precedes it. */ private ParameterDescription getDescriptionFor(String[] args, String arg) { ParameterDescription result = getPrefixDescriptionFor(arg); if (result != null) return result; for (String a : args) { ParameterDescription pd = getPrefixDescriptionFor(arg); if (pd != null) result = pd; if (a.equals(arg)) return result; } throw new ParameterException("Unknown parameter: " + arg); } private String getSeparatorFor(String[] args, String arg) { ParameterDescription pd = getDescriptionFor(args, arg); // Could be null if only main parameters were passed if (pd != null) { Parameters p = pd.getObject().getClass().getAnnotation(Parameters.class); if (p != null) return p.separators(); } return " "; } private String getOptionPrefixes(String[] args, String arg) { ParameterDescription pd = getDescriptionFor(args, arg); // Could be null if only main parameters were passed if (pd != null) { Parameters p = pd.getObject().getClass().getAnnotation(Parameters.class); if (p != null) return p.optionPrefixes(); } String result = Parameters.DEFAULT_OPTION_PREFIXES; // See if any of the objects contains a @Parameters(optionPrefixes) StringBuilder sb = new StringBuilder(); for (Object o : m_objects) { Parameters p = o.getClass().getAnnotation(Parameters.class); if (p != null && !Parameters.DEFAULT_OPTION_PREFIXES.equals(p.optionPrefixes())) { sb.append(p.optionPrefixes()); } } if (!Strings.isStringEmpty(sb.toString())) { result = sb.toString(); } return result; } /** * Reads the file specified by filename and returns the file content as a string. End of lines are * replaced by a space. * * @param fileName the command line filename * @return the file content as a string. */ private static List readFile(String fileName) { List result = new ArrayList(); try { BufferedReader bufRead = new BufferedReader(new InputStreamReader(new FileInputStream(fileName), "UTF-8")); String line; // Read through file one line at time. Print line # and line while ((line = bufRead.readLine()) != null) { // Allow empty lines and # comments in these at files if (line.length() > 0 && !line.trim().startsWith("#")) { result.add(line); } } bufRead.close(); } catch (IOException e) { throw new ParameterException("Could not read file " + fileName + ": " + e); } return result; } /** Remove spaces at both ends and handle double quotes. */ private static String trim(String string) { // Do not remove anything return string; // String result = string.trim(); // if (result.startsWith("\"") && result.endsWith("\"") && result.length() > 1) { // result = result.substring(1, result.length() - 1); // } // return result; } /** Create the ParameterDescriptions for all the \@Parameter found. */ private void createDescriptions() { m_descriptions = new HashMap(); for (Object object : m_objects) { addDescription(object); } } private void addDescription(Object object) { Class cls = object.getClass(); List parameterizeds = Parameterized.parseArg(object); for (Parameterized parameterized : parameterizeds) { WrappedParameter wp = parameterized.getWrappedParameter(); if (wp != null && wp.getParameter() != null) { Parameter annotation = wp.getParameter(); // // @Parameter // Parameter p = annotation; if (p.names().length == 0) { p("Found main parameter:" + parameterized); if (m_mainParameter != null) { throw new ParameterException( "Only one @Parameter with no names attribute is" + " allowed, found:" + m_mainParameter + " and " + parameterized); } m_mainParameter = parameterized; m_mainParameterObject = object; m_mainParameterAnnotation = p; m_mainParameterDescription = new ParameterDescription(object, p, parameterized, m_bundle, this); } else { ParameterDescription pd = new ParameterDescription(object, p, parameterized, m_bundle, this); for (String name : p.names()) { if (m_descriptions.containsKey(new StringKey(name))) { throw new ParameterException("Found the option " + name + " multiple times"); } p("Adding description for " + name); m_fields.put(parameterized, pd); m_descriptions.put(new StringKey(name), pd); if (p.required()) m_requiredFields.put(parameterized, pd); } } } else if (parameterized.getDelegateAnnotation() != null) { // // @ParametersDelegate // Object delegateObject = parameterized.get(object); if (delegateObject == null) { throw new ParameterException( "Delegate field '" + parameterized.getName() + "' cannot be null."); } addDescription(delegateObject); } else if (wp != null && wp.getDynamicParameter() != null) { // // @DynamicParameter // DynamicParameter dp = wp.getDynamicParameter(); for (String name : dp.names()) { if (m_descriptions.containsKey(name)) { throw new ParameterException("Found the option " + name + " multiple times"); } p("Adding description for " + name); ParameterDescription pd = new ParameterDescription(object, dp, parameterized, m_bundle, this); m_fields.put(parameterized, pd); m_descriptions.put(new StringKey(name), pd); if (dp.required()) m_requiredFields.put(parameterized, pd); } } } // while (!Object.class.equals(cls)) { // for (Field f : cls.getDeclaredFields()) { // p("Field:" + cls.getSimpleName() + "." + f.getName()); // f.setAccessible(true); // Annotation annotation = f.getAnnotation(Parameter.class); // Annotation delegateAnnotation = f.getAnnotation(ParametersDelegate.class); // Annotation dynamicParameter = f.getAnnotation(DynamicParameter.class); // if (annotation != null) { // // // // @Parameter // // // Parameter p = (Parameter) annotation; // if (p.names().length == 0) { // p("Found main parameter:" + f); // if (m_mainParameterField != null) { // throw new ParameterException("Only one @Parameter with no names attribute is" // + " allowed, found:" + m_mainParameterField + " and " + f); // } // m_mainParameterField = parameterized; // m_mainParameterObject = object; // m_mainParameterAnnotation = p; // m_mainParameterDescription = new ParameterDescription(object, p, f, m_bundle, // this); // } else { // for (String name : p.names()) { // if (m_descriptions.containsKey(name)) { // throw new ParameterException("Found the option " + name + " multiple times"); // } // p("Adding description for " + name); // ParameterDescription pd = new ParameterDescription(object, p, f, m_bundle, // this); // m_fields.put(f, pd); // m_descriptions.put(name, pd); // // if (p.required()) m_requiredFields.put(f, pd); // } // } // } else if (delegateAnnotation != null) { // // // // @ParametersDelegate // // // try { // Object delegateObject = f.get(object); // if (delegateObject == null){ // throw new ParameterException("Delegate field '" + f.getName() + "' cannot be // null."); // } // addDescription(delegateObject); // } catch (IllegalAccessException e) { // } // } else if (dynamicParameter != null) { // // // // @DynamicParameter // // // DynamicParameter dp = (DynamicParameter) dynamicParameter; // for (String name : dp.names()) { // if (m_descriptions.containsKey(name)) { // throw new ParameterException("Found the option " + name + " multiple times"); // } // p("Adding description for " + name); // ParameterDescription pd = new ParameterDescription(object, dp, f, m_bundle, this); // m_fields.put(f, pd); // m_descriptions.put(name, pd); // // if (dp.required()) m_requiredFields.put(f, pd); // } // } // } // // Traverse the super class until we find Object.class // cls = cls.getSuperclass(); // } } private void initializeDefaultValue(ParameterDescription pd) { for (String optionName : pd.getParameter().names()) { String def = m_defaultProvider.getDefaultValueFor(optionName); if (def != null) { p("Initializing " + optionName + " with default value:" + def); pd.addValue(def, true /* default */); return; } } } /** Main method that parses the values and initializes the fields accordingly. */ private void parseValues(String[] args, boolean validate) { // This boolean becomes true if we encounter a command, which indicates we need // to stop parsing (the parsing of the command will be done in a sub JCommander // object) boolean commandParsed = false; int i = 0; boolean isDashDash = false; // once we encounter --, everything goes into the main parameter while (i < args.length && !commandParsed) { String arg = args[i]; String a = trim(arg); args[i] = a; p("Parsing arg: " + a); JCommander jc = findCommandByAlias(arg); int increment = 1; if (!isDashDash && !"--".equals(a) && isOption(args, a) && jc == null) { // // Option // ParameterDescription pd = findParameterDescription(a); if (pd != null) { if (pd.getParameter().password()) { // // Password option, use the Console to retrieve the password // char[] password = readPassword(pd.getDescription(), pd.getParameter().echoInput()); pd.addValue(new String(password)); m_requiredFields.remove(pd.getParameterized()); } else { if (pd.getParameter().variableArity()) { // // Variable arity? // increment = processVariableArity(args, i, pd); } else { // // Regular option // Class fieldType = pd.getParameterized().getType(); // Boolean, set to true as soon as we see it, unless it specified // an arity of 1, in which case we need to read the next value if ((fieldType == boolean.class || fieldType == Boolean.class) && pd.getParameter().arity() == -1) { pd.addValue("true"); m_requiredFields.remove(pd.getParameterized()); } else { increment = processFixedArity(args, i, pd, fieldType); } // If it's a help option, remember for later if (pd.isHelp()) { m_helpWasSpecified = true; } } } } else { if (m_acceptUnknownOptions) { m_unknownArgs.add(arg); i++; while (i < args.length && !isOption(args, args[i])) { m_unknownArgs.add(args[i++]); } increment = 0; } else { throw new ParameterException("Unknown option: " + arg); } } } else { // // Main parameter // if (!Strings.isStringEmpty(arg)) { if ("--".equals(arg)) { isDashDash = true; a = trim(args[++i]); } if (m_commands.isEmpty()) { // // Regular (non-command) parsing // List mp = getMainParameter(arg); String value = a; // If there's a non-quoted version, prefer that one Object convertedValue = value; if (m_mainParameter.getGenericType() instanceof ParameterizedType) { ParameterizedType p = (ParameterizedType) m_mainParameter.getGenericType(); Type cls = p.getActualTypeArguments()[0]; if (cls instanceof Class) { convertedValue = convertValue(m_mainParameter, (Class) cls, value); } } ParameterDescription.validateParameter( m_mainParameterDescription, m_mainParameterAnnotation.validateWith(), "Default", value); m_mainParameterDescription.setAssigned(true); mp.add(convertedValue); } else { // // Command parsing // if (jc == null && validate) { throw new MissingCommandException("Expected a command, got " + arg); } else if (jc != null) { m_parsedCommand = jc.m_programName.m_name; m_parsedAlias = arg; // preserve the original form // Found a valid command, ask it to parse the remainder of the arguments. // Setting the boolean commandParsed to true will force the current // loop to end. jc.parse(subArray(args, i + 1)); commandParsed = true; } } } } i += increment; } // Mark the parameter descriptions held in m_fields as assigned for (ParameterDescription parameterDescription : m_descriptions.values()) { if (parameterDescription.isAssigned()) { m_fields.get(parameterDescription.getParameterized()).setAssigned(true); } } } private class DefaultVariableArity implements IVariableArity { @Override public int processVariableArity(String optionName, String[] options) { int i = 0; while (i < options.length && !isOption(options, options[i])) { i++; } return i; } } private final IVariableArity DEFAULT_VARIABLE_ARITY = new DefaultVariableArity(); private int m_verbose = 0; private boolean m_caseSensitiveOptions = true; private boolean m_allowAbbreviatedOptions = false; /** @return the number of options that were processed. */ private int processVariableArity(String[] args, int index, ParameterDescription pd) { Object arg = pd.getObject(); IVariableArity va; if (!(arg instanceof IVariableArity)) { va = DEFAULT_VARIABLE_ARITY; } else { va = (IVariableArity) arg; } List currentArgs = new ArrayList(); for (int j = index + 1; j < args.length; j++) { currentArgs.add(args[j]); } int arity = va.processVariableArity(pd.getParameter().names()[0], currentArgs.toArray(new String[0])); int result = processFixedArity(args, index, pd, List.class, arity); return result; } private int processFixedArity( String[] args, int index, ParameterDescription pd, Class fieldType) { // Regular parameter, use the arity to tell use how many values // we need to consume int arity = pd.getParameter().arity(); int n = (arity != -1 ? arity : 1); return processFixedArity(args, index, pd, fieldType, n); } private int processFixedArity( String[] args, int originalIndex, ParameterDescription pd, Class fieldType, int arity) { int index = originalIndex; String arg = args[index]; // Special case for boolean parameters of arity 0 if (arity == 0 && (Boolean.class.isAssignableFrom(fieldType) || boolean.class.isAssignableFrom(fieldType))) { pd.addValue("true"); m_requiredFields.remove(pd.getParameterized()); } else if (index < args.length - 1) { int offset = "--".equals(args[index + 1]) ? 1 : 0; if (index + arity < args.length) { for (int j = 1; j <= arity; j++) { pd.addValue(trim(args[index + j + offset])); m_requiredFields.remove(pd.getParameterized()); } index += arity + offset; } else { throw new ParameterException("Expected " + arity + " values after " + arg); } } else { throw new ParameterException("Expected a value after parameter " + arg); } return arity + 1; } /** Invoke Console.readPassword through reflection to avoid depending on Java 6. */ private char[] readPassword(String description, boolean echoInput) { getConsole().print(description + ": "); return getConsole().readPassword(echoInput); } private String[] subArray(String[] args, int index) { int l = args.length - index; String[] result = new String[l]; System.arraycopy(args, index, result, 0, l); return result; } /** * @return the field that's meant to receive all the parameters that are not options. * @param arg the arg that we're about to add (only passed here to output a meaningful error * message). */ private List getMainParameter(String arg) { if (m_mainParameter == null) { throw new ParameterException( "Was passed main parameter '" + arg + "' but no main parameter was defined"); } List result = (List) m_mainParameter.get(m_mainParameterObject); if (result == null) { result = new ArrayList(); if (!List.class.isAssignableFrom(m_mainParameter.getType())) { throw new ParameterException( "Main parameter field " + m_mainParameter + " needs to be of type List, not " + m_mainParameter.getType()); } m_mainParameter.set(m_mainParameterObject, result); } if (m_firstTimeMainParameter) { result.clear(); m_firstTimeMainParameter = false; } return result; } public String getMainParameterDescription() { if (m_descriptions == null) createDescriptions(); return m_mainParameterAnnotation != null ? m_mainParameterAnnotation.description() : null; } // private int longestName(Collection objects) { // int result = 0; // for (Object o : objects) { // int l = o.toString().length(); // if (l > result) result = l; // } // // return result; // } /** * Set the program name * * @param name program name * @param aliases aliases to the program name */ public void setProgramName(String name, String group, String... aliases) { m_programName = new ProgramName(name, group, Arrays.asList(aliases)); } /** Display the usage for this command. */ public void usage(String commandName) { StringBuilder sb = new StringBuilder(); usage(commandName, sb); getConsole().println(sb.toString()); } /** Store the help for the command in the passed string builder. */ public void usage(String commandName, StringBuilder out) { usage(commandName, out, ""); } /** * Store the help for the command in the passed string builder, indenting every line with * "indent". */ public void usage(String commandName, StringBuilder out, String indent) { String description = getCommandDescription(commandName); JCommander jc = findCommandByAlias(commandName); if (description != null) { out.append(indent).append(description); out.append("\n\n"); } jc.usage(out, indent); } /** @return the description of the command. */ public String getCommandDescription(String commandName) { JCommander jc = findCommandByAlias(commandName); if (jc == null) { throw new ParameterException("Asking description for unknown command: " + commandName); } Object arg = jc.getObjects().get(0); Parameters p = arg.getClass().getAnnotation(Parameters.class); ResourceBundle bundle = null; String result = null; if (p != null) { result = p.commandDescription(); String bundleName = p.resourceBundle(); if (!"".equals(bundleName)) { bundle = ResourceBundle.getBundle(bundleName, Locale.getDefault()); } else { bundle = m_bundle; } if (bundle != null) { result = getI18nString(bundle, p.commandDescriptionKey(), p.commandDescription()); } } return result; } /** @return The internationalized version of the string if available, otherwise return def. */ private String getI18nString(ResourceBundle bundle, String key, String def) { String s = bundle != null ? bundle.getString(key) : null; return s != null ? s : def; } /** Display the help on System.out. */ public void usage() { StringBuilder sb = new StringBuilder(); usage(sb); getConsole().println(sb.toString()); } /** Store the help in the passed string builder. */ public void usage(StringBuilder out) { usage(out, ""); } public void usage(StringBuilder out, String indent) { usage(out, indent, UsageOptions.values()); } public void usage(StringBuilder out, String indent, UsageOptions... optionList) { final EnumSet options = EnumSet.noneOf(UsageOptions.class); options.addAll(Arrays.asList(optionList)); if (m_descriptions == null) createDescriptions(); boolean hasCommands = !m_commands.isEmpty(); // // Align the descriptions at the "longestName" column // int longestName = 0; List sorted = new ArrayList(); for (ParameterDescription pd : m_fields.values()) { if (!pd.getParameter().hidden()) { sorted.add(pd); // + to have an extra space between the name and the description int length = pd.getNames().length() + 2; if (length > longestName) { longestName = length; } } } // // First line of the usage // if (options.contains(UsageOptions.DISPLAY_SYNTAX_LINE)) { String programName = m_programName != null ? m_programName.getDisplayName() : null; out.append(indent).append("Usage:"); if (programName != null && !programName.isEmpty()) { out.append(" ").append(programName); } if (!sorted.isEmpty()) { out.append(" [options]"); } if (hasCommands) out.append(indent).append(" [command] [command options]"); if (m_mainParameterDescription != null) { out.append(" " + m_mainParameterDescription.getDescription()); } out.append("\n\n"); } // // Sort the options // Collections.sort(sorted, getParameterDescriptionComparator()); // // Display all the names and descriptions // if (options.contains(UsageOptions.DISPLAY_PARAMETERS)) { int descriptionIndent = 6; if (sorted.size() > 0) out.append(indent).append(" Options:\n"); for (ParameterDescription pd : sorted) { WrappedParameter parameter = pd.getParameter(); out.append(indent) .append( " " + (parameter.required() ? "* " : " ") + pd.getNames() + "\n" + indent + s(descriptionIndent)); int indentCount = indent.length() + descriptionIndent; wrapDescription(out, indentCount, pd.getDescription()); Object def = pd.getDefault(); if (pd.isDynamicParameter()) { out.append("\n" + s(indentCount + 1)) .append( "Syntax: " + parameter.names()[0] + "key" + parameter.getAssignment() + "value"); } if (def != null) { String displayedDef = Strings.isStringEmpty(def.toString()) ? "" : def.toString(); out.append("\n" + s(indentCount + 1)) .append("Default: " + (parameter.password() ? "********" : displayedDef)); } out.append("\n\n"); } } // // If commands were specified, show them as well // if (hasCommands && options.contains(UsageOptions.DISPLAY_COMMANDS)) { // The magic value 3 is the number of spaces between the name of the option // and its description List listOfCommands = new ArrayList(); int maxCommandLength = 0; for (Map.Entry commands : m_commands.entrySet()) { Object arg = commands.getValue().getObjects().get(0); Parameters p = arg.getClass().getAnnotation(Parameters.class); if (!p.hidden() || options.contains(UsageOptions.DISPLAY_COMMANDS_HIDDEN)) { ProgramName cmd = commands.getKey(); String name = cmd.getDisplayName(); maxCommandLength = Math.max(maxCommandLength, Character.codePointCount(name, 0, name.length())); listOfCommands.add(cmd); } } if (options.contains(UsageOptions.SORT_COMMANDS)) { Collections.sort( listOfCommands, new Comparator() { @Override public int compare(ProgramName p1, ProgramName p2) { return p1.getDisplayName().compareToIgnoreCase(p2.getDisplayName()); } }); } Map> commandGroups = new LinkedHashMap>(); if (options.contains(UsageOptions.GROUP_COMMANDS)) { for (ProgramName p : listOfCommands) { String group = p.getGroup(); if (!commandGroups.containsKey(group)) { commandGroups.put(group, new ArrayList()); } commandGroups.get(group).add(p); } } else { commandGroups.put("Commands", listOfCommands); } maxCommandLength += 3; Iterator>> i = commandGroups.entrySet().iterator(); while (i.hasNext()) { Map.Entry> e = i.next(); if (!e.getKey().isEmpty()) { out.append(String.format(Locale.ROOT, " %s:\n", e.getKey())); } for (ProgramName p : e.getValue()) { if (options.contains(UsageOptions.DISPLAY_OPTIONS_FOR_EACH_COMMAND)) { out.append(indent).append(" " + p.getDisplayName()); usage(p.getName(), out, " "); } else { out.append( String.format( Locale.ROOT, "%s%s%-" + maxCommandLength + "s%s", indent, " ", p.getDisplayName(), getCommandDescription(p.getName()))); } out.append("\n"); } if (i.hasNext()) { out.append("\n"); } } } } private Comparator getParameterDescriptionComparator() { return m_parameterDescriptionComparator; } public void setParameterDescriptionComparator(Comparator c) { m_parameterDescriptionComparator = c; } public void setColumnSize(int columnSize) { m_columnSize = columnSize; } public int getColumnSize() { return m_columnSize; } private void wrapDescription(StringBuilder out, int indent, String description) { int max = getColumnSize(); String[] words = description.split(" "); int current = indent; int i = 0; while (i < words.length) { String word = words[i]; if (word.length() > max || current + word.length() <= max) { out.append(" ").append(word); current += word.length() + 1; } else { out.append("\n").append(s(indent + 1)).append(word); current = indent; } i++; } } /** * @return a Collection of all the \@Parameter annotations found on the target class. This can be * used to display the usage() in a different format (e.g. HTML). */ public List getParameters() { return new ArrayList(m_fields.values()); } /** @return the main parameter description or null if none is defined. */ public ParameterDescription getMainParameter() { return m_mainParameterDescription; } private void p(String string) { if (m_verbose > 0 || System.getProperty(JCommander.DEBUG_PROPERTY) != null) { getConsole().println("[JCommander] " + string); } } /** Define the default provider for this instance. */ public void setDefaultProvider(IDefaultProvider defaultProvider) { m_defaultProvider = defaultProvider; for (Map.Entry entry : m_commands.entrySet()) { entry.getValue().setDefaultProvider(defaultProvider); } } public void addConverterFactory(IStringConverterFactory converterFactory) { CONVERTER_FACTORIES.addFirst(converterFactory); } public Class> findConverter(Class cls) { for (IStringConverterFactory f : CONVERTER_FACTORIES) { Class> result = f.getConverter(cls); if (result != null) return result; } return null; } public Object convertValue(ParameterDescription pd, String value) { return convertValue(pd.getParameterized(), pd.getParameterized().getType(), value); } /** * @param type The type of the actual parameter * @param value The value to convert */ public Object convertValue(Parameterized parameterized, Class type, String value) { Parameter annotation = parameterized.getParameter(); // Do nothing if it's a @DynamicParameter if (annotation == null) return value; Class> converterClass = annotation.converter(); boolean listConverterWasSpecified = annotation.listConverter() != NoConverter.class; // // Try to find a converter on the annotation // if (converterClass == null || converterClass == NoConverter.class) { // If no converter specified and type is enum, used enum values to convert if (type.isEnum()) { converterClass = type; } else { converterClass = findConverter(type); } } if (converterClass == null) { Type elementType = parameterized.findFieldGenericType(); converterClass = elementType != null ? findConverter((Class>) elementType) : StringConverter.class; // Check for enum type parameter if (converterClass == null && Enum.class.isAssignableFrom((Class) elementType)) { converterClass = (Class>) elementType; } } IStringConverter converter; Object result = null; try { String[] names = annotation.names(); String optionName = names.length > 0 ? names[0] : "[Main class]"; if (converterClass != null && converterClass.isEnum()) { try { result = Enum.valueOf((Class) converterClass, value); } catch (IllegalArgumentException e) { try { result = Enum.valueOf( (Class) converterClass, value.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ex) { throw new ParameterException( "Invalid value for " + optionName + " parameter. Allowed values:" + EnumSet.allOf((Class) converterClass)); } } catch (Exception e) { throw new ParameterException( "Invalid value for " + optionName + " parameter. Allowed values:" + EnumSet.allOf((Class) converterClass)); } } else { converter = instantiateConverter(optionName, converterClass); if (type.isAssignableFrom(List.class) && parameterized.getGenericType() instanceof ParameterizedType) { // The field is a List if (listConverterWasSpecified) { // If a list converter was specified, pass the value to it // for direct conversion IStringConverter listConverter = instantiateConverter(optionName, annotation.listConverter()); result = listConverter.convert(value); } else { // No list converter: use the single value converter and pass each // parsed value to it individually result = convertToList(value, converter, annotation.splitter()); } } else { result = converter.convert(value); } } } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new ParameterException(e); } return result; } /** * Use the splitter to split the value into multiple values and then convert each of them * individually. */ private Object convertToList( String value, IStringConverter converter, Class splitterClass) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { IParameterSplitter splitter = splitterClass.getConstructor().newInstance(); List result = new ArrayList(); for (String param : splitter.split(value)) { result.add(converter.convert(param)); } return result; } private IStringConverter instantiateConverter( String optionName, Class> converterClass) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor> ctor = null; Constructor> stringCtor = null; Constructor>[] ctors = (Constructor>[]) converterClass.getDeclaredConstructors(); for (Constructor> c : ctors) { Class[] types = c.getParameterTypes(); if (types.length == 1 && types[0].equals(String.class)) { stringCtor = c; } else if (types.length == 0) { ctor = c; } } IStringConverter result = stringCtor != null ? stringCtor.newInstance(optionName) : (ctor != null ? ctor.newInstance() : null); return result; } /** Add a command object. */ public void addCommand(String name, String group, Object object) { addCommand(name, group, object, new String[0]); } public void addCommand(Object object) { Parameters p = object.getClass().getAnnotation(Parameters.class); if (p != null && p.commandNames().length > 0) { String cmdGroup = ""; if (object.getClass().isAnnotationPresent(CommandGroup.class)) { cmdGroup = object.getClass().getAnnotation(CommandGroup.class).value(); } for (String commandName : p.commandNames()) { addCommand(commandName, cmdGroup, object); } } else { throw new ParameterException( "Trying to add command " + object.getClass().getName() + " without specifying its names in @Parameters"); } } /** Add a command object and its aliases. */ public void addCommand(String name, String group, Object object, String... aliases) { JCommander jc = new JCommander(object); jc.setProgramName(name, group, aliases); jc.setDefaultProvider(m_defaultProvider); jc.setAcceptUnknownOptions(m_acceptUnknownOptions); ProgramName progName = jc.m_programName; m_commands.put(progName, jc); /* * Register aliases */ // register command name as an alias of itself for reverse lookup // Note: Name clash check is intentionally omitted to resemble the // original behaviour of clashing commands. // Aliases are, however, are strictly checked for name clashes. aliasMap.put(new StringKey(name), progName); for (String a : aliases) { IKey alias = new StringKey(a); // omit pointless aliases to avoid name clash exception if (!alias.equals(name)) { ProgramName mappedName = aliasMap.get(alias); if (mappedName != null && !mappedName.equals(progName)) { throw new ParameterException( "Cannot set alias " + alias + " for " + name + " command because it has already been defined for " + mappedName.m_name + " command"); } aliasMap.put(alias, progName); } } } public Map getCommands() { Map res = new LinkedHashMap(); for (Map.Entry entry : m_commands.entrySet()) { res.put(entry.getKey().m_name, entry.getValue()); } return res; } public String getParsedCommand() { return m_parsedCommand; } /** * The name of the command or the alias in the form it was passed to the command line. null * if no command or alias was specified. * * @return Name of command or alias passed to command line. If none passed: null. */ public String getParsedAlias() { return m_parsedAlias; } /** @return n spaces */ private String s(int count) { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) { result.append(" "); } return result.toString(); } /** @return the objects that JCommander will fill with the result of parsing the command line. */ public List getObjects() { return m_objects; } private ParameterDescription findParameterDescription(String arg) { return FuzzyMap.findInMap( m_descriptions, new StringKey(arg), m_caseSensitiveOptions, m_allowAbbreviatedOptions); } private JCommander findCommand(ProgramName name) { return FuzzyMap.findInMap(m_commands, name, m_caseSensitiveOptions, m_allowAbbreviatedOptions); // if (! m_caseSensitiveOptions) { // return m_commands.get(name); // } else { // for (ProgramName c : m_commands.keySet()) { // if (c.getName().equalsIgnoreCase(name.getName())) { // return m_commands.get(c); // } // } // } // return null; } private ProgramName findProgramName(String name) { return FuzzyMap.findInMap( aliasMap, new StringKey(name), m_caseSensitiveOptions, m_allowAbbreviatedOptions); } /* * Reverse lookup JCommand object by command's name or its alias */ private JCommander findCommandByAlias(String commandOrAlias) { ProgramName progName = findProgramName(commandOrAlias); if (progName == null) { return null; } JCommander jc = findCommand(progName); if (jc == null) { throw new IllegalStateException( "There appears to be inconsistency in the internal command database. " + " This is likely a bug. Please report."); } return jc; } /** Encapsulation of either a main application or an individual command. */ private static final class ProgramName implements IKey { private final String m_name; private final String group; private final List m_aliases; ProgramName(String name, String group, List aliases) { m_name = name; m_aliases = aliases; this.group = group; } public String getGroup() { return group; } @Override public String getName() { return m_name; } private String getDisplayName() { StringBuilder sb = new StringBuilder(); sb.append(m_name); if (!m_aliases.isEmpty()) { sb.append("("); Iterator aliasesIt = m_aliases.iterator(); while (aliasesIt.hasNext()) { sb.append(aliasesIt.next()); if (aliasesIt.hasNext()) { sb.append(","); } } sb.append(")"); } return sb.toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((m_name == null) ? 0 : m_name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ProgramName other = (ProgramName) obj; if (m_name == null) { if (other.m_name != null) return false; } else if (!m_name.equals(other.m_name)) return false; return true; } /* * Important: ProgramName#toString() is used by longestName(Collection) function * to format usage output. */ @Override public String toString() { return getDisplayName(); } } public void setVerbose(int verbose) { m_verbose = verbose; } public void setCaseSensitiveOptions(boolean b) { m_caseSensitiveOptions = b; } public void setAllowAbbreviatedOptions(boolean b) { m_allowAbbreviatedOptions = b; } public void setAcceptUnknownOptions(boolean b) { m_acceptUnknownOptions = b; } public List getUnknownOptions() { return m_unknownArgs; } public void setAllowParameterOverwriting(boolean b) { m_allowParameterOverwriting = b; } public boolean isParameterOverwritingAllowed() { return m_allowParameterOverwriting; } // public void setCaseSensitiveCommands(boolean b) { // m_caseSensitiveCommands = b; // } }