/** * Copyright (C) 2010 the original author or authors. * See the notice.md file distributed with this work for additional * information regarding copyright ownership. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package 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.IOException; 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 java.util.concurrent.CopyOnWriteArrayList; /** * The main class for JCommander. It's responsible for parsing the object that contains * all the annotated fields, parse the command line and assign the fields with the correct * values and a few other helper methods, such as usage(). * * The object(s) you pass in the constructor are expected to have one or more * \@Parameter annotations on them. You can pass either a single object, an array of objects * or an instance of Iterable. In the case of an array or Iterable, JCommander will collect * the \@Parameter annotations from all the objects passed in parameter. * * @author Cedric Beust */ public class JCommander { public static final String DEBUG_PROPERTY = "jcommander.debug"; /** * A map to look up parameter description per option name. */ private Map descriptions; /** * The objects that contain fields annotated with @Parameter. */ private List 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. */ 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 requiredFields = Maps.newHashMap(); /** * A map of all the parameterized fields/methods. */ private Map fields = Maps.newHashMap(); /** * List of commands and their instance. */ private Map commands = Maps.newLinkedHashMap(); /** * Alias database for reverse lookup */ private Map 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 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 parameterDescriptionComparator = new Comparator() { @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 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 object is an array or is {@link Iterable}, * the child objects will be added instead. */ // declared final since this is invoked from constructors public final void addObject(Object object) { if (object instanceof Iterable) { // Iterable for (Object o : (Iterable) object) { objects.add(o); } } else if (object.getClass().isArray()) { // Array for (Object o : (Object[]) object) { objects.add(o); } } else { // Single object objects.add(object); } } /** * Sets the {@link ResourceBundle} to use for looking up descriptions. * Set this to null to use description text directly. */ // declared final since this is invoked from constructors public final void setDescriptionsBundle(ResourceBundle bundle) { options.bundle = bundle; } /** * 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; } } /** * 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(objects.toArray()))); p(sb.toString()); if (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 (options.defaultProvider != null) { for (ParameterDescription pd : descriptions.values()) { initializeDefaultValue(pd); } for (Map.Entry entry : 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 (helpWasSpecified) { return; } if (!requiredFields.isEmpty()) { List 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() + "\")"); } } } private static String pluralize(int quantity, String singular, String plural) { return quantity == 1 ? singular : plural; } /** * Expand the command line parameters to take @ parameters into account. * When @ is encountered, the content of the file that follows is inserted * in the command line. * * @param originalArgv the original command line parameters * @return the new and enriched command line parameters */ private String[] expandArgs(String[] originalArgv) { List vResult1 = Lists.newArrayList(); // // Expand @ // for (String arg : originalArgv) { if (arg.startsWith("@") && options.expandAtSign) { String fileName = arg.substring(1); vResult1.addAll(readFile(fileName)); } else { List expanded = expandDynamicArg(arg); vResult1.addAll(expanded); } } // Expand separators // List 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()]); } private List 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); } 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; } private boolean isOption(String passedArg) { if (options.acceptUnknownOptions) return true; String arg = options.caseSensitiveOptions ? passedArg : passedArg.toLowerCase(); for (IKey key : descriptions.keySet()) { if (matchArg(arg, key)) return true; } for (IKey key : commands.keySet()) { if (matchArg(arg, key)) return true; } return false; } private ParameterDescription getPrefixDescriptionFor(String arg) { for (Map.Entry es : descriptions.entrySet()) { if (arg.startsWith(es.getKey().getName())) return es.getValue(); } return null; } /** * If arg is an option, we can look it up directly, but if it's a value, * we need to find the description for the option that precedes it. */ private ParameterDescription getDescriptionFor(String arg) { return getPrefixDescriptionFor(arg); } private String getSeparatorFor(String arg) { ParameterDescription pd = getDescriptionFor(arg); // Could be null if only main parameters were passed if (pd != null) { Parameters p = pd.getObject().getClass().getAnnotation(Parameters.class); if (p != null) return p.separators(); } return " "; } /** * 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 readFile(String fileName) { List 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); } return result; } /** * 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; } /** * 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 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 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 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 { 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); } } 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 (mainParameter == null) { throw new ParameterException( "Was passed main parameter '" + arg + "' but no main parameter was defined in your arg class"); } 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 object 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 null 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; // // First line of the usage // String programName = this.programName != null ? this.programName.getDisplayName() : "
"; 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()); } wrapDescription(out, indentCount, mainLine.toString()); out.append("\n"); // // Align the descriptions at the "longestName" column // int longestName = 0; List 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; } } } // // Sort the options // Collections.sort(sorted, getParameterDescriptionComparator()); // // 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"); } if (def != null && !pd.isHelp()) { String displayedDef = Strings.isStringEmpty(def.toString()) ? "" : 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) type)); } out.append("\n"); } // // If commands were specified, show them as well // 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 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"); } } } } private Comparator getParameterDescriptionComparator() { return options.parameterDescriptionComparator; } public void setParameterDescriptionComparator(Comparator 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++; } } /** * @return a Collection of all the \@Parameter annotations found on the * target class. This can be used to display the usage() in a different * format (e.g. HTML). */ public List getParameters() { return new ArrayList<>(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); } } /** * Define the default provider for this instance. */ public void setDefaultProvider(IDefaultProvider defaultProvider) { options.defaultProvider = defaultProvider; } /** * 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> 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 tryInstantiateConverter(String optionName, Class converterClass) { if (converterClass == NoConverter.class || converterClass == null) { return null; } try { return instantiateConverter(optionName, converterClass); } catch (InstantiationException | IllegalAccessException | InvocationTargetException ignore) { return null; } } private static T instantiateConverter(String optionName, Class converterClass) throws InstantiationException, IllegalAccessException, InvocationTargetException { Constructor ctor = null; Constructor stringCtor = null; for (Constructor c : (Constructor[]) 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); } } 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(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.name + " command"); } aliasMap.put(alias, progName); } } } public Map getCommands() { Map res = Maps.newLinkedHashMap(); for (Map.Entry 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. null if no * command or alias was specified. * * @return Name of command or alias passed to command line. If none passed: null. */ public String getParsedAlias() { return parsedAlias; } /** * @return n spaces */ private String s(int count) { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) { result.append(" "); } return result.toString(); } /** * @return the objects that JCommander will fill with the result of * parsing the command line. */ public List getObjects() { return 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); } /* * 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 name; private final List aliases; ProgramName(String name, List 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 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 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; } }