diff options
Diffstat (limited to 'src/main/java/com/beust/jcommander/JCommander.java')
-rw-r--r-- | src/main/java/com/beust/jcommander/JCommander.java | 3052 |
1 files changed, 1572 insertions, 1480 deletions
diff --git a/src/main/java/com/beust/jcommander/JCommander.java b/src/main/java/com/beust/jcommander/JCommander.java index 2e049a1..59073c6 100644 --- a/src/main/java/com/beust/jcommander/JCommander.java +++ b/src/main/java/com/beust/jcommander/JCommander.java @@ -7,7 +7,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -18,37 +18,19 @@ package com.beust.jcommander; +import com.beust.jcommander.FuzzyMap.IKey; +import com.beust.jcommander.converters.*; +import com.beust.jcommander.internal.*; + import java.io.BufferedReader; -import java.io.FileReader; import java.io.IOException; -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.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.lang.reflect.*; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; import java.util.ResourceBundle; - -import com.beust.jcommander.FuzzyMap.IKey; -import com.beust.jcommander.converters.IParameterSplitter; -import com.beust.jcommander.converters.NoConverter; -import com.beust.jcommander.converters.StringConverter; -import com.beust.jcommander.internal.Console; -import com.beust.jcommander.internal.DefaultConsole; -import com.beust.jcommander.internal.DefaultConverterFactory; -import com.beust.jcommander.internal.JDK6Console; -import com.beust.jcommander.internal.Lists; -import com.beust.jcommander.internal.Maps; -import com.beust.jcommander.internal.Nullable; +import java.util.concurrent.CopyOnWriteArrayList; /** * The main class for JCommander. It's responsible for parsing the object that contains @@ -63,1537 +45,1647 @@ import com.beust.jcommander.internal.Nullable; * @author Cedric Beust <cedric@beust.com> */ public class JCommander { - public static final String DEBUG_PROPERTY = "jcommander.debug"; - - /** - * A map to look up parameter description per option name. - */ - private Map<IKey, ParameterDescription> m_descriptions; - - /** - * The objects that contain fields annotated with @Parameter. - */ - private List<Object> m_objects = Lists.newArrayList(); - - 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<String>. - */ - 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<Parameterized, ParameterDescription> m_requiredFields = Maps.newHashMap(); - - /** - * A map of all the parameterized fields/methods. - */ - private Map<Parameterized, ParameterDescription> m_fields = Maps.newHashMap(); - - 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<ProgramName, JCommander> m_commands = Maps.newLinkedHashMap(); - - /** - * Alias database for reverse lookup - */ - private Map<IKey, ProgramName> aliasMap = Maps.newLinkedHashMap(); - - /** - * 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<? super ParameterDescription> m_parameterDescriptionComparator - = new Comparator<ParameterDescription>() { - @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<String> m_unknownArgs = Lists.newArrayList(); - 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<IStringConverterFactory> CONVERTER_FACTORIES = Lists.newLinkedList(); - - 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 <code>object</code> 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 <code>null</code> 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<ProgramName, JCommander> 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<String> vResult1 = Lists.newArrayList(); - - // - // Expand @ - // - for (String arg : originalArgv) { - - if (arg.startsWith("@")) { - String fileName = arg.substring(1); - vResult1.addAll(readFile(fileName)); - } - else { - List<String> expanded = expandDynamicArg(arg); - vResult1.addAll(expanded); - } - } - - // Expand separators - // - List<String> vResult2 = Lists.newArrayList(); - 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); - } + public static final String DEBUG_PROPERTY = "jcommander.debug"; + + /** + * A map to look up parameter description per option name. + */ + private Map<IKey, ParameterDescription> descriptions; + + /** + * The objects that contain fields annotated with @Parameter. + */ + private List<Object> objects = Lists.newArrayList(); + + private boolean firstTimeMainParameter = true; + + /** + * This field/method will contain whatever command line parameter is not an option. + * It is expected to be a List<String>. + */ + private Parameterized mainParameter = null; + + /** + * The object on which we found the main parameter field. + */ + private Object mainParameterObject; + + /** + * The annotation found on the main parameter field. + */ + private Parameter mainParameterAnnotation; + + private ParameterDescription 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<Parameterized, ParameterDescription> requiredFields = Maps.newHashMap(); + + /** + * A map of all the parameterized fields/methods. + */ + private Map<Parameterized, ParameterDescription> fields = Maps.newHashMap(); + + /** + * List of commands and their instance. + */ + private Map<ProgramName, JCommander> commands = Maps.newLinkedHashMap(); + + /** + * Alias database for reverse lookup + */ + private Map<IKey, ProgramName> aliasMap = Maps.newLinkedHashMap(); + + /** + * The name of the command after the parsing has run. + */ + private String parsedCommand; + + /** + * The name of command or alias as it was passed to the + * command line + */ + private String parsedAlias; + + private ProgramName programName; + + private boolean helpWasSpecified; + + private List<String> unknownArgs = Lists.newArrayList(); + + private static Console console; + + private final Options options; + + /** + * Options shared with sub commands + */ + private static class Options { + + private ResourceBundle bundle; + + /** + * A default provider returns default values for the parameters. + */ + private IDefaultProvider defaultProvider; + + private Comparator<? super ParameterDescription> parameterDescriptionComparator + = new Comparator<ParameterDescription>() { + @Override + public int compare(ParameterDescription p0, ParameterDescription p1) { + Parameter a0 = p0.getParameterAnnotation(); + Parameter a1 = p1.getParameterAnnotation(); + if (a0 != null && a0.order() != -1 && a1 != null && a1.order() != -1) { + return Integer.compare(a0.order(), a1.order()); + } else if (a0 != null && a0.order() != -1) { + return -1; + } else if (a1 != null && a1.order() != -1) { + return 1; + } else { + return p0.getLongestName().compareTo(p1.getLongestName()); + } + } + }; + private int columnSize = 79; + private boolean acceptUnknownOptions = false; + private boolean allowParameterOverwriting = false; + private boolean expandAtSign = true; + private int verbose = 0; + private boolean caseSensitiveOptions = true; + private boolean allowAbbreviatedOptions = false; + /** + * The factories used to look up string converters. + */ + private final List<IStringConverterInstanceFactory> converterInstanceFactories = new CopyOnWriteArrayList<>(); + private Charset atFileCharset = Charset.defaultCharset(); + } + + private JCommander(Options options) { + if (options == null) { + throw new NullPointerException("options"); + } + this.options = options; + addConverterFactory(new DefaultConverterFactory()); + } + + /** + * Creates a new un-configured JCommander object. + */ + public JCommander() { + this(new Options()); + } + + /** + * @param object The arg object expected to contain {@link Parameter} annotations. + */ + public JCommander(Object object) { + this(object, (ResourceBundle) null); + } + + /** + * @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) { + this(object, bundle, (String[]) null); + } + + /** + * @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, @Nullable ResourceBundle bundle, String... args) { + this(); + addObject(object); + if (bundle != null) { + setDescriptionsBundle(bundle); + } + createDescriptions(); + if (args != null) { + parse(args); + } + } + + /** + * @param object The arg object expected to contain {@link Parameter} annotations. + * @param args The arguments to parse (optional). + * + * @deprecated Construct a JCommander instance first and then call parse() on it. + */ + @Deprecated() + public JCommander(Object object, String... args) { + this(object); + parse(args); + } + + /** + * Disables expanding {@code @file}. + * + * JCommander supports the {@code @file} syntax, which allows you to put all your options + * into a file and pass this file as parameter @param expandAtSign whether to expand {@code @file}. + */ + public void setExpandAtSign(boolean expandAtSign) { + options.expandAtSign = expandAtSign; + } + + public static Console getConsole() { + if (console == null) { + try { + Method consoleMethod = System.class.getDeclaredMethod("console"); + Object console = consoleMethod.invoke(null); + JCommander.console = new JDK6Console(console); + } catch (Throwable t) { + console = new DefaultConsole(); + } + } + return 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 <code>object</code> 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) { + objects.add(o); + } + } else if (object.getClass().isArray()) { + // Array + for (Object o : (Object[]) object) { + objects.add(o); + } } else { - vResult2.add(arg); + // Single object + objects.add(object); } - } else { - vResult2.add(arg); - } } - return vResult2.toArray(new String[vResult2.size()]); - } + /** + * Sets the {@link ResourceBundle} to use for looking up descriptions. + * Set this to <code>null</code> to use description text directly. + */ + // declared final since this is invoked from constructors + public final void setDescriptionsBundle(ResourceBundle bundle) { + options.bundle = bundle; + } - private List<String> 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())); - } + /** + * Parse and validate the command line parameters. + */ + public void parse(String... args) { + try { + parse(true /* validate */, args); + } catch(ParameterException ex) { + ex.setJCommander(this); + throw ex; } - } } - return Arrays.asList(arg); - } + /** + * Parse the command line parameters without validating them. + */ + public void parseWithoutValidation(String... args) { + parse(false /* no validation */, args); + } - private boolean isOption(String[] args, String arg) { - String prefixes = getOptionPrefixes(args, arg); - return arg.length() > 0 && prefixes.indexOf(arg.charAt(0)) >= 0; - } + private void parse(boolean validate, String... args) { + StringBuilder sb = new StringBuilder("Parsing \""); + sb.append(join(args).append("\"\n with:").append(join(objects.toArray()))); + p(sb.toString()); - private ParameterDescription getPrefixDescriptionFor(String arg) { - for (Map.Entry<IKey, ParameterDescription> es : m_descriptions.entrySet()) { - if (arg.startsWith(es.getKey().getName())) return es.getValue(); + if (descriptions == null) createDescriptions(); + initializeDefaultValues(); + parseValues(expandArgs(args), validate); + if (validate) validateOptions(); } - return null; - } + 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; + } - /** - * 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; + private void initializeDefaultValues() { + if (options.defaultProvider != null) { + for (ParameterDescription pd : descriptions.values()) { + initializeDefaultValue(pd); + } - for (String a : args) { - ParameterDescription pd = getPrefixDescriptionFor(arg); - if (pd != null) result = pd; - if (a.equals(arg)) return result; + for (Map.Entry<ProgramName, JCommander> entry : commands.entrySet()) { + entry.getValue().initializeDefaultValues(); + } + } } - throw new ParameterException("Unknown parameter: " + arg); - } + /** + * Make sure that all the required parameters have received a value. + */ + private void validateOptions() { + // No validation if we found a help parameter + if (helpWasSpecified) { + return; + } - private String getSeparatorFor(String[] args, String arg) { - ParameterDescription pd = getDescriptionFor(args, arg); + if (!requiredFields.isEmpty()) { + List<String> missingFields = new ArrayList<>(); + for (ParameterDescription pd : requiredFields.values()) { + missingFields.add("[" + String.join(" | ", pd.getParameter().names()) + "]"); + } + String message = String.join(", ", missingFields); + throw new ParameterException("The following " + + pluralize(requiredFields.size(), "option is required: ", "options are required: ") + + message); + } + + if (mainParameterDescription != null) { + if (mainParameterDescription.getParameter().required() && + !mainParameterDescription.isAssigned()) { + throw new ParameterException("Main parameters are required (\"" + + mainParameterDescription.getDescription() + "\")"); + } + } + } - // 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(); + private static String pluralize(int quantity, String singular, String plural) { + return quantity == 1 ? singular : plural; } - return " "; - } + /** + * 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<String> vResult1 = Lists.newArrayList(); - private String getOptionPrefixes(String[] args, String arg) { - ParameterDescription pd = getDescriptionFor(args, arg); + // + // Expand @ + // + for (String arg : originalArgv) { - // 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(); + if (arg.startsWith("@") && options.expandAtSign) { + String fileName = arg.substring(1); + vResult1.addAll(readFile(fileName)); + } else { + List<String> expanded = expandDynamicArg(arg); + vResult1.addAll(expanded); + } + } + + // Expand separators + // + List<String> vResult2 = Lists.newArrayList(); + for (String arg : vResult1) { + if (isOption(arg)) { + String sep = getSeparatorFor(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()]); } - 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()); - } + private List<String> expandDynamicArg(String arg) { + for (ParameterDescription pd : 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); } - if (! Strings.isStringEmpty(sb.toString())) { - result = sb.toString(); + private boolean matchArg(String arg, IKey key) { + String kn = options.caseSensitiveOptions + ? key.getName() + : key.getName().toLowerCase(); + if (options.allowAbbreviatedOptions) { + if (kn.startsWith(arg)) return true; + } else { + ParameterDescription pd = descriptions.get(key); + if (pd != null) { + // It's an option. If the option has a separator (e.g. -author==foo) then + // we only do a beginsWith match + String separator = getSeparatorFor(arg); + if (! " ".equals(separator)) { + if (arg.startsWith(kn)) return true; + } else { + if (kn.equals(arg)) return true; + } + } else { + // It's a command do a strict equality check + if (kn.equals(arg)) return true; + } + } + return false; } - return result; - } + private boolean isOption(String passedArg) { + if (options.acceptUnknownOptions) return true; - /** - * 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<String> readFile(String fileName) { - List<String> result = Lists.newArrayList(); + String arg = options.caseSensitiveOptions ? passedArg : passedArg.toLowerCase(); - try { - BufferedReader bufRead = new BufferedReader(new FileReader(fileName)); + for (IKey key : descriptions.keySet()) { + if (matchArg(arg, key)) return true; + } + for (IKey key : commands.keySet()) { + if (matchArg(arg, key)) return true; + } - String line; + return false; + } - // 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); + private ParameterDescription getPrefixDescriptionFor(String arg) { + for (Map.Entry<IKey, ParameterDescription> es : descriptions.entrySet()) { + if (arg.startsWith(es.getKey().getName())) return es.getValue(); } - } - bufRead.close(); + return null; } - catch (IOException e) { - throw new ParameterException("Could not read file " + fileName + ": " + e); + + /** + * 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 arg) { + return getPrefixDescriptionFor(arg); } - return result; - } + private String getSeparatorFor(String arg) { + ParameterDescription pd = getDescriptionFor(arg); - /** - * Remove spaces at both ends and handle double quotes. - */ - private static String trim(String string) { - String result = string.trim(); - if (result.startsWith("\"") && result.endsWith("\"") && result.length() > 1) { - result = result.substring(1, result.length() - 1); + // 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 " "; } - return result; - } - /** - * Create the ParameterDescriptions for all the \@Parameter found. - */ - private void createDescriptions() { - m_descriptions = Maps.newHashMap(); + /** + * 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 List<String> readFile(String fileName) { + List<String> result = Lists.newArrayList(); + + try (BufferedReader bufRead = Files.newBufferedReader(Paths.get(fileName), options.atFileCharset)) { + 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); + } + } + } catch (IOException e) { + throw new ParameterException("Could not read file " + fileName + ": " + e); + } - for (Object object : m_objects) { - addDescription(object); + return result; } - } - private void addDescription(Object object) { - Class<?> cls = object.getClass(); + /** + * Remove spaces at both ends and handle double quotes. + */ + private static String trim(String string) { + String result = string.trim(); + if (result.startsWith("\"") && result.endsWith("\"") && result.length() > 1) { + result = result.substring(1, result.length() - 1); + } + return result; + } - List<Parameterized> 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); + /** + * Create the ParameterDescriptions for all the \@Parameter found. + */ + private void createDescriptions() { + descriptions = Maps.newHashMap(); + + for (Object object : objects) { + addDescription(object); + } + } + + private void addDescription(Object object) { + Class<?> cls = object.getClass(); + + List<Parameterized> 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 (mainParameter != null) { + throw new ParameterException("Only one @Parameter with no names attribute is" + + " allowed, found:" + mainParameter + " and " + parameterized); + } + mainParameter = parameterized; + mainParameterObject = object; + mainParameterAnnotation = p; + mainParameterDescription = + new ParameterDescription(object, p, parameterized, options.bundle, this); + } else { + ParameterDescription pd = + new ParameterDescription(object, p, parameterized, options.bundle, this); + for (String name : p.names()) { + if (descriptions.containsKey(new StringKey(name))) { + throw new ParameterException("Found the option " + name + " multiple times"); + } + p("Adding description for " + name); + fields.put(parameterized, pd); + descriptions.put(new StringKey(name), pd); + + if (p.required()) 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 (descriptions.containsKey(name)) { + throw new ParameterException("Found the option " + name + " multiple times"); + } + p("Adding description for " + name); + ParameterDescription pd = + new ParameterDescription(object, dp, parameterized, options.bundle, this); + fields.put(parameterized, pd); + descriptions.put(new StringKey(name), pd); + + if (dp.required()) requiredFields.put(parameterized, pd); + } + } + } + } + + private void initializeDefaultValue(ParameterDescription pd) { + for (String optionName : pd.getParameter().names()) { + String def = options.defaultProvider.getDefaultValueFor(optionName); + if (def != null) { + p("Initializing " + optionName + " with default value:" + def); + pd.addValue(def, true /* default */); + // remove the parameter from the list of fields to be required + requiredFields.remove(pd.getParameterized()); + 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(a) && jc == null) { + // + // Option + // + ParameterDescription pd = findParameterDescription(a); + + if (pd != null) { + if (pd.getParameter().password()) { + increment = processPassword(args, i, pd, validate); + } else { + if (pd.getParameter().variableArity()) { + // + // Variable arity? + // + increment = processVariableArity(args, i, pd, validate); + } 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) { + // Flip the value this boolean was initialized with + Boolean value = (Boolean) pd.getParameterized().get(pd.getObject()); + pd.addValue(value ? "false" : "true"); + requiredFields.remove(pd.getParameterized()); + } else { + increment = processFixedArity(args, i, pd, validate, fieldType); + } + // If it's a help option, remember for later + if (pd.isHelp()) { + helpWasSpecified = true; + } + } + } + } else { + if (options.acceptUnknownOptions) { + unknownArgs.add(arg); + i++; + while (i < args.length && !isOption(args[i])) { + unknownArgs.add(args[i++]); + } + increment = 0; + } else { + throw new ParameterException("Unknown option: " + arg); + } + } + } else { + // + // Main parameter + // + if ("--".equals(arg) && !isDashDash) { + isDashDash = true; + } + else if (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 (mainParameter.getGenericType() instanceof ParameterizedType) { + ParameterizedType p = (ParameterizedType) mainParameter.getGenericType(); + Type cls = p.getActualTypeArguments()[0]; + if (cls instanceof Class) { + convertedValue = convertValue(mainParameter, (Class) cls, null, value); + } + } + + for(final Class<? extends IParameterValidator> validator : mainParameterAnnotation.validateWith() ) { + ParameterDescription.validateParameter(mainParameterDescription, + validator, + "Default", value); + } + + mainParameterDescription.setAssigned(true); + mp.add(convertedValue); + } else { + // + // Command parsing + // + if (jc == null && validate) { + throw new MissingCommandException("Expected a command, got " + arg, arg); + } else if (jc != null) { + parsedCommand = jc.programName.name; + 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(validate, subArray(args, i + 1)); + commandParsed = true; + } + } + } + i += increment; + } + + // Mark the parameter descriptions held in fields as assigned + for (ParameterDescription parameterDescription : descriptions.values()) { + if (parameterDescription.isAssigned()) { + 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[i])) { + i++; + } + return i; + } + } + + private final IVariableArity DEFAULT_VARIABLE_ARITY = new DefaultVariableArity(); + + private final int determineArity(String[] args, int index, ParameterDescription pd, IVariableArity va) { + List<String> currentArgs = Lists.newArrayList(); + for (int j = index + 1; j < args.length; j++) { + currentArgs.add(args[j]); + } + return va.processVariableArity(pd.getParameter().names()[0], + currentArgs.toArray(new String[0])); + } + + /** + * @return the number of options that were processed. + */ + private int processPassword(String[] args, int index, ParameterDescription pd, boolean validate) { + final int passwordArity = determineArity(args, index, pd, DEFAULT_VARIABLE_ARITY); + if (passwordArity == 0) { + // password option with password not specified, use the Console to retrieve the password + char[] password = readPassword(pd.getDescription(), pd.getParameter().echoInput()); + pd.addValue(new String(password)); + requiredFields.remove(pd.getParameterized()); + return 1; + } else if (passwordArity == 1) { + // password option with password specified + return processFixedArity(args, index, pd, validate, List.class, 1); } 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"); + throw new ParameterException("Password parameter must have at most 1 argument."); + } + } + + /** + * @return the number of options that were processed. + */ + private int processVariableArity(String[] args, int index, ParameterDescription pd, boolean validate) { + Object arg = pd.getObject(); + IVariableArity va; + if (!(arg instanceof IVariableArity)) { + va = DEFAULT_VARIABLE_ARITY; + } else { + va = (IVariableArity) arg; + } + + int arity = determineArity(args, index, pd, va); + int result = processFixedArity(args, index, pd, validate, List.class, arity); + return result; + } + + private int processFixedArity(String[] args, int index, ParameterDescription pd, boolean validate, + 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, validate, fieldType, n); + } + + private int processFixedArity(String[] args, int originalIndex, ParameterDescription pd, boolean validate, + 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))) { + // Flip the value this boolean was initialized with + Boolean value = (Boolean) pd.getParameterized().get(pd.getObject()); + pd.addValue(value ? "false" : "true"); + requiredFields.remove(pd.getParameterized()); + } else if (arity == 0) { + throw new ParameterException("Expected a value after parameter " + arg); + + } else if (index < args.length - 1) { + int offset = "--".equals(args[index + 1]) ? 1 : 0; + + Object finalValue = null; + if (index + arity < args.length) { + for (int j = 1; j <= arity; j++) { + String value = trim(args[index + j + offset]); + finalValue = pd.addValue(arg, value, false, validate, j - 1); + requiredFields.remove(pd.getParameterized()); + } + + if (finalValue != null && validate) { + pd.validateValueParameter(arg, finalValue); + } + index += arity + offset; + } else { + throw new ParameterException("Expected " + arity + " values after " + arg); } - p("Adding description for " + name); - m_fields.put(parameterized, pd); - m_descriptions.put(new StringKey(name), pd); + } 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); + } - if (p.required()) m_requiredFields.put(parameterized, pd); - } + 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 (mainParameter == null) { + throw new ParameterException( + "Was passed main parameter '" + arg + "' but no main parameter was defined in your arg class"); } - } else if (parameterized.getDelegateAnnotation() != null) { + + List<?> result = (List<?>) mainParameter.get(mainParameterObject); + if (result == null) { + result = Lists.newArrayList(); + if (!List.class.isAssignableFrom(mainParameter.getType())) { + throw new ParameterException("Main parameter field " + mainParameter + + " needs to be of type List, not " + mainParameter.getType()); + } + mainParameter.set(mainParameterObject, result); + } + if (firstTimeMainParameter) { + result.clear(); + firstTimeMainParameter = false; + } + return result; + } + + public String getMainParameterDescription() { + if (descriptions == null) createDescriptions(); + return mainParameterAnnotation != null ? mainParameterAnnotation.description() + : null; + } + + /** + * Set the program name (used only in the usage). + */ + public void setProgramName(String name) { + setProgramName(name, new String[0]); + } + + /** + * Get the program name (used only in the usage). + */ + public String getProgramName(){ + return programName == null ? null : programName.getName(); + } + + /** + * Set the program name + * + * @param name program name + * @param aliases aliases to the program name + */ + public void setProgramName(String name, String... aliases) { + programName = new ProgramName(name, 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"); + } + 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 = options.bundle; + } + + if (bundle != null) { + String descriptionKey = p.commandDescriptionKey(); + if (!"".equals(descriptionKey)) { + result = getI18nString(bundle, descriptionKey, 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()); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private JCommander jCommander = new JCommander(); + private String[] args = null; + + public Builder() { + } + + /** + * Adds the provided arg object to the set of objects that this commander + * will parse arguments into. + * + * @param o The arg object expected to contain {@link Parameter} + * annotations. If <code>object</code> is an array or is {@link Iterable}, + * the child objects will be added instead. + */ + public Builder addObject(Object o) { + jCommander.addObject(o); + return this; + } + + /** + * Sets the {@link ResourceBundle} to use for looking up descriptions. + * Set this to <code>null</code> to use description text directly. + */ + public Builder resourceBundle(ResourceBundle bundle) { + jCommander.setDescriptionsBundle(bundle); + return this; + } + + public Builder args(String[] args) { + this.args = args; + return this; + } + + /** + * Disables expanding {@code @file}. + * + * JCommander supports the {@code @file} syntax, which allows you to put all your options + * into a file and pass this file as parameter @param expandAtSign whether to expand {@code @file}. + */ + public Builder expandAtSign(Boolean expand) { + jCommander.setExpandAtSign(expand); + return this; + } + + /** + * Set the program name (used only in the usage). + */ + public Builder programName(String name) { + jCommander.setProgramName(name); + return this; + } + + public Builder columnSize(int columnSize) { + jCommander.setColumnSize(columnSize); + return this; + } + + /** + * Define the default provider for this instance. + */ + public Builder defaultProvider(IDefaultProvider provider) { + jCommander.setDefaultProvider(provider); + return this; + } + + /** + * Adds a factory to lookup string converters. The added factory is used prior to previously added factories. + * @param factory the factory determining string converters + */ + public Builder addConverterFactory(IStringConverterFactory factory) { + jCommander.addConverterFactory(factory); + return this; + } + + public Builder verbose(int verbose) { + jCommander.setVerbose(verbose); + return this; + } + + public Builder allowAbbreviatedOptions(boolean b) { + jCommander.setAllowAbbreviatedOptions(b); + return this; + } + + public Builder acceptUnknownOptions(boolean b) { + jCommander.setAcceptUnknownOptions(b); + return this; + } + + public Builder allowParameterOverwriting(boolean b) { + jCommander.setAllowParameterOverwriting(b); + return this; + } + + public Builder atFileCharset(Charset charset) { + jCommander.setAtFileCharset(charset); + return this; + } + + public Builder addConverterInstanceFactory(IStringConverterInstanceFactory factory) { + jCommander.addConverterInstanceFactory(factory); + return this; + } + + public Builder addCommand(Object command) { + jCommander.addCommand(command); + return this; + } + + public Builder addCommand(String name, Object command, String... aliases) { + jCommander.addCommand(name, command, aliases); + return this; + } + + public JCommander build() { + if (args != null) { + jCommander.parse(args); + } + return jCommander; + } + } + + + /** + * Store the help in the passed string builder. + */ + public void usage(StringBuilder out) { + usage(out, ""); + } + + public void usage(StringBuilder out, String indent) { + if (descriptions == null) createDescriptions(); + boolean hasCommands = !commands.isEmpty(); + boolean hasOptions = !descriptions.isEmpty(); + + //indenting + int descriptionIndent = 6; + int indentCount = indent.length() + descriptionIndent; + // - // @ParametersDelegate + // First line of the usage // - Object delegateObject = parameterized.get(object); - if (delegateObject == null){ - throw new ParameterException("Delegate field '" + parameterized.getName() - + "' cannot be null."); + String programName = this.programName != null ? this.programName.getDisplayName() : "<main class>"; + StringBuilder mainLine = new StringBuilder(); + mainLine.append(indent).append("Usage: ").append(programName); + if (hasOptions) mainLine.append(" [options]"); + if (hasCommands) mainLine.append(indent).append(" [command] [command options]"); + if (mainParameterDescription != null) { + mainLine.append(" ").append(mainParameterDescription.getDescription()); } - addDescription(delegateObject); - } else if (wp != null && wp.getDynamicParameter() != null) { + wrapDescription(out, indentCount, mainLine.toString()); + out.append("\n"); + // - // @DynamicParameter + // Align the descriptions at the "longestName" column // - 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) { + int longestName = 0; + List<ParameterDescription> sorted = Lists.newArrayList(); + for (ParameterDescription pd : 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; + } + } + } + // - // Option + // Sort the options // - ParameterDescription pd = findParameterDescription(a); + Collections.sort(sorted, getParameterDescriptionComparator()); - 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; - } + // + // Display all the names and descriptions + // + 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"); + wrapDescription(out, indentCount, s(indentCount) + pd.getDescription()); + Object def = pd.getDefault(); + if (pd.isDynamicParameter()) { + out.append("\n" + s(indentCount)) + .append("Syntax: " + parameter.names()[0] + + "key" + parameter.getAssignment() + + "value"); } - } - } else { - if (m_acceptUnknownOptions) { - m_unknownArgs.add(arg); - i++; - while (i < args.length && ! isOption(args, args[i])) { - m_unknownArgs.add(args[i++]); + if (def != null && !pd.isHelp()) { + String displayedDef = Strings.isStringEmpty(def.toString()) + ? "<empty string>" + : def.toString(); + out.append("\n" + s(indentCount)) + .append("Default: " + (parameter.password() ? "********" : displayedDef)); + } + Class<?> type = pd.getParameterized().getType(); + if (type.isEnum()) { + out.append("\n" + s(indentCount)) + .append("Possible Values: " + EnumSet.allOf((Class<? extends Enum>) type)); } - increment = 0; - } else { - throw new ParameterException("Unknown option: " + arg); - } + out.append("\n"); } - } - else { + // - // Main parameter + // If commands were specified, show them as well // - 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); - } + if (hasCommands) { + out.append(indent + " Commands:\n"); + // The magic value 3 is the number of spaces between the name of the option + // and its description + for (Map.Entry<ProgramName, JCommander> commands : this.commands.entrySet()) { + Object arg = commands.getValue().getObjects().get(0); + Parameters p = arg.getClass().getAnnotation(Parameters.class); + if (p == null || !p.hidden()) { + ProgramName progName = commands.getKey(); + String dispName = progName.getDisplayName(); + String description = getCommandDescription(progName.getName()); + wrapDescription(out, indentCount + descriptionIndent, + indent + " " + dispName + " " + description); + out.append("\n"); + + // Options for this command + JCommander jc = findCommandByAlias(progName.getName()); + jc.usage(out, indent + " "); + out.append("\n"); + } } + } + } - 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; + private Comparator<? super ParameterDescription> getParameterDescriptionComparator() { + return options.parameterDescriptionComparator; + } + + public void setParameterDescriptionComparator(Comparator<? super ParameterDescription> c) { + options.parameterDescriptionComparator = c; + } + + public void setColumnSize(int columnSize) { + options.columnSize = columnSize; + } + + public int getColumnSize() { + return options.columnSize; + } + + /** + * Wrap a potentially long line to {@link #getColumnSize()}. + * + * @param out the output + * @param indent the indentation in spaces for lines after the first line. + * @param description the text to wrap. No extra spaces are inserted before {@code + * description}. If the first line needs to be indented prepend the + * correct number of spaces to {@code description}. + */ + private void wrapDescription(StringBuilder out, int indent, String description) { + int max = getColumnSize(); + String[] words = description.split(" "); + int current = 0; + int i = 0; + while (i < words.length) { + String word = words[i]; + if (word.length() > max || current + 1 + word.length() <= max) { + out.append(word); + current += word.length(); + if (i != words.length - 1) { + out.append(" "); + current++; + } + } else { + out.append("\n").append(s(indent)).append(word).append(" "); + current = indent + 1 + word.length(); } - } + i++; } - } - 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); - } + /** + * @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<ParameterDescription> getParameters() { + return new ArrayList<>(fields.values()); + } + + /** + * @return the main parameter description or null if none is defined. + */ + public ParameterDescription getMainParameter() { + return mainParameterDescription; } - } + private void p(String string) { + if (options.verbose > 0 || System.getProperty(JCommander.DEBUG_PROPERTY) != null) { + getConsole().println("[JCommander] " + string); + } + } - private class DefaultVariableArity implements IVariableArity { + /** + * Define the default provider for this instance. + */ + public void setDefaultProvider(IDefaultProvider defaultProvider) { + options.defaultProvider = defaultProvider; + } - @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<String> currentArgs = Lists.newArrayList(); - 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 = Lists.newArrayList(); - 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 (used only in the usage). - */ - public void setProgramName(String name) { - setProgramName(name, new String[0]); - } - - /** - * Set the program name - * - * @param name program name - * @param aliases aliases to the program name - */ - public void setProgramName(String name, String... aliases) { - m_programName = new ProgramName(name, 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"); - } - 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) { - if (m_descriptions == null) createDescriptions(); - boolean hasCommands = !m_commands.isEmpty(); - - // - // First line of the usage - // - String programName = m_programName != null ? m_programName.getDisplayName() : "<main class>"; - out.append(indent).append("Usage: " + programName + " [options]"); - if (hasCommands) out.append(indent).append(" [command] [command options]"); - if (m_mainParameterDescription != null) { - out.append(" " + m_mainParameterDescription.getDescription()); - } - out.append("\n"); - - // - // Align the descriptions at the "longestName" column - // - int longestName = 0; - List<ParameterDescription> sorted = Lists.newArrayList(); - 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; - } - } - } - - // - // Sort the options - // - Collections.sort(sorted, getParameterDescriptionComparator()); - - // - // Display all the names and descriptions - // - 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()) - ? "<empty string>" - : def.toString(); - out.append("\n" + s(indentCount + 1)) - .append("Default: " + (parameter.password()?"********" : displayedDef)); - } - Class<?> type = pd.getParameterized().getType(); - if(type.isEnum()){ - out.append("\n" + s(indentCount + 1)) - .append("Possible Values: " + EnumSet.allOf((Class<? extends Enum>) type)); - } - out.append("\n"); - } - - // - // If commands were specified, show them as well - // - if (hasCommands) { - out.append(" Commands:\n"); - // The magic value 3 is the number of spaces between the name of the option - // and its description - for (Map.Entry<ProgramName, JCommander> commands : m_commands.entrySet()) { - Object arg = commands.getValue().getObjects().get(0); - Parameters p = arg.getClass().getAnnotation(Parameters.class); - if (!p.hidden()) { - ProgramName progName = commands.getKey(); - String dispName = progName.getDisplayName(); - out.append(indent).append(" " + dispName); // + s(spaceCount) + getCommandDescription(progName.name) + "\n"); - - // Options for this command - usage(progName.getName(), out, " "); - out.append("\n"); - } - } - } - } - - private Comparator<? super ParameterDescription> getParameterDescriptionComparator() { - return m_parameterDescriptionComparator; - } - - public void setParameterDescriptionComparator(Comparator<? super ParameterDescription> 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<ParameterDescription> getParameters() { - return new ArrayList<ParameterDescription>(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<ProgramName, JCommander> entry : m_commands.entrySet()) { - entry.getValue().setDefaultProvider(defaultProvider); - } - } - - public void addConverterFactory(IStringConverterFactory converterFactory) { - CONVERTER_FACTORIES.addFirst(converterFactory); - } - - public <T> Class<? extends IStringConverter<T>> findConverter(Class<T> cls) { - for (IStringConverterFactory f : CONVERTER_FACTORIES) { - Class<? extends IStringConverter<T>> 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<? extends IStringConverter<?>> 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<? extends IStringConverter<?>>) elementType) - : StringConverter.class; - // Check for enum type parameter - if (converterClass == null && Enum.class.isAssignableFrom((Class) elementType)) { - converterClass = (Class<? extends IStringConverter<?>>) 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()) { + /** + * Adds a factory to lookup string converters. The added factory is used prior to previously added factories. + * @param converterFactory the factory determining string converters + */ + public void addConverterFactory(final IStringConverterFactory converterFactory) { + addConverterInstanceFactory(new IStringConverterInstanceFactory() { + @SuppressWarnings("unchecked") + @Override + public IStringConverter<?> getConverterInstance(Parameter parameter, Class<?> forType, String optionName) { + final Class<? extends IStringConverter<?>> converterClass = converterFactory.getConverter(forType); + try { + if(optionName == null) { + optionName = parameter.names().length > 0 ? parameter.names()[0] : "[Main class]"; + } + return converterClass != null ? instantiateConverter(optionName, converterClass) : null; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new ParameterException(e); + } + } + }); + } + + /** + * Adds a factory to lookup string converters. The added factory is used prior to previously added factories. + * @param converterInstanceFactory the factory generating string converter instances + */ + public void addConverterInstanceFactory(IStringConverterInstanceFactory converterInstanceFactory) { + options.converterInstanceFactories.add(0, converterInstanceFactory); + } + + private IStringConverter<?> findConverterInstance(Parameter parameter, Class<?> forType, String optionName) { + for (IStringConverterInstanceFactory f : options.converterInstanceFactories) { + IStringConverter<?> result = f.getConverterInstance(parameter, forType, optionName); + if (result != null) return result; + } + + return null; + } + + /** + * @param type The type of the actual parameter + * @param optionName + * @param value The value to convert + */ + public Object convertValue(final Parameterized parameterized, Class type, String optionName, String value) { + final Parameter annotation = parameterized.getParameter(); + + // Do nothing if it's a @DynamicParameter + if (annotation == null) return value; + + if(optionName == null) { + optionName = annotation.names().length > 0 ? annotation.names()[0] : "[Main class]"; + } + + IStringConverter<?> converter = null; + if (type.isAssignableFrom(List.class)) { + // If a list converter was specified, pass the value to it for direct conversion + converter = tryInstantiateConverter(optionName, annotation.listConverter()); + } + if (type.isAssignableFrom(List.class) && converter == null) { + // No list converter: use the single value converter and pass each parsed value to it individually + final IParameterSplitter splitter = tryInstantiateConverter(null, annotation.splitter()); + converter = new DefaultListConverter(splitter, new IStringConverter() { + @Override + public Object convert(String value) { + final Type genericType = parameterized.findFieldGenericType(); + return convertValue(parameterized, genericType instanceof Class ? (Class) genericType : String.class, null, value); + } + }); + } + + if (converter == null) { + converter = tryInstantiateConverter(optionName, annotation.converter()); + } + if (converter == null) { + converter = findConverterInstance(annotation, type, optionName); + } + if (converter == null && type.isEnum()) { + converter = new EnumConverter(optionName, type); + } + if (converter == null) { + converter = new StringConverter(); + } + return converter.convert(value); + } + + private static <T> T tryInstantiateConverter(String optionName, Class<T> converterClass) { + if (converterClass == NoConverter.class || converterClass == null) { + return null; + } try { - result = Enum.valueOf((Class<? extends Enum>) converterClass, value); - } catch (IllegalArgumentException e) { - try { - result = Enum.valueOf((Class<? extends Enum>) converterClass, value.toUpperCase()); - } catch (IllegalArgumentException ex) { - throw new ParameterException("Invalid value for " + optionName + " parameter. Allowed values:" + - EnumSet.allOf((Class<? extends Enum>) converterClass)); + return instantiateConverter(optionName, converterClass); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException ignore) { + return null; + } + } + + private static <T> T instantiateConverter(String optionName, Class<? extends T> converterClass) + throws InstantiationException, IllegalAccessException, + InvocationTargetException { + Constructor<T> ctor = null; + Constructor<T> stringCtor = null; + for (Constructor<T> c : (Constructor<T>[]) converterClass.getDeclaredConstructors()) { + c.setAccessible(true); + Class<?>[] types = c.getParameterTypes(); + if (types.length == 1 && types[0].equals(String.class)) { + stringCtor = c; + } else if (types.length == 0) { + ctor = c; + } + } + + return stringCtor != null + ? stringCtor.newInstance(optionName) + : ctor != null + ? ctor.newInstance() + : null; + } + + /** + * Add a command object. + */ + public void addCommand(String name, Object object) { + addCommand(name, object, new String[0]); + } + + public void addCommand(Object object) { + Parameters p = object.getClass().getAnnotation(Parameters.class); + if (p != null && p.commandNames().length > 0) { + for (String commandName : p.commandNames()) { + addCommand(commandName, object); } - } catch (Exception e) { - throw new ParameterException("Invalid value for " + optionName + " parameter. Allowed values:" + - EnumSet.allOf((Class<? extends Enum>) 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 e) { - throw new ParameterException(e); - } catch (IllegalAccessException e) { - throw new ParameterException(e); - } catch (InvocationTargetException 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<? extends IParameterSplitter> splitterClass) - throws InstantiationException, IllegalAccessException { - IParameterSplitter splitter = splitterClass.newInstance(); - List<Object> result = Lists.newArrayList(); - for (String param : splitter.split(value)) { - result.add(converter.convert(param)); - } - return result; - } - - private IStringConverter<?> instantiateConverter(String optionName, - Class<? extends IStringConverter<?>> converterClass) - throws IllegalArgumentException, InstantiationException, IllegalAccessException, - InvocationTargetException { - Constructor<IStringConverter<?>> ctor = null; - Constructor<IStringConverter<?>> stringCtor = null; - Constructor<IStringConverter<?>>[] ctors - = (Constructor<IStringConverter<?>>[]) converterClass.getDeclaredConstructors(); - for (Constructor<IStringConverter<?>> 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, Object object) { - addCommand(name, object, new String[0]); - } - - public void addCommand(Object object) { - Parameters p = object.getClass().getAnnotation(Parameters.class); - if (p != null && p.commandNames().length > 0) { - for (String commandName : p.commandNames()) { - addCommand(commandName, 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, Object object, String... aliases) { - JCommander jc = new JCommander(object); - jc.setProgramName(name, aliases); - jc.setDefaultProvider(m_defaultProvider); - jc.setAcceptUnknownOptions(m_acceptUnknownOptions); - ProgramName progName = jc.m_programName; - m_commands.put(progName, jc); + 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, Object object, String... aliases) { + JCommander jc = new JCommander(options); + jc.addObject(object); + jc.createDescriptions(); + jc.setProgramName(name, aliases); + ProgramName progName = jc.programName; + 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<String, JCommander> getCommands() { - Map<String, JCommander> res = Maps.newLinkedHashMap(); - for (Map.Entry<ProgramName, JCommander> 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. <code>null</code> if no - * command or alias was specified. - * - * @return Name of command or alias passed to command line. If none passed: <code>null</code>. - */ - 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<Object> 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 List<String> m_aliases; - - ProgramName(String name, List<String> aliases) { - m_name = name; - m_aliases = aliases; - } - - @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<String> 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; + //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.name + " command"); + } + aliasMap.put(alias, progName); + } + } + } + + public Map<String, JCommander> getCommands() { + Map<String, JCommander> res = Maps.newLinkedHashMap(); + for (Map.Entry<ProgramName, JCommander> entry : commands.entrySet()) { + res.put(entry.getKey().name, entry.getValue()); + } + return res; + } + + public String getParsedCommand() { + return parsedCommand; + } + + /** + * The name of the command or the alias in the form it was + * passed to the command line. <code>null</code> if no + * command or alias was specified. + * + * @return Name of command or alias passed to command line. If none passed: <code>null</code>. + */ + public String getParsedAlias() { + return 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<Object> getObjects() { + return objects; + } + + private ParameterDescription findParameterDescription(String arg) { + return FuzzyMap.findInMap(descriptions, new StringKey(arg), + options.caseSensitiveOptions, options.allowAbbreviatedOptions); + } + + private JCommander findCommand(ProgramName name) { + return FuzzyMap.findInMap(commands, name, + options.caseSensitiveOptions, options.allowAbbreviatedOptions); + } + + private ProgramName findProgramName(String name) { + return FuzzyMap.findInMap(aliasMap, new StringKey(name), + options.caseSensitiveOptions, options.allowAbbreviatedOptions); } /* - * Important: ProgramName#toString() is used by longestName(Collection) function - * to format usage output. + * 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. */ - @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<String> 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; -// } -} + private static final class ProgramName implements IKey { + private final String name; + private final List<String> aliases; + + ProgramName(String name, List<String> aliases) { + this.name = name; + this.aliases = aliases; + } + + @Override + public String getName() { + return name; + } + + private String getDisplayName() { + StringBuilder sb = new StringBuilder(); + sb.append(name); + if (!aliases.isEmpty()) { + sb.append("("); + Iterator<String> aliasesIt = 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 + ((name == null) ? 0 : 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 (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.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) { + options.verbose = verbose; + } + + public void setCaseSensitiveOptions(boolean b) { + options.caseSensitiveOptions = b; + } + + public void setAllowAbbreviatedOptions(boolean b) { + options.allowAbbreviatedOptions = b; + } + + public void setAcceptUnknownOptions(boolean b) { + options.acceptUnknownOptions = b; + } + + public List<String> getUnknownOptions() { + return unknownArgs; + } + + public void setAllowParameterOverwriting(boolean b) { + options.allowParameterOverwriting = b; + } + + public boolean isParameterOverwritingAllowed() { + return options.allowParameterOverwriting; + } + + /** + * Sets the charset used to expand {@code @files}. + * @param charset the charset + */ + public void setAtFileCharset(Charset charset) { + options.atFileCharset = charset; + } + +} |