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

com.beust.jcommander.JCommander Maven / Gradle / Ivy

The newest version!
/**
 * 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 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.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;

/**
 * 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 m_descriptions;

  /**
   * The objects that contain fields annotated with @Parameter.
   */
  private List m_objects = Lists.newArrayList();

  /**
   * This field/method will contain whatever command line parameter is not an option.
   * It is expected to be a List.
   */
  private Parameterized m_mainParameter = null;

  /**
   * The object on which we found the main parameter field.
   */
  private Object m_mainParameterObject;

  /**
   * The annotation found on the main parameter field.
   */
  private Parameter m_mainParameterAnnotation;

  private ParameterDescription m_mainParameterDescription;

  /**
   * A set of all the parameterizeds that are required. During the reflection phase,
   * this field receives all the fields that are annotated with required=true
   * and during the parsing phase, all the fields that are assigned a value
   * are removed from it. At the end of the parsing phase, if it's not empty,
   * then some required fields did not receive a value and an exception is
   * thrown.
   */
  private Map m_requiredFields = Maps.newHashMap();

  /**
   * A map of all the parameterized fields/methods.
   */
  private Map 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 m_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 m_parsedCommand;

  /**
   * The name of command or alias as it was passed to the
   * command line
   */
  private String m_parsedAlias;

  private ProgramName m_programName;

  private Comparator m_parameterDescriptionComparator
      = new Comparator() {
        @Override
        public int compare(ParameterDescription p0, ParameterDescription p1) {
          return p0.getLongestName().compareTo(p1.getLongestName());
        }
      };

  private int m_columnSize = 79;

  private boolean m_helpWasSpecified;

  private List m_unknownArgs = Lists.newArrayList();
  private boolean m_acceptUnknownOptions = false;
  
  private static Console m_console;

  /**
   * The factories used to look up string converters.
   */
  private static LinkedList 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 object is an array or is {@link Iterable},
   * the child objects will be added instead.
   */
  // declared final since this is invoked from constructors
  public final void addObject(Object object) {
    if (object instanceof Iterable) {
      // Iterable
      for (Object o : (Iterable) object) {
        m_objects.add(o);
      }
    } else if (object.getClass().isArray()) {
      // Array
      for (Object o : (Object[]) object) {
        m_objects.add(o);
      }
    } else {
      // Single object
      m_objects.add(object);
    }
  }

  /**
   * Sets the {@link ResourceBundle} to use for looking up descriptions.
   * Set this to null to use description text directly.
   */
  // declared final since this is invoked from constructors
  public final void setDescriptionsBundle(ResourceBundle bundle) {
    m_bundle = bundle;
  }

  /**
   * Parse and validate the command line parameters.
   */
  public void parse(String... args) {
    parse(true /* validate */, args);
  }

  /**
   * Parse the command line parameters without validating them.
   */
  public void parseWithoutValidation(String... args) {
    parse(false /* no validation */, args);
  }

  private void parse(boolean validate, String... args) {
    StringBuilder sb = new StringBuilder("Parsing \"");
    sb.append(join(args).append("\"\n  with:").append(join(m_objects.toArray())));
    p(sb.toString());

    if (m_descriptions == null) createDescriptions();
    initializeDefaultValues();
    parseValues(expandArgs(args), validate);
    if (validate) validateOptions();
  }

  private StringBuilder join(Object[] args) {
    StringBuilder result = new StringBuilder();
    for (int i = 0; i < args.length; i++) {
      if (i > 0) result.append(" ");
      result.append(args[i]);
    }
    return result;
  }

  private void initializeDefaultValues() {
    if (m_defaultProvider != null) {
      for (ParameterDescription pd : m_descriptions.values()) {
        initializeDefaultValue(pd);
      }

      for (Map.Entry entry : m_commands.entrySet()) {
        entry.getValue().initializeDefaultValues();
      }
    }
  }

  /**
   * Make sure that all the required parameters have received a value.
   */
  private void validateOptions() {
    // No validation if we found a help parameter
    if (m_helpWasSpecified) {
      return;
    }

    if (! m_requiredFields.isEmpty()) {
      StringBuilder missingFields = new StringBuilder();
      for (ParameterDescription pd : m_requiredFields.values()) {
        missingFields.append(pd.getNames()).append(" ");
      }
      throw new ParameterException("The following "
            + pluralize(m_requiredFields.size(), "option is required: ", "options are required: ")
            + missingFields);
    }

    if (m_mainParameterDescription != null) {
      if (m_mainParameterDescription.getParameter().required() &&
          !m_mainParameterDescription.isAssigned()) {
        throw new ParameterException("Main parameters are required (\""
            + m_mainParameterDescription.getDescription() + "\")");
      }
    }
  }

  private static String pluralize(int quantity, String singular, String plural) {
    return quantity == 1 ? singular : plural;
  }

  /**
   * Expand the command line parameters to take @ parameters into account.
   * When @ is encountered, the content of the file that follows is inserted
   * in the command line.
   *
   * @param originalArgv the original command line parameters
   * @return the new and enriched command line parameters
   */
  private String[] expandArgs(String[] originalArgv) {
    List vResult1 = Lists.newArrayList();

    //
    // Expand @
    //
    for (String arg : originalArgv) {

      if (arg.startsWith("@")) {
        String fileName = arg.substring(1);
        vResult1.addAll(readFile(fileName));
      }
      else {
        List expanded = expandDynamicArg(arg);
        vResult1.addAll(expanded);
      }
    }

    // Expand separators
    //
    List vResult2 = 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);
          }
        } else {
          vResult2.add(arg);
        }
      } else {
        vResult2.add(arg);
      }
    }

    return vResult2.toArray(new String[vResult2.size()]);
  }

  private List expandDynamicArg(String arg) {
    for (ParameterDescription pd : m_descriptions.values()) {
      if (pd.isDynamicParameter()) {
        for (String name : pd.getParameter().names()) {
          if (arg.startsWith(name) && !arg.equals(name)) {
            return Arrays.asList(name, arg.substring(name.length()));
          }
        }
      }
    }

    return Arrays.asList(arg);
  }

  private boolean isOption(String[] args, String arg) {
    String prefixes = getOptionPrefixes(args, arg);
    return arg.length() > 0 && prefixes.indexOf(arg.charAt(0)) >= 0;
  }

  private ParameterDescription getPrefixDescriptionFor(String arg) {
    for (Map.Entry es : m_descriptions.entrySet()) {
      if (arg.startsWith(es.getKey().getName())) return es.getValue();
    }

    return null;
  }

  /**
   * If arg is an option, we can look it up directly, but if it's a value,
   * we need to find the description for the option that precedes it.
   */
  private ParameterDescription getDescriptionFor(String[] args, String arg) {
    ParameterDescription result = getPrefixDescriptionFor(arg);
    if (result != null) return result;

    for (String a : args) {
      ParameterDescription pd = getPrefixDescriptionFor(arg);
      if (pd != null) result = pd;
      if (a.equals(arg)) return result;
    }

    throw new ParameterException("Unknown parameter: " + arg);
  }

  private String getSeparatorFor(String[] args, String arg) {
    ParameterDescription pd = getDescriptionFor(args, arg);

    // Could be null if only main parameters were passed
    if (pd != null) {
      Parameters p = pd.getObject().getClass().getAnnotation(Parameters.class);
      if (p != null) return p.separators();
    }

    return " ";
  }

  private String getOptionPrefixes(String[] args, String arg) {
    ParameterDescription pd = getDescriptionFor(args, arg);

    // Could be null if only main parameters were passed
    if (pd != null) {
      Parameters p = pd.getObject().getClass()
          .getAnnotation(Parameters.class);
      if (p != null) return p.optionPrefixes();
    }
    String result = Parameters.DEFAULT_OPTION_PREFIXES;

    // See if any of the objects contains a @Parameters(optionPrefixes)
    StringBuilder sb = new StringBuilder();
    for (Object o : m_objects) {
      Parameters p = o.getClass().getAnnotation(Parameters.class);
      if (p != null && !Parameters.DEFAULT_OPTION_PREFIXES.equals(p.optionPrefixes())) {
        sb.append(p.optionPrefixes());
      }
    }

    if (! Strings.isStringEmpty(sb.toString())) {
      result = sb.toString();
    }

    return result;
  }

  /**
   * Reads the file specified by filename and returns the file content as a string.
   * End of lines are replaced by a space.
   *
   * @param fileName the command line filename
   * @return the file content as a string.
   */
  private static List readFile(String fileName) {
    List result = Lists.newArrayList();

    try {
      BufferedReader bufRead = new BufferedReader(new FileReader(fileName));

      String line;

      // Read through file one line at time. Print line # and line
      while ((line = bufRead.readLine()) != null) {
        // Allow empty lines in these at files
        if (line.length() > 0) result.add(line);
      }

      bufRead.close();
    }
    catch (IOException e) {
      throw new ParameterException("Could not read file " + fileName + ": " + e);
    }

    return result;
  }

  /**
   * Remove spaces at both ends and handle double quotes.
   */
  private static String trim(String string) {
    String result = string.trim();
    if (result.startsWith("\"") && result.endsWith("\"") && result.length() > 1) {
      result = result.substring(1, result.length() - 1);
    }
    return result;
  }

  /**
   * Create the ParameterDescriptions for all the \@Parameter found.
   */
  private void createDescriptions() {
    m_descriptions = Maps.newHashMap();

    for (Object object : m_objects) {
      addDescription(object);
    }
  }

  private void addDescription(Object object) {
    Class cls = object.getClass();

    List parameterizeds = Parameterized.parseArg(object);
    for (Parameterized parameterized : parameterizeds) {
      WrappedParameter wp = parameterized.getWrappedParameter();
      if (wp != null && wp.getParameter() != null) {
        Parameter annotation = wp.getParameter();
        //
        // @Parameter
        //
        Parameter p = annotation;
        if (p.names().length == 0) {
          p("Found main parameter:" + parameterized);
          if (m_mainParameter != null) {
            throw new ParameterException("Only one @Parameter with no names attribute is"
                + " allowed, found:" + m_mainParameter + " and " + parameterized);
          }
          m_mainParameter = parameterized;
          m_mainParameterObject = object;
          m_mainParameterAnnotation = p;
          m_mainParameterDescription =
              new ParameterDescription(object, p, parameterized, m_bundle, this);
        } else {
          for (String name : p.names()) {
            if (m_descriptions.containsKey(new StringKey(name))) {
              throw new ParameterException("Found the option " + name + " multiple times");
            }
            p("Adding description for " + name);
            ParameterDescription pd =
                new ParameterDescription(object, p, parameterized, m_bundle, this);
            m_fields.put(parameterized, pd);
            m_descriptions.put(new StringKey(name), pd);

            if (p.required()) m_requiredFields.put(parameterized, pd);
          }
        }
      } else if (parameterized.getDelegateAnnotation() != null) {
        //
        // @ParametersDelegate
        //
        Object delegateObject = parameterized.get(object);
        if (delegateObject == null){
          throw new ParameterException("Delegate field '" + parameterized.getName()
              + "' cannot be null.");
        }
        addDescription(delegateObject);
      } else if (wp != null && wp.getDynamicParameter() != null) {
        //
        // @DynamicParameter
        //
        DynamicParameter dp = wp.getDynamicParameter();
        for (String name : dp.names()) {
          if (m_descriptions.containsKey(name)) {
            throw new ParameterException("Found the option " + name + " multiple times");
          }
          p("Adding description for " + name);
          ParameterDescription pd =
              new ParameterDescription(object, dp, parameterized, m_bundle, this);
          m_fields.put(parameterized, pd);
          m_descriptions.put(new StringKey(name), pd);
    
          if (dp.required()) m_requiredFields.put(parameterized, pd);
        }
      }
    }

//    while (!Object.class.equals(cls)) {
//      for (Field f : cls.getDeclaredFields()) {
//        p("Field:" + cls.getSimpleName() + "." + f.getName());
//        f.setAccessible(true);
//        Annotation annotation = f.getAnnotation(Parameter.class);
//        Annotation delegateAnnotation = f.getAnnotation(ParametersDelegate.class);
//        Annotation dynamicParameter = f.getAnnotation(DynamicParameter.class);
//        if (annotation != null) {
//          //
//          // @Parameter
//          //
//          Parameter p = (Parameter) annotation;
//          if (p.names().length == 0) {
//            p("Found main parameter:" + f);
//            if (m_mainParameterField != null) {
//              throw new ParameterException("Only one @Parameter with no names attribute is"
//                  + " allowed, found:" + m_mainParameterField + " and " + f);
//            }
//            m_mainParameterField = parameterized;
//            m_mainParameterObject = object;
//            m_mainParameterAnnotation = p;
//            m_mainParameterDescription = new ParameterDescription(object, p, f, m_bundle, this);
//          } else {
//            for (String name : p.names()) {
//              if (m_descriptions.containsKey(name)) {
//                throw new ParameterException("Found the option " + name + " multiple times");
//              }
//              p("Adding description for " + name);
//              ParameterDescription pd = new ParameterDescription(object, p, f, m_bundle, this);
//              m_fields.put(f, pd);
//              m_descriptions.put(name, pd);
//
//              if (p.required()) m_requiredFields.put(f, pd);
//            }
//          }
//        } else if (delegateAnnotation != null) {
//          //
//          // @ParametersDelegate
//          //
//          try {
//            Object delegateObject = f.get(object);
//            if (delegateObject == null){
//              throw new ParameterException("Delegate field '" + f.getName() + "' cannot be null.");
//            }
//            addDescription(delegateObject);
//          } catch (IllegalAccessException e) {
//          }
//        } else if (dynamicParameter != null) {
//          //
//          // @DynamicParameter
//          //
//          DynamicParameter dp = (DynamicParameter) dynamicParameter;
//          for (String name : dp.names()) {
//            if (m_descriptions.containsKey(name)) {
//              throw new ParameterException("Found the option " + name + " multiple times");
//            }
//            p("Adding description for " + name);
//            ParameterDescription pd = new ParameterDescription(object, dp, f, m_bundle, this);
//            m_fields.put(f, pd);
//            m_descriptions.put(name, pd);
//
//            if (dp.required()) m_requiredFields.put(f, pd);
//          }
//        }
//      }
//      // Traverse the super class until we find Object.class
//      cls = cls.getSuperclass();
//    }
  }

  private void initializeDefaultValue(ParameterDescription pd) {
    for (String optionName : pd.getParameter().names()) {
      String def = m_defaultProvider.getDefaultValueFor(optionName);
      if (def != null) {
        p("Initializing " + optionName + " with default value:" + def);
        pd.addValue(def, true /* default */);
        return;
      }
    }
  }

  /**
   * Main method that parses the values and initializes the fields accordingly.
   */
  private void parseValues(String[] args, boolean validate) {
    // This boolean becomes true if we encounter a command, which indicates we need
    // to stop parsing (the parsing of the command will be done in a sub JCommander
    // object)
    boolean commandParsed = false;
    int i = 0;
    while (i < args.length && ! commandParsed) {
      String arg = args[i];
      String a = trim(arg);
      p("Parsing arg: " + a);

      JCommander jc = findCommandByAlias(arg);
      int increment = 1;
      if (isOption(args, a) && jc == null) {
        //
        // Option
        //
        ParameterDescription pd = findParameterDescription(a);

        if (pd != null) {
          if (pd.getParameter().password()) {
            //
            // Password option, use the Console to retrieve the password
            //
            char[] password = readPassword(pd.getDescription(), pd.getParameter().echoInput());
            pd.addValue(new String(password));
            m_requiredFields.remove(pd.getParameterized());
          } else {
            if (pd.getParameter().variableArity()) {
              //
              // Variable arity?
              //
              increment = processVariableArity(args, i, pd);
            } else {
              //
              // Regular option
              //
              Class fieldType = pd.getParameterized().getType();

              // Boolean, set to true as soon as we see it, unless it specified
              // an arity of 1, in which case we need to read the next value
              if ((fieldType == boolean.class || fieldType == Boolean.class)
                  && pd.getParameter().arity() == -1) {
                pd.addValue("true");
                m_requiredFields.remove(pd.getParameterized());
              } else {
                increment = processFixedArity(args, i, pd, fieldType);
              }
              // If it's a help option, remember for later
              if (pd.isHelp()) {
                m_helpWasSpecified = true;
              }
            }
          }
        } else {
          if (m_acceptUnknownOptions) {
            m_unknownArgs.add(arg);
            i++;
            while (i < args.length && ! isOption(args, args[i])) {
              m_unknownArgs.add(args[i++]);
            }
            increment = 0;
          } else {
            throw new ParameterException("Unknown option: " + arg);
          }
        }
      }
      else {
        //
        // Main parameter
        //
        if (! Strings.isStringEmpty(arg)) {
          if (m_commands.isEmpty()) {
            //
            // Regular (non-command) parsing
            //
            List mp = getMainParameter(arg);
            String value = arg;
            Object convertedValue = value;

            if (m_mainParameter.getGenericType() instanceof ParameterizedType) {
              ParameterizedType p = (ParameterizedType) m_mainParameter.getGenericType();
              Type cls = p.getActualTypeArguments()[0];
              if (cls instanceof Class) {
                convertedValue = convertValue(m_mainParameter, (Class) cls, value);
              }
            }

            ParameterDescription.validateParameter(m_mainParameterDescription,
                m_mainParameterAnnotation.validateWith(),
                "Default", value);

            m_mainParameterDescription.setAssigned(true);
            mp.add(convertedValue);
          }
          else {
            //
            // Command parsing
            //
            if (jc == null && validate) {
                throw new MissingCommandException("Expected a command, got " + arg);
            } else if (jc != null){
                m_parsedCommand = jc.m_programName.m_name;
                m_parsedAlias = arg; //preserve the original form
    
                // Found a valid command, ask it to parse the remainder of the arguments.
                // Setting the boolean commandParsed to true will force the current
                // loop to end.
                jc.parse(subArray(args, i + 1));
                commandParsed = true;
            }
          }
        }
      }
      i += increment;
    }

    // Mark the parameter descriptions held in m_fields as assigned
    for (ParameterDescription parameterDescription : m_descriptions.values()) {
      if (parameterDescription.isAssigned()) {
        m_fields.get(parameterDescription.getParameterized()).setAssigned(true);
      }
    }

  }

  private class DefaultVariableArity implements IVariableArity {

    @Override
    public int processVariableArity(String optionName, String[] options) {
        int i = 0;
        while (i < options.length && !isOption(options, options[i])) {
          i++;
        }
        return i;
    }
  }
  private final IVariableArity DEFAULT_VARIABLE_ARITY = new DefaultVariableArity();

  private int m_verbose = 0;

  private boolean m_caseSensitiveOptions = true;
  private boolean m_allowAbbreviatedOptions = false;

  /**
   * @return the number of options that were processed.
   */
  private int processVariableArity(String[] args, int index, ParameterDescription pd) {
    Object arg = pd.getObject();
    IVariableArity va;
    if (! (arg instanceof IVariableArity)) {
        va = DEFAULT_VARIABLE_ARITY;
    } else {
        va = (IVariableArity) arg;
    }

    List currentArgs = 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);
    }
    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() : "
"; 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 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()) ? "" : def.toString(); out.append("\n" + s(indentCount + 1)) .append("Default: " + (parameter.password()?"********" : displayedDef)); } 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 commands : m_commands.entrySet()) { 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 getParameterDescriptionComparator() { return m_parameterDescriptionComparator; } public void setParameterDescriptionComparator(Comparator c) { m_parameterDescriptionComparator = c; } public void setColumnSize(int columnSize) { m_columnSize = columnSize; } public int getColumnSize() { return m_columnSize; } private void wrapDescription(StringBuilder out, int indent, String description) { int max = getColumnSize(); String[] words = description.split(" "); int current = indent; int i = 0; while (i < words.length) { String word = words[i]; if (word.length() > max || current + word.length() <= max) { out.append(" ").append(word); current += word.length() + 1; } else { out.append("\n").append(s(indent + 1)).append(word); current = indent; } i++; } } /** * @return a Collection of all the \@Parameter annotations found on the * target class. This can be used to display the usage() in a different * format (e.g. HTML). */ public List getParameters() { return new ArrayList(m_fields.values()); } /** * @return the main parameter description or null if none is defined. */ public ParameterDescription getMainParameter() { return m_mainParameterDescription; } private void p(String string) { if (m_verbose > 0 || System.getProperty(JCommander.DEBUG_PROPERTY) != null) { getConsole().println("[JCommander] " + string); } } /** * Define the default provider for this instance. */ public void setDefaultProvider(IDefaultProvider defaultProvider) { m_defaultProvider = defaultProvider; for (Map.Entry entry : m_commands.entrySet()) { entry.getValue().setDefaultProvider(defaultProvider); } } public void addConverterFactory(IStringConverterFactory converterFactory) { CONVERTER_FACTORIES.addFirst(converterFactory); } public Class> findConverter(Class cls) { for (IStringConverterFactory f : CONVERTER_FACTORIES) { Class> result = f.getConverter(cls); if (result != null) return result; } return null; } public Object convertValue(ParameterDescription pd, String value) { return convertValue(pd.getParameterized(), pd.getParameterized().getType(), value); } /** * @param type The type of the actual parameter * @param value The value to convert */ public Object convertValue(Parameterized parameterized, Class type, String value) { Parameter annotation = parameterized.getParameter(); // Do nothing if it's a @DynamicParameter if (annotation == null) return value; Class> converterClass = annotation.converter(); boolean listConverterWasSpecified = annotation.listConverter() != NoConverter.class; // // Try to find a converter on the annotation // if (converterClass == null || converterClass == NoConverter.class) { // If no converter specified and type is enum, used enum values to convert if (type.isEnum()){ converterClass = type; } else { converterClass = findConverter(type); } } if (converterClass == null) { Type elementType = parameterized.findFieldGenericType(); converterClass = elementType != null ? findConverter((Class>) elementType) : StringConverter.class; // Check for enum type parameter if (converterClass == null && Enum.class.isAssignableFrom((Class) elementType)) { converterClass = (Class>) elementType; } } // // // // // Try to find a converter in the factory // // // IStringConverter converter = null; // if (converterClass == null && m_converterFactories != null) { // // Mmmh, javac requires a cast here // converter = (IStringConverter) m_converterFactories.getConverter(type); // } // if (converterClass == null) { // throw new ParameterException("Don't know how to convert " + value // + " to type " + type + " (field: " + field.getName() + ")"); // } IStringConverter converter; Object result = null; try { String[] names = annotation.names(); String optionName = names.length > 0 ? names[0] : "[Main class]"; if (converterClass != null && converterClass.isEnum()) { try { result = Enum.valueOf((Class) converterClass, value.toUpperCase()); } catch (Exception e) { throw new ParameterException("Invalid value for " + optionName + " parameter. Allowed values:" + EnumSet.allOf((Class) converterClass)); } } else { converter = instantiateConverter(optionName, converterClass); if (type.isAssignableFrom(List.class) && parameterized.getGenericType() instanceof ParameterizedType) { // The field is a List if (listConverterWasSpecified) { // If a list converter was specified, pass the value to it // for direct conversion IStringConverter listConverter = instantiateConverter(optionName, annotation.listConverter()); result = listConverter.convert(value); } else { // No list converter: use the single value converter and pass each // parsed value to it individually result = convertToList(value, converter, annotation.splitter()); } } else { result = converter.convert(value); } } } catch (InstantiationException 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 splitterClass) throws InstantiationException, IllegalAccessException { IParameterSplitter splitter = splitterClass.newInstance(); List result = Lists.newArrayList(); for (String param : splitter.split(value)) { result.add(converter.convert(param)); } return result; } private IStringConverter instantiateConverter(String optionName, Class> converterClass) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException { Constructor> ctor = null; Constructor> stringCtor = null; Constructor>[] ctors = (Constructor>[]) converterClass.getDeclaredConstructors(); for (Constructor> c : ctors) { Class[] types = c.getParameterTypes(); if (types.length == 1 && types[0].equals(String.class)) { stringCtor = c; } else if (types.length == 0) { ctor = c; } } IStringConverter result = stringCtor != null ? stringCtor.newInstance(optionName) : (ctor != null ? ctor.newInstance() : null); return result; } /** * Add a command object. */ public void addCommand(String name, 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); ProgramName progName = jc.m_programName; m_commands.put(progName, jc); /* * Register aliases */ //register command name as an alias of itself for reverse lookup //Note: Name clash check is intentionally omitted to resemble the // original behaviour of clashing commands. // Aliases are, however, are strictly checked for name clashes. aliasMap.put(new StringKey(name), progName); for (String a : aliases) { IKey alias = new StringKey(a); //omit pointless aliases to avoid name clash exception if (!alias.equals(name)) { ProgramName mappedName = aliasMap.get(alias); if (mappedName != null && !mappedName.equals(progName)) { throw new ParameterException("Cannot set alias " + alias + " for " + name + " command because it has already been defined for " + mappedName.m_name + " command"); } aliasMap.put(alias, progName); } } } public Map getCommands() { Map res = Maps.newLinkedHashMap(); for (Map.Entry entry : m_commands.entrySet()) { res.put(entry.getKey().m_name, entry.getValue()); } return res; } public String getParsedCommand() { return m_parsedCommand; } /** * The name of the command or the alias in the form it was * passed to the command line. null if no * command or alias was specified. * * @return Name of command or alias passed to command line. If none passed: null. */ public String getParsedAlias() { return m_parsedAlias; } /** * @return n spaces */ private String s(int count) { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) { result.append(" "); } return result.toString(); } /** * @return the objects that JCommander will fill with the result of * parsing the command line. */ public List getObjects() { return m_objects; } private ParameterDescription findParameterDescription(String arg) { return FuzzyMap.findInMap(m_descriptions, new StringKey(arg), m_caseSensitiveOptions, m_allowAbbreviatedOptions); } private JCommander findCommand(ProgramName name) { return FuzzyMap.findInMap(m_commands, name, m_caseSensitiveOptions, m_allowAbbreviatedOptions); // if (! m_caseSensitiveOptions) { // return m_commands.get(name); // } else { // for (ProgramName c : m_commands.keySet()) { // if (c.getName().equalsIgnoreCase(name.getName())) { // return m_commands.get(c); // } // } // } // return null; } private ProgramName findProgramName(String name) { return FuzzyMap.findInMap(aliasMap, new StringKey(name), m_caseSensitiveOptions, m_allowAbbreviatedOptions); } /* * Reverse lookup JCommand object by command's name or its alias */ private JCommander findCommandByAlias(String commandOrAlias) { ProgramName progName = findProgramName(commandOrAlias); if (progName == null) { return null; } JCommander jc = findCommand(progName); if (jc == null) { throw new IllegalStateException( "There appears to be inconsistency in the internal command database. " + " This is likely a bug. Please report."); } return jc; } /** * Encapsulation of either a main application or an individual command. */ private static final class ProgramName implements IKey { private final String m_name; private final List m_aliases; ProgramName(String name, List 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 aliasesIt = m_aliases.iterator(); while (aliasesIt.hasNext()) { sb.append(aliasesIt.next()); if (aliasesIt.hasNext()) { sb.append(","); } } sb.append(")"); } return sb.toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((m_name == null) ? 0 : m_name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ProgramName other = (ProgramName) obj; if (m_name == null) { if (other.m_name != null) return false; } else if (!m_name.equals(other.m_name)) return false; return true; } /* * Important: ProgramName#toString() is used by longestName(Collection) function * to format usage output. */ @Override public String toString() { return getDisplayName(); } } public void setVerbose(int verbose) { m_verbose = verbose; } public void setCaseSensitiveOptions(boolean b) { m_caseSensitiveOptions = b; } public void setAllowAbbreviatedOptions(boolean b) { m_allowAbbreviatedOptions = b; } public void setAcceptUnknownOptions(boolean b) { m_acceptUnknownOptions = b; } public List getUnknownOptions() { return m_unknownArgs; } // public void setCaseSensitiveCommands(boolean b) { // m_caseSensitiveCommands = b; // } }