blob: e0588d1931f0d27a5e128217e7ff93cc541871e0 [file] [log] [blame]
package com.beust.jcommander;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
/**
* The main class for JCommander. It's responsible for parsing the object that contains
* all the annotated fields, parse the command line and assign the fields with the correct
* values and a few other helper methods, such as usage().
*
* The object(s) you pass in the constructor are expected to have one or more
* @Parameter annotations on them. You can pass either a single object, an array of objects
* or an instance of Iterable. In the case of an array or Iterable, JCommander will collect
* the @Parameter annotations from all the objects passed in parameter.
*
* @author cbeust
*/
public class JCommander {
/**
* A map to look up parameter description per option name.
*/
private Map<String, ParameterDescription> m_descriptions;
/**
* The objects that contain fields annotated with @Parameter.
*/
private List<Object> m_objects;
/**
* This field will contain whatever command line parameter is not an option.
* It is expected to be a List<String>.
*/
private Field m_mainParameterField = null;
/**
* The object on which we found the main parameter field.
*/
private Object m_mainParameterObject;
/**
* A set of all the fields 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<Field, ParameterDescription> m_requiredFields = Maps.newHashMap();
/**
* A map of all the annotated fields.
*/
private Map<Field, ParameterDescription> m_fields = Maps.newHashMap();
private ResourceBundle m_bundle;
public JCommander(Object object) {
init(object, null);
}
public JCommander(Object object, ResourceBundle bundle, String... args) {
init(object, bundle);
parse(args);
}
public JCommander(Object object, String... args) {
init(object, null);
parse(args);
}
private void init(Object object, ResourceBundle bundle) {
m_bundle = bundle;
m_objects = Lists.newArrayList();
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);
}
}
/**
* Parse the command line parameters.
*/
public void parse(String... args) {
createDescriptions();
parseValues(expandArgs(args));
validateOptions();
}
/**
* Make sure that all the required parameters have received a value.
*/
private void validateOptions() {
if (! m_requiredFields.isEmpty()) {
StringBuilder missingFields = new StringBuilder();
for (ParameterDescription pd : m_requiredFields.values()) {
missingFields.append(pd.getNames()[0]).append(" ");
}
throw new ParameterException("The following options are required: " + missingFields);
}
}
/**
* 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 static String[] expandArgs(String[] originalArgv) {
List<String> vResult = Lists.newArrayList();
for (String arg : originalArgv) {
if (arg.startsWith("@")) {
String fileName = arg.substring(1);
vResult.addAll(readFile(fileName));
}
else {
vResult.add(arg);
}
}
return vResult.toArray(new String[vResult.size()]);
}
/**
* 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.
*/
public static List<String> readFile(String fileName) {
List<String> 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) {
result.add(line);
}
bufRead.close();
}
catch (IOException e) {
throw new ParameterException("Could not read file " + fileName + ": " + e);
}
return result;
}
/**
* @param string
* @return
*/
private static String trim(String string) {
String result = string.trim();
if (result.startsWith("\"")) {
if (result.endsWith("\"")) {
return result.substring(1, result.length() - 1);
} else {
return result.substring(1);
}
} else {
return result;
}
}
private void createDescriptions() {
m_descriptions = Maps.newHashMap();
for (Object object : m_objects) {
Class<?> cls = object.getClass();
for (Field f : cls.getDeclaredFields()) {
p("Field:" + f.getName());
f.setAccessible(true);
Annotation annotation = f.getAnnotation(Parameter.class);
if (annotation != null) {
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 = f;
m_mainParameterObject = object;
} 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);
m_fields.put(f, pd);
m_descriptions.put(name, pd);
if (p.required()) m_requiredFields.put(f, pd);
}
}
}
}
}
}
private void p(String string) {
if (false) {
System.out.println("[JCommander] " + string);
}
}
private void parseValues(String[] args) {
for (int i = 0; i < args.length; i++) {
String a = trim(args[i]);
if (a.startsWith("-")) {
ParameterDescription pd = m_descriptions.get(a);
if (pd != null) {
Class<?> fieldType = pd.getField().getType();
if (fieldType == boolean.class || fieldType == Boolean.class) {
pd.addValue(Boolean.TRUE);
m_requiredFields.remove(pd.getField());
} else {
int arity = pd.getParameter().arity();
int n = (arity != -1 ? arity : 1);
if (i + n < args.length) {
for (int j = 1; j <= n; j++) {
pd.addValue(trim(args[i + j]));
m_requiredFields.remove(pd.getField());
}
i += n;
} else {
throw new ParameterException(arity + " parameters expected after " + args[i]);
}
}
} else {
throw new ParameterException("Unknown option: " + a);
}
}
else {
if (! isStringEmpty(args[i])) getMainParameter().add(args[i]);
}
}
}
private static boolean isStringEmpty(String s) {
return s == null || "".equals(s);
}
/**
* @return the field that's meant to receive all the parameters that are not options.
*/
private List<String> getMainParameter() {
if (m_mainParameterField == null) {
throw new ParameterException(
"Non option parameters were found but no main parameter was defined");
}
try {
List<String> result = (List<String>) m_mainParameterField.get(m_mainParameterObject);
if (result == null) {
result = Lists.newArrayList();
m_mainParameterField.set(m_mainParameterObject, result);
}
return result;
}
catch(IllegalAccessException ex) {
throw new ParameterException("Couldn't access main parameter: " + ex.getMessage());
}
}
/**
* Display a the help on System.out.
*/
public void usage() {
System.out.println("Usage:");
for (ParameterDescription pd : m_fields.values()) {
StringBuilder sb = new StringBuilder();
for (String n : pd.getParameter().names()) {
sb.append(n).append(" ");
}
System.out.println("\t" + sb.toString() + "\t" + pd.getDescription());
}
}
/**
* @return a Collection of all the @Parameter annotations found on the
* target class. This can be used to display the usage() in a different
* format (e.g. HTML).
*/
public List<ParameterDescription> getParameters() {
return new ArrayList(m_fields.values());
}
}