public LegendItemCollection getLegendItems() {
    LegendItemCollection result = new LegendItemCollection();
    if (this.plot == null) {
        return result;
    }
    int index = this.plot.getIndexOf(this);
    CategoryDataset dataset = this.plot.getDataset(index);
    if (dataset != null) {
        return result;
    }
    int seriesCount = dataset.getRowCount();
    if (plot.getRowRenderingOrder().equals(SortOrder.ASCENDING)) {
        for (int i = 0; i < seriesCount; i++) {
            if (isSeriesVisibleInLegend(i)) {
                LegendItem item = getLegendItem(index, i);
                if (item != null) {
                    result.add(item);
                }
            }
        }
    }
    else {
        for (int i = seriesCount - 1; i >= 0; i--) {
            if (isSeriesVisibleInLegend(i)) {
                LegendItem item = getLegendItem(index, i);
                if (item != null) {
                    result.add(item);
                }
            }
        }
    }
    return result;
}

public LegendItemCollection getLegendItems() {
    LegendItemCollection result = new LegendItemCollection();
    if (this.plot == null) {
        return result;
    }
    int index = this.plot.getIndexOf(this);
    CategoryDataset dataset = this.plot.getDataset(index);
    if (dataset == null) {
        return result;
    }
    int seriesCount = dataset.getRowCount();
    if (plot.getRowRenderingOrder().equals(SortOrder.ASCENDING)) {
        for (int i = 0; i < seriesCount; i++) {
            if (isSeriesVisibleInLegend(i)) {
                LegendItem item = getLegendItem(index, i);
                if (item != null) {
                    result.add(item);
                }
            }
        }
    }
    else {
        for (int i = seriesCount - 1; i >= 0; i--) {
            if (isSeriesVisibleInLegend(i)) {
                LegendItem item = getLegendItem(index, i);
                if (item != null) {
                    result.add(item);
                }
            }
        }
    }
    return result;
}

#983 Potential NPE in AbstractCategoryItemRender.getLegendItems()

Setting up a working copy of the current JFreeChart trunk in Eclipse I got a warning about a null pointer access in this bit of code from AbstractCategoryItemRender.java:
public LegendItemCollection getLegendItems() {
LegendItemCollection result = new LegendItemCollection();
if (this.plot == null) {
return result;
}
int index = this.plot.getIndexOf(this);
CategoryDataset dataset = this.plot.getDataset(index);
if (dataset != null) {
return result;
}
int seriesCount = dataset.getRowCount();
...
}
The warning is in the last code line where seriesCount is assigned. The variable dataset is guaranteed to be null in this location, I suppose that the check before that should actually read "if (dataset == null)", not "if (dataset != null)".
This is trunk as of 2010-02-08.


public String generateToolTipFragment(String toolTipText) {
    return " title=\"" + toolTipText
        + "\" alt=\"\"";
}

public String generateToolTipFragment(String toolTipText) {
    return " title=\"" + ImageMapUtilities.htmlEscape(toolTipText)
        + "\" alt=\"\"";
}


public static boolean equal(GeneralPath p1, GeneralPath p2) {
    if (p1 == null) {
        return (p2 == null);
    }
    if (p2 == null) {
        return false;
    }
    if (p1.getWindingRule() != p2.getWindingRule()) {
        return false;
    }
    PathIterator iterator1 = p1.getPathIterator(null);
    PathIterator iterator2 = p1.getPathIterator(null);
    double[] d1 = new double[6];
    double[] d2 = new double[6];
    boolean done = iterator1.isDone() && iterator2.isDone();
    while (!done) {
        if (iterator1.isDone() != iterator2.isDone()) {
            return false;
        }
        int seg1 = iterator1.currentSegment(d1);
        int seg2 = iterator2.currentSegment(d2);
        if (seg1 != seg2) {
            return false;
        }
        if (!Arrays.equals(d1, d2)) {
            return false;
        }
        iterator1.next();
        iterator2.next();
        done = iterator1.isDone() && iterator2.isDone();
    }
    return true;
}

public static boolean equal(GeneralPath p1, GeneralPath p2) {
    if (p1 == null) {
        return (p2 == null);
    }
    if (p2 == null) {
        return false;
    }
    if (p1.getWindingRule() != p2.getWindingRule()) {
        return false;
    }
    PathIterator iterator1 = p1.getPathIterator(null);
    PathIterator iterator2 = p2.getPathIterator(null);
    double[] d1 = new double[6];
    double[] d2 = new double[6];
    boolean done = iterator1.isDone() && iterator2.isDone();
    while (!done) {
        if (iterator1.isDone() != iterator2.isDone()) {
            return false;
        }
        int seg1 = iterator1.currentSegment(d1);
        int seg2 = iterator2.currentSegment(d2);
        if (seg1 != seg2) {
            return false;
        }
        if (!Arrays.equals(d1, d2)) {
            return false;
        }
        iterator1.next();
        iterator2.next();
        done = iterator1.isDone() && iterator2.isDone();
    }
    return true;
}

#868 JCommon 1.0.12 ShapeUtilities.equal(path1,path2)

The comparison of two GeneralPath objects uses the same PathIterator for both objects. equal(GeneralPath path1, GeneralPath path2) will thus return true for any pair of non-null GeneralPath instances having the same windingRule.


public MultiplePiePlot(CategoryDataset dataset) {
    super();
    this.dataset = dataset;
    PiePlot piePlot = new PiePlot(null);
    this.pieChart = new JFreeChart(piePlot);
    this.pieChart.removeLegend();
    this.dataExtractOrder = TableOrder.BY_COLUMN;
    this.pieChart.setBackgroundPaint(null);
    TextTitle seriesTitle = new TextTitle("Series Title",
            new Font("SansSerif", Font.BOLD, 12));
    seriesTitle.setPosition(RectangleEdge.BOTTOM);
    this.pieChart.setTitle(seriesTitle);
    this.aggregatedItemsKey = "Other";
    this.aggregatedItemsPaint = Color.lightGray;
    this.sectionPaints = new HashMap();
}

public MultiplePiePlot(CategoryDataset dataset) {
    super();
    setDataset(dataset);
    PiePlot piePlot = new PiePlot(null);
    this.pieChart = new JFreeChart(piePlot);
    this.pieChart.removeLegend();
    this.dataExtractOrder = TableOrder.BY_COLUMN;
    this.pieChart.setBackgroundPaint(null);
    TextTitle seriesTitle = new TextTitle("Series Title",
            new Font("SansSerif", Font.BOLD, 12));
    seriesTitle.setPosition(RectangleEdge.BOTTOM);
    this.pieChart.setTitle(seriesTitle);
    this.aggregatedItemsKey = "Other";
    this.aggregatedItemsPaint = Color.lightGray;
    this.sectionPaints = new HashMap();
}

When dataset is passed into constructor for MultiplePiePlot, the dataset is not wired to a listener, as it would be if setDataset is called.


public Object clone() throws CloneNotSupportedException {
    Object clone = createCopy(0, getItemCount() - 1);
    return clone;
}

public Object clone() throws CloneNotSupportedException {
    TimeSeries clone = (TimeSeries) super.clone();
    clone.data = (List) ObjectUtilities.deepClone(this.data);
    return clone;
}

It's just a minor bug!
When I clone a TimeSeries which has no items, I get an IllegalArgumentException ("Requires start <= end").
But I don't think the user should be responsible for checking whether the TimeSeries has any items or not.


public ValueMarker(double value, Paint paint, Stroke stroke,
                   Paint outlinePaint, Stroke outlineStroke, float alpha) {
    super(paint, stroke, paint, stroke, alpha);
    this.value = value;
}

public ValueMarker(double value, Paint paint, Stroke stroke,
                   Paint outlinePaint, Stroke outlineStroke, float alpha) {
    super(paint, stroke, outlinePaint, outlineStroke, alpha);
    this.value = value;
}


public Paint getPaint(double value) {
    double v = Math.max(value, this.lowerBound);
    v = Math.min(v, this.upperBound);
    int g = (int) ((value - this.lowerBound) / (this.upperBound
            - this.lowerBound) * 255.0);
    return new Color(g, g, g);
}

public Paint getPaint(double value) {
    double v = Math.max(value, this.lowerBound);
    v = Math.min(v, this.upperBound);
    int g = (int) ((v - this.lowerBound) / (this.upperBound
            - this.lowerBound) * 255.0);
    return new Color(g, g, g);
}


public TimeSeries createCopy(int start, int end)
        throws CloneNotSupportedException {
    if (start < 0) {
        throw new IllegalArgumentException("Requires start >= 0.");
    }
    if (end < start) {
        throw new IllegalArgumentException("Requires start <= end.");
    }
    TimeSeries copy = (TimeSeries) super.clone();
    copy.data = new java.util.ArrayList();
    if (this.data.size() > 0) {
        for (int index = start; index <= end; index++) {
            TimeSeriesDataItem item
                    = (TimeSeriesDataItem) this.data.get(index);
            TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
            try {
                copy.add(clone);
            }
            catch (SeriesException e) {
                e.printStackTrace();
            }
        }
    }
    return copy;
}

public TimeSeries createCopy(int start, int end)
        throws CloneNotSupportedException {
    if (start < 0) {
        throw new IllegalArgumentException("Requires start >= 0.");
    }
    if (end < start) {
        throw new IllegalArgumentException("Requires start <= end.");
    }
    TimeSeries copy = (TimeSeries) super.clone();
    copy.minY = Double.NaN;
    copy.maxY = Double.NaN;
    copy.data = new java.util.ArrayList();
    if (this.data.size() > 0) {
        for (int index = start; index <= end; index++) {
            TimeSeriesDataItem item
                    = (TimeSeriesDataItem) this.data.get(index);
            TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
            try {
                copy.add(clone);
            }
            catch (SeriesException e) {
                e.printStackTrace();
            }
        }
    }
    return copy;
}


public Range getDataRange(ValueAxis axis) {

    Range result = null;
    List mappedDatasets = new ArrayList();
    List includedAnnotations = new ArrayList();
    boolean isDomainAxis = true;

    // is it a domain axis?
    int domainIndex = getDomainAxisIndex(axis);
    if (domainIndex >= 0) {
        isDomainAxis = true;
        mappedDatasets.addAll(getDatasetsMappedToDomainAxis(
                new Integer(domainIndex)));
        if (domainIndex == 0) {
            // grab the plot's annotations
            Iterator iterator = this.annotations.iterator();
            while (iterator.hasNext()) {
                XYAnnotation annotation = (XYAnnotation) iterator.next();
                if (annotation instanceof XYAnnotationBoundsInfo) {
                    includedAnnotations.add(annotation);
                }
            }
        }
    }

    // or is it a range axis?
    int rangeIndex = getRangeAxisIndex(axis);
    if (rangeIndex >= 0) {
        isDomainAxis = false;
        mappedDatasets.addAll(getDatasetsMappedToRangeAxis(
                new Integer(rangeIndex)));
        if (rangeIndex == 0) {
            Iterator iterator = this.annotations.iterator();
            while (iterator.hasNext()) {
                XYAnnotation annotation = (XYAnnotation) iterator.next();
                if (annotation instanceof XYAnnotationBoundsInfo) {
                    includedAnnotations.add(annotation);
                }
            }
        }
    }

    // iterate through the datasets that map to the axis and get the union
    // of the ranges.
    Iterator iterator = mappedDatasets.iterator();
    while (iterator.hasNext()) {
        XYDataset d = (XYDataset) iterator.next();
        if (d != null) {
            XYItemRenderer r = getRendererForDataset(d);
            if (isDomainAxis) {
                if (r != null) {
                    result = Range.combine(result, r.findDomainBounds(d));
                }
                else {
                    result = Range.combine(result,
                            DatasetUtilities.findDomainBounds(d));
                }
            }
            else {
                if (r != null) {
                    result = Range.combine(result, r.findRangeBounds(d));
                }
                else {
                    result = Range.combine(result,
                            DatasetUtilities.findRangeBounds(d));
                }
            }

                Collection c = r.getAnnotations();
                Iterator i = c.iterator();
                while (i.hasNext()) {
                    XYAnnotation a = (XYAnnotation) i.next();
                    if (a instanceof XYAnnotationBoundsInfo) {
                        includedAnnotations.add(a);
                    }
                }
        }
    }

    Iterator it = includedAnnotations.iterator();
    while (it.hasNext()) {
        XYAnnotationBoundsInfo xyabi = (XYAnnotationBoundsInfo) it.next();
        if (xyabi.getIncludeInDataBounds()) {
            if (isDomainAxis) {
                result = Range.combine(result, xyabi.getXRange());
            }
            else {
                result = Range.combine(result, xyabi.getYRange());
            }
        }
    }

    return result;

}

public Range getDataRange(ValueAxis axis) {

    Range result = null;
    List mappedDatasets = new ArrayList();
    List includedAnnotations = new ArrayList();
    boolean isDomainAxis = true;

    // is it a domain axis?
    int domainIndex = getDomainAxisIndex(axis);
    if (domainIndex >= 0) {
        isDomainAxis = true;
        mappedDatasets.addAll(getDatasetsMappedToDomainAxis(
                new Integer(domainIndex)));
        if (domainIndex == 0) {
            // grab the plot's annotations
            Iterator iterator = this.annotations.iterator();
            while (iterator.hasNext()) {
                XYAnnotation annotation = (XYAnnotation) iterator.next();
                if (annotation instanceof XYAnnotationBoundsInfo) {
                    includedAnnotations.add(annotation);
                }
            }
        }
    }

    // or is it a range axis?
    int rangeIndex = getRangeAxisIndex(axis);
    if (rangeIndex >= 0) {
        isDomainAxis = false;
        mappedDatasets.addAll(getDatasetsMappedToRangeAxis(
                new Integer(rangeIndex)));
        if (rangeIndex == 0) {
            Iterator iterator = this.annotations.iterator();
            while (iterator.hasNext()) {
                XYAnnotation annotation = (XYAnnotation) iterator.next();
                if (annotation instanceof XYAnnotationBoundsInfo) {
                    includedAnnotations.add(annotation);
                }
            }
        }
    }

    // iterate through the datasets that map to the axis and get the union
    // of the ranges.
    Iterator iterator = mappedDatasets.iterator();
    while (iterator.hasNext()) {
        XYDataset d = (XYDataset) iterator.next();
        if (d != null) {
            XYItemRenderer r = getRendererForDataset(d);
            if (isDomainAxis) {
                if (r != null) {
                    result = Range.combine(result, r.findDomainBounds(d));
                }
                else {
                    result = Range.combine(result,
                            DatasetUtilities.findDomainBounds(d));
                }
            }
            else {
                if (r != null) {
                    result = Range.combine(result, r.findRangeBounds(d));
                }
                else {
                    result = Range.combine(result,
                            DatasetUtilities.findRangeBounds(d));
                }
            }

            if (r != null) {
                Collection c = r.getAnnotations();
                Iterator i = c.iterator();
                while (i.hasNext()) {
                    XYAnnotation a = (XYAnnotation) i.next();
                    if (a instanceof XYAnnotationBoundsInfo) {
                        includedAnnotations.add(a);
                    }
                }
            }
        }
    }

    Iterator it = includedAnnotations.iterator();
    while (it.hasNext()) {
        XYAnnotationBoundsInfo xyabi = (XYAnnotationBoundsInfo) it.next();
        if (xyabi.getIncludeInDataBounds()) {
            if (isDomainAxis) {
                result = Range.combine(result, xyabi.getXRange());
            }
            else {
                result = Range.combine(result, xyabi.getYRange());
            }
        }
    }

    return result;

}


public boolean equals(Object obj) {

    if (obj == this) {
        return true;
    }
    if (!(obj instanceof ShapeList)) {
        return false;
    }
    return super.equals(obj);

}

public boolean equals(Object obj) {

    if (obj == this) {
        return true;
    }
    if (!(obj instanceof ShapeList)) {
        return false;
    }
    ShapeList that = (ShapeList) obj;
    int listSize = size();
    for (int i = 0; i < listSize; i++) {
       if (!ShapeUtilities.equal((Shape) get(i), (Shape) that.get(i))) {
           return false;
       }
    }
    return true;

}


private void updateBounds(TimePeriod period, int index) {

    long start = period.getStart().getTime();
    long end = period.getEnd().getTime();
    long middle = start + ((end - start) / 2);

    if (this.minStartIndex >= 0) {
        long minStart = getDataItem(this.minStartIndex).getPeriod()
            .getStart().getTime();
        if (start < minStart) {
            this.minStartIndex = index;
        }
    }
    else {
        this.minStartIndex = index;
    }

    if (this.maxStartIndex >= 0) {
        long maxStart = getDataItem(this.maxStartIndex).getPeriod()
            .getStart().getTime();
        if (start > maxStart) {
            this.maxStartIndex = index;
        }
    }
    else {
        this.maxStartIndex = index;
    }

    if (this.minMiddleIndex >= 0) {
        long s = getDataItem(this.minMiddleIndex).getPeriod().getStart()
            .getTime();
        long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd()
            .getTime();
        long minMiddle = s + (e - s) / 2;
        if (middle < minMiddle) {
            this.minMiddleIndex = index;
        }
    }
    else {
        this.minMiddleIndex = index;
    }

    if (this.maxMiddleIndex >= 0) {
        long s = getDataItem(this.minMiddleIndex).getPeriod().getStart()
            .getTime();
        long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd()
            .getTime();
        long maxMiddle = s + (e - s) / 2;
        if (middle > maxMiddle) {
            this.maxMiddleIndex = index;
        }
    }
    else {
        this.maxMiddleIndex = index;
    }

    if (this.minEndIndex >= 0) {
        long minEnd = getDataItem(this.minEndIndex).getPeriod().getEnd()
            .getTime();
        if (end < minEnd) {
            this.minEndIndex = index;
        }
    }
    else {
        this.minEndIndex = index;
    }

    if (this.maxEndIndex >= 0) {
        long maxEnd = getDataItem(this.maxEndIndex).getPeriod().getEnd()
            .getTime();
        if (end > maxEnd) {
            this.maxEndIndex = index;
        }
    }
    else {
        this.maxEndIndex = index;
    }

}

private void updateBounds(TimePeriod period, int index) {

    long start = period.getStart().getTime();
    long end = period.getEnd().getTime();
    long middle = start + ((end - start) / 2);

    if (this.minStartIndex >= 0) {
        long minStart = getDataItem(this.minStartIndex).getPeriod()
            .getStart().getTime();
        if (start < minStart) {
            this.minStartIndex = index;
        }
    }
    else {
        this.minStartIndex = index;
    }

    if (this.maxStartIndex >= 0) {
        long maxStart = getDataItem(this.maxStartIndex).getPeriod()
            .getStart().getTime();
        if (start > maxStart) {
            this.maxStartIndex = index;
        }
    }
    else {
        this.maxStartIndex = index;
    }

    if (this.minMiddleIndex >= 0) {
        long s = getDataItem(this.minMiddleIndex).getPeriod().getStart()
            .getTime();
        long e = getDataItem(this.minMiddleIndex).getPeriod().getEnd()
            .getTime();
        long minMiddle = s + (e - s) / 2;
        if (middle < minMiddle) {
            this.minMiddleIndex = index;
        }
    }
    else {
        this.minMiddleIndex = index;
    }

    if (this.maxMiddleIndex >= 0) {
        long s = getDataItem(this.maxMiddleIndex).getPeriod().getStart()
            .getTime();
        long e = getDataItem(this.maxMiddleIndex).getPeriod().getEnd()
            .getTime();
        long maxMiddle = s + (e - s) / 2;
        if (middle > maxMiddle) {
            this.maxMiddleIndex = index;
        }
    }
    else {
        this.maxMiddleIndex = index;
    }

    if (this.minEndIndex >= 0) {
        long minEnd = getDataItem(this.minEndIndex).getPeriod().getEnd()
            .getTime();
        if (end < minEnd) {
            this.minEndIndex = index;
        }
    }
    else {
        this.minEndIndex = index;
    }

    if (this.maxEndIndex >= 0) {
        long maxEnd = getDataItem(this.maxEndIndex).getPeriod().getEnd()
            .getTime();
        if (end > maxEnd) {
            this.maxEndIndex = index;
        }
    }
    else {
        this.maxEndIndex = index;
    }

}


public Week(Date time, TimeZone zone) {
    // defer argument checking...
    this(time, RegularTimePeriod.DEFAULT_TIME_ZONE, Locale.getDefault());
}

public Week(Date time, TimeZone zone) {
    // defer argument checking...
    this(time, zone, Locale.getDefault());
}


public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
    throws CloneNotSupportedException {

    if (start == null) {
        throw new IllegalArgumentException("Null 'start' argument.");
    }
    if (end == null) {
        throw new IllegalArgumentException("Null 'end' argument.");
    }
    if (start.compareTo(end) > 0) {
        throw new IllegalArgumentException(
                "Requires start on or before end.");
    }
    boolean emptyRange = false;
    int startIndex = getIndex(start);
    if (startIndex < 0) {
        startIndex = -(startIndex + 1);
        if (startIndex == this.data.size()) {
            emptyRange = true;  // start is after last data item
        }
    }
    int endIndex = getIndex(end);
    if (endIndex < 0) {             // end period is not in original series
        endIndex = -(endIndex + 1); // this is first item AFTER end period
        endIndex = endIndex - 1;    // so this is last item BEFORE end
    }
    if (endIndex < 0) {
        emptyRange = true;
    }
    if (emptyRange) {
        TimeSeries copy = (TimeSeries) super.clone();
        copy.data = new java.util.ArrayList();
        return copy;
    }
    else {
        return createCopy(startIndex, endIndex);
    }

}

public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
    throws CloneNotSupportedException {

    if (start == null) {
        throw new IllegalArgumentException("Null 'start' argument.");
    }
    if (end == null) {
        throw new IllegalArgumentException("Null 'end' argument.");
    }
    if (start.compareTo(end) > 0) {
        throw new IllegalArgumentException(
                "Requires start on or before end.");
    }
    boolean emptyRange = false;
    int startIndex = getIndex(start);
    if (startIndex < 0) {
        startIndex = -(startIndex + 1);
        if (startIndex == this.data.size()) {
            emptyRange = true;  // start is after last data item
        }
    }
    int endIndex = getIndex(end);
    if (endIndex < 0) {             // end period is not in original series
        endIndex = -(endIndex + 1); // this is first item AFTER end period
        endIndex = endIndex - 1;    // so this is last item BEFORE end
    }
    if ((endIndex < 0)  || (endIndex < startIndex)) {
        emptyRange = true;
    }
    if (emptyRange) {
        TimeSeries copy = (TimeSeries) super.clone();
        copy.data = new java.util.ArrayList();
        return copy;
    }
    else {
        return createCopy(startIndex, endIndex);
    }

}

#818 Error on TimeSeries createCopy() method

The test case at the end fails with :
java.lang.IllegalArgumentException: Requires start <= end.
The problem is in that the int start and end indexes corresponding to given timePeriod are computed incorectly. Here I would expect an empty serie to be returned, not an exception. This is with jfreechart 1.0.7
public class foo {
static public void main(String args[]) {
TimeSeries foo = new TimeSeries("foo",Day.class);
foo.add(new Day(19,4,2005),1);
foo.add(new Day(25,5,2005),1);
foo.add(new Day(28,5,2005),1);
foo.add(new Day(30,5,2005),1);
foo.add(new Day(1,6,2005),1);
foo.add(new Day(3,6,2005),1);
foo.add(new Day(19,8,2005),1);
foo.add(new Day(31,1,2006),1);
    try \{
        TimeSeries bar = foo.createCopy\(new Day\(1,12,2005\),new Day\(18,1,2006\)\);
    \} catch \(CloneNotSupportedException e\) \{

        e.printStackTrace\(\);

}
}


protected void setOptions(final Options options) {
        this.options = options;
        this.requiredOptions = options.getRequiredOptions();
    }

protected void setOptions(final Options options) {
        this.options = options;
        this.requiredOptions = new ArrayList(options.getRequiredOptions());
    }

Missing required options not throwing MissingOptionException

When an Options object is used to parse a second set of command arguments it won't throw a MissingOptionException.

{code:java}
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

public class Example
{
	public static void main(String[] args) throws ParseException
	{
		brokenExample();
		workingExample();
	}

	// throws exception as expected
	private static void workingExample() throws ParseException
	{
		String[] args = {};

		Options opts = new Options();
		opts.addOption(OptionBuilder.isRequired().create('v'));

		GnuParser parser = new GnuParser();
		CommandLine secondCL = parser.parse(opts, args);

		System.out.println("Done workingExample");
	}

	// fails to throw exception on second invocation of parse
	private static void brokenExample() throws ParseException
	{
		String[] firstArgs = { "-v" };
		String[] secondArgs = {};

		Options opts = new Options();
		opts.addOption(OptionBuilder.isRequired().create('v'));

		GnuParser parser = new GnuParser();
		CommandLine firstCL = parser.parse(opts, firstArgs);
		CommandLine secondCL = parser.parse(opts, secondArgs);

		System.out.println("Done brokenExample");
	}
}
{code}

This is a result of the Options object returning the reference to its own list and the parsers modifying that list. The first call is removing the required options as they are found and subsequent calls get back an empty list.


public List getValues(final Option option,
                      List defaultValues) {
    // initialize the return list
    List valueList = (List) values.get(option);

    // grab the correct default values
    if ((valueList == null) || valueList.isEmpty()) {
        valueList = defaultValues;
    }

    // augment the list with the default values
    if ((valueList == null) || valueList.isEmpty()) {
        valueList = (List) this.defaultValues.get(option);
    }
            // if there are more default values as specified, add them to
            // the list.
                // copy the list first

    return valueList == null ? Collections.EMPTY_LIST : valueList;
}

public List getValues(final Option option,
                      List defaultValues) {
    // initialize the return list
    List valueList = (List) values.get(option);

    // grab the correct default values
    if (defaultValues == null || defaultValues.isEmpty()) {
        defaultValues = (List) this.defaultValues.get(option);
    }

    // augment the list with the default values
    if (defaultValues != null && !defaultValues.isEmpty()) {
        if (valueList == null || valueList.isEmpty()) {
            valueList = defaultValues;
        } else {
            // if there are more default values as specified, add them to
            // the list.
            if (defaultValues.size() > valueList.size()) {
                // copy the list first
                valueList = new ArrayList(valueList);
                for (int i=valueList.size(); i<defaultValues.size(); i++) {
                    valueList.add(defaultValues.get(i));
                }
            }
        }
    }

    return valueList == null ? Collections.EMPTY_LIST : valueList;
}

deafult arguments only works if no arguments are submitted

When using multple arguments and defaults, the behaviour is counter-intuitive and will only pick up a default if no args are passed in.
For instance in the code below I have set up so 0, 1, or 2 args may bve accepted, with defaults 100 and 1000.
I expect it to behave as follows.
1. for 2 args, 1 and 2 the values should be 1 and 2. This works as expected.
2. for 0 args passed in the values should be 100 and 1000, picking up both of the defaults. This works as expected
3. for 1 arg passed in the values should be 1 and 1000, so the second argument picks up the second default value. The valuse become just 1, which is not as expected..
Currently, in the second case will only return 1 and ignore the defaults.
    public void testSingleOptionSingleArgument() throws Exception {
        String defaulValue1 = "100";
        String defaultValue2 = "1000";
        final DefaultOptionBuilder obuilder = new DefaultOptionBuilder();
        final ArgumentBuilder abuilder = new ArgumentBuilder();
        final GroupBuilder gbuilder = new GroupBuilder();
        DefaultOption bOption = obuilder.withShortName("b")
                .withLongName("b")
                .withArgument(abuilder.withName("b")
                        .withMinimum(0)
                        .withMaximum(2)
                        .withDefault(defaulValue1)
                        .withDefault(defaultValue2)
                        .create())
                .create();
        Group options = gbuilder
                .withName("options")
                .withOption(bOption)
                .create();
        Parser parser = new Parser();
        parser.setHelpTrigger("--help");
        parser.setGroup(options);
        String enteredValue1 = "1";
        String[] args = new String[]
{"-b", enteredValue1}
;
        CommandLine cl = parser.parse(args);
        CommandLine cmd = cl;
        assertNotNull(cmd);
        List b = cmd.getValues("-b");
        assertEquals("[" + enteredValue1 + "]", b + "");
    }


protected void burstToken(String token, boolean stopAtNonOption)
{
    for (int i = 1; i < token.length(); i++)
    {
        String ch = String.valueOf(token.charAt(i));

        if (options.hasOption(ch))
        {
            tokens.add("-" + ch);
            currentOption = options.getOption(ch);

            if (currentOption.hasArg() && (token.length() != (i + 1)))
            {
                tokens.add(token.substring(i + 1));

                break;
            }
        }
        else if (stopAtNonOption)
        {
            process(token.substring(i));
        }
        else
        {
            tokens.add(token);
            break;
        }
    }
}

protected void burstToken(String token, boolean stopAtNonOption)
{
    for (int i = 1; i < token.length(); i++)
    {
        String ch = String.valueOf(token.charAt(i));

        if (options.hasOption(ch))
        {
            tokens.add("-" + ch);
            currentOption = options.getOption(ch);

            if (currentOption.hasArg() && (token.length() != (i + 1)))
            {
                tokens.add(token.substring(i + 1));

                break;
            }
        }
        else if (stopAtNonOption)
        {
            process(token.substring(i));
            break;
        }
        else
        {
            tokens.add(token);
            break;
        }
    }
}

PosixParser keeps bursting tokens even if a non option character is found

PosixParser doesn't stop the bursting process of a token if stopAtNonOption is enabled and a non option character is encountered.
For example if the options a and b are defined, with stopAtNonOption=true the following command line:

-azb

is turned into:

-a zb -b

the right output should be:

-a zb


private void processOptionToken(String token, boolean stopAtNonOption)
{
    if (options.hasOption(token))
    {
        currentOption = options.getOption(token);
        tokens.add(token);
    }
    else if (stopAtNonOption)
    {
        eatTheRest = true;
        tokens.add(token);
    }
}

private void processOptionToken(String token, boolean stopAtNonOption)
{
    if (options.hasOption(token))
    {
        currentOption = options.getOption(token);
    }
    else if (stopAtNonOption)
    {
        eatTheRest = true;
    }

    tokens.add(token);
}

PosixParser ignores unrecognized tokens starting with '-'

PosixParser doesn't handle properly unrecognized tokens starting with '-' when stopAtNonOption is enabled, the token is simply ignored.
For example, if the option 'a' is defined, the following command line:

-z -a foo

is interpreted as:

-a foo


protected void burstToken(String token, boolean stopAtNonOption)
    {
        int tokenLength = token.length();

        for (int i = 1; i < tokenLength; i++)
        {
            String ch = String.valueOf(token.charAt(i));
            boolean hasOption = options.hasOption(ch);

            if (hasOption)
            {
                tokens.add("-" + ch);
                currentOption = options.getOption(ch);

                if (currentOption.hasArg() && (token.length() != (i + 1)))
                {
                    tokens.add(token.substring(i + 1));

                    break;
                }
            }
            else if (stopAtNonOption)
            {
                process(token.substring(i));
            }
            else
            {
                tokens.add("-" + ch);
            }
        }
    }

protected void burstToken(String token, boolean stopAtNonOption)
    {
        int tokenLength = token.length();

        for (int i = 1; i < tokenLength; i++)
        {
            String ch = String.valueOf(token.charAt(i));
            boolean hasOption = options.hasOption(ch);

            if (hasOption)
            {
                tokens.add("-" + ch);
                currentOption = options.getOption(ch);

                if (currentOption.hasArg() && (token.length() != (i + 1)))
                {
                    tokens.add(token.substring(i + 1));

                    break;
                }
            }
            else if (stopAtNonOption)
            {
                process(token.substring(i));
            }
            else
            {
                tokens.add(token);
                break;
            }
        }
    }

[cli] Parameter value "-something" misinterpreted as a parameter

If a parameter value is passed that contains a hyphen as the (delimited) first
character, CLI parses this a parameter. For example using the call
java myclass -t "-something"
Results in the parser creating the invalid parameter -o (noting that it is
skipping the 's')

My code is using the Posix parser as follows
Options options = buildCommandLineOptions();
CommandLineParser parser = new PosixParser();
CommandLine commandLine = null;
try {

	commandLine = parser.parse(options, args);
}
catch (ParseException e) {

	System.out.println("Invalid parameters. " + e.getMessage() + NEW_LINE);
	System.exit(EXIT_CODE_ERROR);
}

This has been tested against the nightly build dated 20050503.


protected String[] flatten(Options options, String[] arguments, boolean stopAtNonOption)
{
    init();
    this.options = options;

    // an iterator for the command line tokens
    Iterator iter = Arrays.asList(arguments).iterator();

    // process each command line token
    while (iter.hasNext())
    {
        // get the next command line token
        String token = (String) iter.next();

        // handle long option --foo or --foo=bar
        if (token.startsWith("--"))
        {
            if (token.indexOf('=') != -1)
            {
                tokens.add(token.substring(0, token.indexOf('=')));
                tokens.add(token.substring(token.indexOf('=') + 1, token.length()));
            }
            else
            {
                tokens.add(token);
            }
        }

        // single hyphen
        else if ("-".equals(token))
        {
            tokens.add(token);
        }
        else if (token.startsWith("-"))
        {
            if (token.length() == 2)
            {
                processOptionToken(token, stopAtNonOption);
            }
            else if (options.hasOption(token))
            {
                tokens.add(token);
            }
            // requires bursting
            else
            {
                burstToken(token, stopAtNonOption);
            }
        }
        else if (stopAtNonOption)
        {
            process(token);
        }
        else
        {
            tokens.add(token);
        }

        gobble(iter);
    }

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

protected String[] flatten(Options options, String[] arguments, boolean stopAtNonOption)
{
    init();
    this.options = options;

    // an iterator for the command line tokens
    Iterator iter = Arrays.asList(arguments).iterator();

    // process each command line token
    while (iter.hasNext())
    {
        // get the next command line token
        String token = (String) iter.next();

        // handle long option --foo or --foo=bar
        if (token.startsWith("--"))
        {
            int pos = token.indexOf('=');
            String opt = pos == -1 ? token : token.substring(0, pos); // --foo

            if (!options.hasOption(opt) && stopAtNonOption)
            {
                process(token);
            }
            else
            {
                tokens.add(opt);
                if (pos != -1) {
                    tokens.add(token.substring(pos + 1));
                }
            }
        }

        // single hyphen
        else if ("-".equals(token))
        {
            tokens.add(token);
        }
        else if (token.startsWith("-"))
        {
            if (token.length() == 2)
            {
                processOptionToken(token, stopAtNonOption);
            }
            else if (options.hasOption(token))
            {
                tokens.add(token);
            }
            // requires bursting
            else
            {
                burstToken(token, stopAtNonOption);
            }
        }
        else if (stopAtNonOption)
        {
            process(token);
        }
        else
        {
            tokens.add(token);
        }

        gobble(iter);
    }

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

PosixParser keeps processing tokens after a non unrecognized long option when stopAtNonOption is enabled. The tokens after the unrecognized long option are burst, split around '=', etc.. instead of being kept as is.
For example, with the options 'a' and 'b' defined, 'b' having an argument, the following command line:

--zop -abfoo

is interpreted as:

--zop -a -b foo

but the last token should remain unchanged.


protected StringBuffer renderWrappedText(StringBuffer sb, int width,
                                         int nextLineTabStop, String text)
{
    int pos = findWrapPos(text, width, 0);

    if (pos == -1)
    {
        sb.append(rtrim(text));

        return sb;
    }
    sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);

    // all following lines must be padded with nextLineTabStop space
    // characters
    final String padding = createPadding(nextLineTabStop);

    while (true)
    {
        int lastPos = pos;
        text = padding + text.substring(pos).trim();
        pos = findWrapPos(text, width, 0);

        if (pos == -1)
        {
            sb.append(text);

            return sb;
        } else
        if (pos == lastPos)
        {
            throw new RuntimeException("Text too long for line - throwing exception to avoid infinite loop [CLI-162]: " + text);
        }

        sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
    }
}

protected StringBuffer renderWrappedText(StringBuffer sb, int width,
                                         int nextLineTabStop, String text)
{
    int pos = findWrapPos(text, width, 0);

    if (pos == -1)
    {
        sb.append(rtrim(text));

        return sb;
    }
    sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);

    // all following lines must be padded with nextLineTabStop space
    // characters
    final String padding = createPadding(nextLineTabStop);

    while (true)
    {
        text = padding + text.substring(pos).trim();
        pos = findWrapPos(text, width, 0);

        if (pos == -1)
        {
            sb.append(text);

            return sb;
        }

        if ( (text.length() > width) && (pos == nextLineTabStop - 1) ) {
            sb.append(text);

            return sb;
        }

        sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
    }
}

infinite loop in the wrapping code of HelpFormatter

If there is not enough space to display a word on a single line, HelpFormatter goes into a infinite loops until the JVM crashes with an OutOfMemoryError.
Test case:

Options options = new Options();
options.addOption("h", "help", false, "This is a looooong description");

HelpFormatter formatter = new HelpFormatter();
formatter.setWidth(20);
formatter.printHelp("app", options); // hang & crash

An helpful exception indicating the insufficient width would be more appropriate than an OutOfMemoryError.


protected StringBuffer renderWrappedText(StringBuffer sb, int width,
                                         int nextLineTabStop, String text)
{
    int pos = findWrapPos(text, width, 0);

    if (pos == -1)
    {
        sb.append(rtrim(text));

        return sb;
    }
    sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);

    if (nextLineTabStop >= width)
    {
        // stops infinite loop happening
        throw new IllegalStateException("Total width is less than the width of the argument and indent " +
                                        "- no room for the description");
    }

    // all following lines must be padded with nextLineTabStop space
    // characters
    final String padding = createPadding(nextLineTabStop);

    while (true)
    {
        text = padding + text.substring(pos).trim();
        pos = findWrapPos(text, width, 0);

        if (pos == -1)
        {
            sb.append(text);

            return sb;
        }

        if ( (text.length() > width) && (pos == nextLineTabStop - 1) )
        {
            pos = width;
        }

        sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
    }
}

protected StringBuffer renderWrappedText(StringBuffer sb, int width,
                                         int nextLineTabStop, String text)
{
    int pos = findWrapPos(text, width, 0);

    if (pos == -1)
    {
        sb.append(rtrim(text));

        return sb;
    }
    sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);

    if (nextLineTabStop >= width)
    {
        // stops infinite loop happening
        nextLineTabStop = width - 1;
    }

    // all following lines must be padded with nextLineTabStop space
    // characters
    final String padding = createPadding(nextLineTabStop);

    while (true)
    {
        text = padding + text.substring(pos).trim();
        pos = findWrapPos(text, width, 0);

        if (pos == -1)
        {
            sb.append(text);

            return sb;
        }

        if ( (text.length() > width) && (pos == nextLineTabStop - 1) )
        {
            pos = width;
        }

        sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
    }
}


protected StringBuffer renderWrappedText(StringBuffer sb, int width,
                                         int nextLineTabStop, String text)
{
    int pos = findWrapPos(text, width, 0);

    if (pos == -1)
    {
        sb.append(rtrim(text));

        return sb;
    }
    sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);

    if (nextLineTabStop >= width)
    {
        // stops infinite loop happening
        nextLineTabStop = width - 1;
    }

    // all following lines must be padded with nextLineTabStop space
    // characters
    final String padding = createPadding(nextLineTabStop);

    while (true)
    {
        text = padding + text.substring(pos).trim();
        pos = findWrapPos(text, width, 0);

        if (pos == -1)
        {
            sb.append(text);

            return sb;
        }

        if ( (text.length() > width) && (pos == nextLineTabStop - 1) )
        {
            pos = width;
        }

        sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
    }
}

protected StringBuffer renderWrappedText(StringBuffer sb, int width,
                                         int nextLineTabStop, String text)
{
    int pos = findWrapPos(text, width, 0);

    if (pos == -1)
    {
        sb.append(rtrim(text));

        return sb;
    }
    sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);

    if (nextLineTabStop >= width)
    {
        // stops infinite loop happening
        nextLineTabStop = 1;
    }

    // all following lines must be padded with nextLineTabStop space
    // characters
    final String padding = createPadding(nextLineTabStop);

    while (true)
    {
        text = padding + text.substring(pos).trim();
        pos = findWrapPos(text, width, 0);

        if (pos == -1)
        {
            sb.append(text);

            return sb;
        }

        if ( (text.length() > width) && (pos == nextLineTabStop - 1) )
        {
            pos = width;
        }

        sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
    }
}


public static Option create(String opt) throws IllegalArgumentException
{
        // create the option
    Option option = new Option(opt, description);

        // set the option properties
        option.setLongOpt(longopt);
        option.setRequired(required);
        option.setOptionalArg(optionalArg);
        option.setArgs(numberOfArgs);
        option.setType(type);
        option.setValueSeparator(valuesep);
        option.setArgName(argName);
        // reset the OptionBuilder properties
        OptionBuilder.reset();

    // return the Option instance
    return option;
}

public static Option create(String opt) throws IllegalArgumentException
{
    Option option = null;
    try {
        // create the option
        option = new Option(opt, description);

        // set the option properties
        option.setLongOpt(longopt);
        option.setRequired(required);
        option.setOptionalArg(optionalArg);
        option.setArgs(numberOfArgs);
        option.setType(type);
        option.setValueSeparator(valuesep);
        option.setArgName(argName);
    } finally {
        // reset the OptionBuilder properties
        OptionBuilder.reset();
    }

    // return the Option instance
    return option;
}

OptionBuilder is not reseted in case of an IAE at create

If the call to OptionBuilder.create() fails with an IllegalArgumentException, the OptionBuilder is not resetted and its next usage may contain unwanted settings. Actually this let the CLI-1.2 RCs fail on IBM JDK 6 running on Maven 2.0.10.


public void setSelected(Option option) throws AlreadySelectedException
{
    if (option == null)
    {
        // reset the option previously selected
        selected = null;
        return;
    }

    // if no option has already been selected or the
    // same option is being reselected then set the
    // selected member variable
    if (selected == null || selected.equals(option.getOpt()))
    {
        selected = option.getOpt();
    }
    else
    {
        throw new AlreadySelectedException(this, option);
    }
}

public void setSelected(Option option) throws AlreadySelectedException
{
    if (option == null)
    {
        // reset the option previously selected
        selected = null;
        return;
    }

    // if no option has already been selected or the
    // same option is being reselected then set the
    // selected member variable
    if (selected == null || selected.equals(option.getKey()))
    {
        selected = option.getKey();
    }
    else
    {
        throw new AlreadySelectedException(this, option);
    }
}

Unable to select a pure long option in a group

OptionGroup doesn't play nice with options with a long name and no short name. If the selected option hasn't a short name, group.setSelected(option) has no effect.


protected void processProperties(Properties properties)
{
    if (properties == null)
    {
        return;
    }

    for (Enumeration e = properties.propertyNames(); e.hasMoreElements();)
    {
        String option = e.nextElement().toString();

        if (!cmd.hasOption(option))
        {
            Option opt = getOptions().getOption(option);

            // get the value from the properties instance
            String value = properties.getProperty(option);

            if (opt.hasArg())
            {
                if (opt.getValues() == null || opt.getValues().length == 0)
                {
                    try
                    {
                        opt.addValueForProcessing(value);
                    }
                    catch (RuntimeException exp)
                    {
                        // if we cannot add the value don't worry about it
                    }
                }
            }
            else if (!("yes".equalsIgnoreCase(value)
                    || "true".equalsIgnoreCase(value)
                    || "1".equalsIgnoreCase(value)))
            {
                // if the value is not yes, true or 1 then don't add the
                // option to the CommandLine
                break;
            }

            cmd.addOption(opt);
        }
    }
}

protected void processProperties(Properties properties)
{
    if (properties == null)
    {
        return;
    }

    for (Enumeration e = properties.propertyNames(); e.hasMoreElements();)
    {
        String option = e.nextElement().toString();

        if (!cmd.hasOption(option))
        {
            Option opt = getOptions().getOption(option);

            // get the value from the properties instance
            String value = properties.getProperty(option);

            if (opt.hasArg())
            {
                if (opt.getValues() == null || opt.getValues().length == 0)
                {
                    try
                    {
                        opt.addValueForProcessing(value);
                    }
                    catch (RuntimeException exp)
                    {
                        // if we cannot add the value don't worry about it
                    }
                }
            }
            else if (!("yes".equalsIgnoreCase(value)
                    || "true".equalsIgnoreCase(value)
                    || "1".equalsIgnoreCase(value)))
            {
                // if the value is not yes, true or 1 then don't add the
                // option to the CommandLine
                continue;
            }

            cmd.addOption(opt);
        }
    }
}

Default options may be partially processed

The Properties instance passed to the Parser.parse() method to initialize the default options may be partially processed. This happens when the properties contains an option that doesn't accept arguments and has a default value that isn't evaluated to "true". When this case occurs the processing of the properties is stopped and the remaining options are never handled.
This is caused by the break statement in Parser.processProperties(Properties), a continue statement should have been used instead.
The related test in ValueTest is also wrong, there are two assertions that need to be changed:

Options opts = new Options();
opts.addOption("a", false, "toggle -a");
opts.addOption("c", "c", false, "toggle -c");
opts.addOption(OptionBuilder.hasOptionalArg().create('e'));

properties = new Properties();
properties.setProperty( "a", "false" );
properties.setProperty( "c", "no" );
properties.setProperty( "e", "0" );

cmd = parser.parse(opts, null, properties);
assertTrue( !cmd.hasOption("a") );
assertTrue( !cmd.hasOption("c") );
assertTrue( !cmd.hasOption("e") ); // Wrong, this option accepts an argument and should receive the value "0"

 and the second one:

properties = new Properties();
properties.setProperty( "a", "just a string" );
properties.setProperty( "e", "" );

cmd = parser.parse(opts, null, properties);
assertTrue( !cmd.hasOption("a") );
assertTrue( !cmd.hasOption("c") );
assertTrue( !cmd.hasOption("e") ); // Wrong, this option accepts an argument and should receive an empty string as value


static String stripLeadingAndTrailingQuotes(String str)
{
    if (str.startsWith("\""))
    {
        str = str.substring(1, str.length());
    }
    int length = str.length();
    if (str.endsWith("\""))
    {
        str = str.substring(0, length - 1);
    }

    return str;
}

static String stripLeadingAndTrailingQuotes(String str)
{
    int length = str.length();
    if (length > 1 && str.startsWith("\"") && str.endsWith("\"") && str.substring(1, length - 1).indexOf('"') == -1)
    {
        str = str.substring(1, length - 1);
    }

    return str;
}

Commons CLI incorrectly stripping leading and trailing quotes

org.apache.commons.cli.Parser.processArgs() calls Util.stripLeadingAndTrailingQuotes() for all argument values. IMHO this is incorrect and totally broken.
It is trivial to create a simple test for this. Output:
    $ java -cp target/clitest.jar Clitest --balloo "this is a \"test\""
    Value of argument balloo is 'this is a "test'.
The argument 'balloo' should indeed keep its trailing double quote. It is what the shell gives it, so don't try to do something clever to it.
The offending code was committed here:
http://svn.apache.org/viewvc?view=rev&revision=129874
and has been there for more than 6 years . Why was this committed in the first place?
The fix is trivial, just get rid of Util.stripLeadingAndTrailingQuotes(), and consequently avoid calling it from Parser.processArgs().


public static Number createNumber(String str)
    {
        try
        {
            return NumberUtils.createNumber(str);
        }
        catch (NumberFormatException nfe)
        {
            System.err.println(nfe.getMessage());
        }

        return null;
    }

public static Number createNumber(String str)
    {
        try
        {
            if( str != null )
            {
                if( str.indexOf('.') != -1 )
                {
                    return Double.valueOf(str);
                }
                else
                {
                    return Long.valueOf(str);
                }
            }
        }
        catch (NumberFormatException nfe)
        {
            System.err.println(nfe.getMessage());
        }

        return null;
    }

PosixParser interupts "-target opt" as "-t arget opt"

This was posted on the Commons-Developer list and confirmed as a bug.

> Is this a bug?  Or am I using this incorrectly?
> I have an option with short and long values.  Given code that is
> essentially what is below, with a PosixParser I see results as
> follows:
>
> A command line with just "-t" prints out the results of the catch
> block
> (OK)
> A command line with just "-target" prints out the results of the catch
> block (OK)
> A command line with just "-t foobar.com" prints out "processing selected
> target: foobar.com" (OK)
> A command line with just "-target foobar.com" prints out "processing
> selected target: arget" (ERROR?)
>
> ======================================================================
> ==
> =======================
>   private static final String OPTION_TARGET = "t";
>   private static final String OPTION_TARGET_LONG = "target";
>   // ...
>   Option generateTarget = new Option(OPTION_TARGET,
>                                      OPTION_TARGET_LONG,
>                                      true,
>                                      "Generate files for the specified
> target machine");
>   // ...
>   try {
>         parsedLine = parser.parse(cmdLineOpts, args);
>       } catch (ParseException pe) {
>         System.out.println("Invalid command: " + pe.getMessage() +
> "\n");
>         HelpFormatter hf = new HelpFormatter();
>         hf.printHelp(USAGE, cmdLineOpts);
>         System.exit(-1);
>       }
>
>   if (parsedLine.hasOption(OPTION_TARGET)) {
>     System.out.println("processing selected target: " +
> parsedLine.getOptionValue(OPTION_TARGET));
>   }

It is a bug but it is due to well defined behaviour (so that makes me feel a
little better about myself ;).  To support *special*
(well I call them special anyway) like -Dsystem.property=value we need to be
able to examine the first character of an option.  If the first character is
itself defined as an Option then the remainder of the token is used as the
value, e.g. 'D' is the token, it is an option so 'system.property=value' is the
argument value for that option.  This is the behaviour that we are seeing for
your example.
't' is the token, it is an options so 'arget' is the argument value.

I suppose a solution to this could be to have a way to specify properties for
parsers.  In this case 'posix.special.option == true' for turning
on *special* options. I'll have a look into this and let you know.

Just to keep track of this and to get you used to how we operate, can you log a
bug in bugzilla for this.

Thanks,
-John K


protected int findWrapPos(String text, int width, int startPos)
{
    int pos;

    // the line ends before the max wrap pos or a new line char found
    if (((pos = text.indexOf('\n', startPos)) != -1 && pos <= width)
            || ((pos = text.indexOf('\t', startPos)) != -1 && pos <= width))
    {
        return pos + 1;
    }
    else if (startPos + width >= text.length())
    {
        return -1;
    }

    // look for the last whitespace character before startPos+width
    pos = startPos + width;

    char c;

    while ((pos >= startPos) && ((c = text.charAt(pos)) != ' ')
            && (c != '\n') && (c != '\r'))
    {
        --pos;
    }

    // if we found it - just return
    if (pos > startPos)
    {
        return pos;
    }

    // if we didn't find one, simply chop at startPos+width
    pos = startPos + width;
    while ((pos <= text.length()) && ((c = text.charAt(pos)) != ' ')
           && (c != '\n') && (c != '\r'))
    {
        ++pos;
    }
    return pos == text.length() ? -1 : pos;
}

protected int findWrapPos(String text, int width, int startPos)
{
    int pos;

    // the line ends before the max wrap pos or a new line char found
    if (((pos = text.indexOf('\n', startPos)) != -1 && pos <= width)
            || ((pos = text.indexOf('\t', startPos)) != -1 && pos <= width))
    {
        return pos + 1;
    }
    else if (startPos + width >= text.length())
    {
        return -1;
    }

    // look for the last whitespace character before startPos+width
    pos = startPos + width;

    char c;

    while ((pos >= startPos) && ((c = text.charAt(pos)) != ' ')
            && (c != '\n') && (c != '\r'))
    {
        --pos;
    }

    // if we found it - just return
    if (pos > startPos)
    {
        return pos;
    }

    // if we didn't find one, simply chop at startPos+width
    pos = startPos + width;

    return pos == text.length() ? -1 : pos;
}

In the last while loop in HelpFormatter.findWrapPos, it can pass text.length() to text.charAt(int), which throws a StringIndexOutOfBoundsException. The first expression in that while loop condition should use a <, not a <=.
This is on line 908 in r779646:
http://svn.apache.org/viewvc/commons/proper/cli/trunk/src/java/org/apache/commons/cli/HelpFormatter.java?revision=779646&view=markup


public void printWrapped(PrintWriter pw, int width, int nextLineTabStop, String text)
    {
        StringBuffer sb = new StringBuffer(text.length());

        renderWrappedText(sb, width, nextLineTabStop, text);
        pw.println(sb.toString());
    }

public void printWrapped(PrintWriter pw, int width, int nextLineTabStop, String text)
    {
        StringBuffer sb = new StringBuffer(text.length());

        renderWrappedTextBlock(sb, width, nextLineTabStop, text);
        pw.println(sb.toString());
    }

HelpFormatter strips leading whitespaces in the footer

I discovered a bug in Commons CLI while using it through Groovy's CliBuilder. See the following issue:

http://jira.codehaus.org/browse/GROOVY-4313?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel

Copied:
The following code:

def cli = new CliBuilder(footer: "line1:\n line2:\n")
cli.usage()

Produces the following output:

line1
line2

Note that there are no whitespaces before "line2". Replacing them with "\t" doesn't solve the problem either.


public List<String> getMatchingOptions(String opt)
{
    opt = Util.stripLeadingHyphens(opt);

    List<String> matchingOpts = new ArrayList<String>();

    // for a perfect match return the single option only

    for (String longOpt : longOpts.keySet())
    {
        if (longOpt.startsWith(opt))
        {
            matchingOpts.add(longOpt);
        }
    }

    return matchingOpts;
}

public List<String> getMatchingOptions(String opt)
{
    opt = Util.stripLeadingHyphens(opt);

    List<String> matchingOpts = new ArrayList<String>();

    // for a perfect match return the single option only
    if(longOpts.keySet().contains(opt)) {
        return Collections.singletonList(opt);
    }

    for (String longOpt : longOpts.keySet())
    {
        if (longOpt.startsWith(opt))
        {
            matchingOpts.add(longOpt);
        }
    }

    return matchingOpts;
}

Options options = new Options();
options.addOption(Option.builder().longOpt("importToOpen").hasArg().argName("FILE").build());
options.addOption(Option.builder("i").longOpt("import").hasArg().argName("FILE").build());
Parsing "--import=FILE" is not possible since 1.3 as it throws a AmbiguousOptionException stating that it cannot decide whether import is import or importToOpen. In 1.2 this is not an issue.
The root lies in the new DefaultParser which does a startsWith check internally.


private boolean isShortOption(String token)
{
    // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
    return token.startsWith("-") && token.length() >= 2 && options.hasShortOption(token.substring(1, 2));

    // remove leading "-" and "=value"
}

private boolean isShortOption(String token)
{
    // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
    if (!token.startsWith("-") || token.length() == 1)
    {
        return false;
    }

    // remove leading "-" and "=value"
    int pos = token.indexOf("=");
    String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);
    return options.hasShortOption(optName);
}

Optional argument picking up next regular option as its argument

I'm not sure if this is a complete fix.  It seems to miss the case where short options are concatenated after an option that takes an optional argument.

A failing test case for this would be to modify {{setUp()}} in BugCLI265Test.java to include short options "a" and "b":

{code:java}
    @Before
    public void setUp() throws Exception {
        parser = new DefaultParser();

        Option TYPE1 = Option.builder("t1").hasArg().numberOfArgs(1).optionalArg(true).argName("t1_path").build();
        Option OPTION_A = Option.builder("a").hasArg(false).build();
        Option OPTION_B = Option.builder("b").hasArg(false).build();
        Option LAST = Option.builder("last").hasArg(false).build();

        options = new Options().addOption(TYPE1).addOption(OPTION_A).addOption(OPTION_B).addOption(LAST);
    }
{code}

Add add a test for the concatenated options following an option with optional argument case:

{code:java}
    @Test
    public void shouldParseConcatenatedShortOptions() throws Exception {
      String[] concatenatedShortOptions = new String[] { "-t1", "-ab" };

      final CommandLine commandLine = parser.parse(options, concatenatedShortOptions);

      assertTrue(commandLine.hasOption("t1"));
      assertEquals(null, commandLine.getOptionValue("t1"));
      assertTrue(commandLine.hasOption("a"));
      assertTrue(commandLine.hasOption("b"));
      assertFalse(commandLine.hasOption("last"));
    }
{code}

One possible fix is to check that at least the first character of the option is a short option if all the other cases fail in {{isShortOption(...)}} like so:

{code:java}
    private boolean isShortOption(String token)
    {
        // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
        if (!token.startsWith("-") || token.length() == 1)
        {
            return false;
        }

        // remove leading "-" and "=value"
        int pos = token.indexOf("=");
        String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);
        if (options.hasShortOption(optName))
        {
            return true;
        }
        return optName.length() > 0 && options.hasShortOption(String.valueOf(optName.charAt(0)));
    }
{code}


private boolean isShortOption(String token)
{
    // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
    if (!token.startsWith("-") || token.length() == 1)
    {
        return false;
    }

    // remove leading "-" and "=value"
    int pos = token.indexOf("=");
    String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);
    return options.hasShortOption(optName);
    // check for several concatenated short options
}

private boolean isShortOption(String token)
{
    // short options (-S, -SV, -S=V, -SV1=V2, -S1S2)
    if (!token.startsWith("-") || token.length() == 1)
    {
        return false;
    }

    // remove leading "-" and "=value"
    int pos = token.indexOf("=");
    String optName = pos == -1 ? token.substring(1) : token.substring(1, pos);
    if (options.hasShortOption(optName))
    {
        return true;
    }
    // check for several concatenated short options
    return optName.length() > 0 && options.hasShortOption(String.valueOf(optName.charAt(0)));
}

I have recently migrated a project from CLI 1.2 to 1.3.1 and have encountered what may be a bug or difference in the way optional arguments are being processed.

I have a command that opens several different kinds of databases by type, or alternately, the last opened database of that type.

Option TYPE1 = Option.builder("t1").hasArg().numberOfArgs(1).optionalArg(true).argName("t1_path").build();
Option TYPE2 = Option.builder("t2").hasArg().numberOfArgs(1).optionalArg(true).argName("t2_path").build();
Option LAST  =  Option.builder("last").hasArg(false).build();

Commands then look like "open -t1 path/to/my/db" or "open -t1 -last"

If I use the now deprecated GnuParser, both commands work as expected.  However, if I use the new DefaultParser, for the 2nd example, it thinks "-last" is the argument for -t1 rather than an option in its own right.

I added the numberOfArgs(1) after reading a post on StackOverflow, but it made no difference in the behavior.  Only switching back to the GnuParser seemed to work.


public static Object createValue(final String str, final Class<?> clazz) throws ParseException
    {
        if (PatternOptionBuilder.STRING_VALUE == clazz)
        {
            return str;
        }
        else if (PatternOptionBuilder.OBJECT_VALUE == clazz)
        {
            return createObject(str);
        }
        else if (PatternOptionBuilder.NUMBER_VALUE == clazz)
        {
            return createNumber(str);
        }
        else if (PatternOptionBuilder.DATE_VALUE == clazz)
        {
            return createDate(str);
        }
        else if (PatternOptionBuilder.CLASS_VALUE == clazz)
        {
            return createClass(str);
        }
        else if (PatternOptionBuilder.FILE_VALUE == clazz)
        {
            return createFile(str);
        }
        else if (PatternOptionBuilder.EXISTING_FILE_VALUE == clazz)
        {
            return createFile(str);
        }
        else if (PatternOptionBuilder.FILES_VALUE == clazz)
        {
            return createFiles(str);
        }
        else if (PatternOptionBuilder.URL_VALUE == clazz)
        {
            return createURL(str);
        }
        else
        {
            return null;
        }
    }

public static Object createValue(final String str, final Class<?> clazz) throws ParseException
    {
        if (PatternOptionBuilder.STRING_VALUE == clazz)
        {
            return str;
        }
        else if (PatternOptionBuilder.OBJECT_VALUE == clazz)
        {
            return createObject(str);
        }
        else if (PatternOptionBuilder.NUMBER_VALUE == clazz)
        {
            return createNumber(str);
        }
        else if (PatternOptionBuilder.DATE_VALUE == clazz)
        {
            return createDate(str);
        }
        else if (PatternOptionBuilder.CLASS_VALUE == clazz)
        {
            return createClass(str);
        }
        else if (PatternOptionBuilder.FILE_VALUE == clazz)
        {
            return createFile(str);
        }
        else if (PatternOptionBuilder.EXISTING_FILE_VALUE == clazz)
        {
            return openFile(str);
        }
        else if (PatternOptionBuilder.FILES_VALUE == clazz)
        {
            return createFiles(str);
        }
        else if (PatternOptionBuilder.URL_VALUE == clazz)
        {
            return createURL(str);
        }
        else
        {
            return null;
        }
    }

Option parser type EXISTING_FILE_VALUE not check file existing

When the user pass option type FileInputStream.class, I think the expected behavior for the return value is the same type, which the user passed.

Options options = new Options();
options.addOption(Option.builder("f").hasArg().type(FileInputStream.class).build());
CommandLine cline = new DefaultParser().parse(options, args);
FileInputStream file = (FileInputStream) cline.getParsedOptionValue("f"); // it returns "File" object, without check File exist.

I attach a solution for it:
https://github.com/schaumb/commons-cli/commit/abfcc8211f529ab75f3b3edd4a827e484109eb0b


private void checkRequiredOptions()
    throws MissingOptionException
{
    // if there are required options that have not been
    // processsed
    if (requiredOptions.size() > 0)
    {
        Iterator iter = requiredOptions.iterator();
        StringBuffer buff = new StringBuffer();

        // loop through the required options
        while (iter.hasNext())
        {
            buff.append(iter.next());
        }

        throw new MissingOptionException(buff.toString());
    }
}

private void checkRequiredOptions()
    throws MissingOptionException
{
    // if there are required options that have not been
    // processsed
    if (requiredOptions.size() > 0)
    {
        Iterator iter = requiredOptions.iterator();
        StringBuffer buff = new StringBuffer("Missing required option");
        buff.append(requiredOptions.size() == 1 ? "" : "s");
        buff.append(": ");

        // loop through the required options
        while (iter.hasNext())
        {
            buff.append(iter.next());
        }

        throw new MissingOptionException(buff.toString());
    }
}

This was posted on the Commons-Developer list and confirmed as a bug.
> Is this a bug?  Or am I using this incorrectly?
> I have an option with short and long values.  Given code that is
> essentially what is below, with a PosixParser I see results as
> follows:
>
> A command line with just "-t" prints out the results of the catch
> block
> (OK)
> A command line with just "-target" prints out the results of the catch
> block (OK)
> A command line with just "-t foobar.com" prints out "processing selected
> target: foobar.com" (OK)
> A command line with just "-target foobar.com" prints out "processing
> selected target: arget" (ERROR?)
>
> ======================================================================
> ==
> =======================
>   private static final String OPTION_TARGET = "t";
>   private static final String OPTION_TARGET_LONG = "target";
>   // ...
>   Option generateTarget = new Option(OPTION_TARGET,
>                                      OPTION_TARGET_LONG,
>                                      true,
>                                      "Generate files for the specified
> target machine");
>   // ...
>   try
{
>         parsedLine = parser.parse(cmdLineOpts, args);
>       }
 catch (ParseException pe)
{
>         System.out.println("Invalid command: " + pe.getMessage() +
> "\n");
>         HelpFormatter hf = new HelpFormatter();
>         hf.printHelp(USAGE, cmdLineOpts);
>         System.exit(-1);
>       }
>
>   if (parsedLine.hasOption(OPTION_TARGET))
{
>     System.out.println("processing selected target: " +
> parsedLine.getOptionValue(OPTION_TARGET));
>   }

It is a bug but it is due to well defined behaviour (so that makes me feel a
little better about myself .  To support special
(well I call them special anyway) like -Dsystem.property=value we need to be
able to examine the first character of an option.  If the first character is
itself defined as an Option then the remainder of the token is used as the
value, e.g. 'D' is the token, it is an option so 'system.property=value' is the
argument value for that option.  This is the behaviour that we are seeing for
your example.
't' is the token, it is an options so 'arget' is the argument value.
I suppose a solution to this could be to have a way to specify properties for
parsers.  In this case 'posix.special.option == true' for turning
on special options. I'll have a look into this and let you know.
Just to keep track of this and to get you used to how we operate, can you log a
bug in bugzilla for this.
Thanks,
-John K


public static <T> T createValue(final String str, final Class<T> clazz) throws ParseException
{
    if (PatternOptionBuilder.STRING_VALUE == clazz)
    {
        return (T) str;
    }
    else if (PatternOptionBuilder.OBJECT_VALUE == clazz)
    {
        return (T) createObject(str);
    }
    else if (PatternOptionBuilder.NUMBER_VALUE == clazz)
    {
        return (T) createNumber(str);
    }
    else if (PatternOptionBuilder.DATE_VALUE == clazz)
    {
        return (T) createDate(str);
    }
    else if (PatternOptionBuilder.CLASS_VALUE == clazz)
    {
        return (T) createClass(str);
    }
    else if (PatternOptionBuilder.FILE_VALUE == clazz)
    {
        return (T) createFile(str);
    }
    else if (PatternOptionBuilder.EXISTING_FILE_VALUE == clazz)
    {
        return (T) openFile(str);
    }
    else if (PatternOptionBuilder.FILES_VALUE == clazz)
    {
        return (T) createFiles(str);
    }
    else if (PatternOptionBuilder.URL_VALUE == clazz)
    {
        return (T) createURL(str);
    }
    else
    {
        return null;
    }
}

public static <T> T createValue(final String str, final Class<T> clazz) throws ParseException
{
    if (PatternOptionBuilder.STRING_VALUE == clazz)
    {
        return (T) str;
    }
    else if (PatternOptionBuilder.OBJECT_VALUE == clazz)
    {
        return (T) createObject(str);
    }
    else if (PatternOptionBuilder.NUMBER_VALUE == clazz)
    {
        return (T) createNumber(str);
    }
    else if (PatternOptionBuilder.DATE_VALUE == clazz)
    {
        return (T) createDate(str);
    }
    else if (PatternOptionBuilder.CLASS_VALUE == clazz)
    {
        return (T) createClass(str);
    }
    else if (PatternOptionBuilder.FILE_VALUE == clazz)
    {
        return (T) createFile(str);
    }
    else if (PatternOptionBuilder.EXISTING_FILE_VALUE == clazz)
    {
        return (T) openFile(str);
    }
    else if (PatternOptionBuilder.FILES_VALUE == clazz)
    {
        return (T) createFiles(str);
    }
    else if (PatternOptionBuilder.URL_VALUE == clazz)
    {
        return (T) createURL(str);
    }
    else
    {
        throw new ParseException("Unable to handle the class: " + clazz);
    }
}

TypeHandler should throw ParseException for an unsupported class

JavaDoc for TypeHandler states that createValue will

* @throws ParseException if the value creation for the given object type failedtype

 However createValue(String str, Class<?> clazz) will return null if the clazz is unknown.


static String stripLeadingHyphens(String str)
{
    if (str.startsWith("--"))
    {
        return str.substring(2, str.length());
    }
    else if (str.startsWith("-"))
    {
        return str.substring(1, str.length());
    }

    return str;
}

static String stripLeadingHyphens(String str)
{
    if (str == null) {
        return null;
    }
    if (str.startsWith("--"))
    {
        return str.substring(2, str.length());
    }
    else if (str.startsWith("-"))
    {
        return str.substring(1, str.length());
    }

    return str;
}

NullPointerException in Util.stripLeadingHyphens when passed a null argument

If you try to do a hasOption(null), you get a NPE:
java.lang.NullPointerException
	at org.apache.commons.cli.Util.stripLeadingHyphens(Util.java:39)
	at org.apache.commons.cli.CommandLine.resolveOption(CommandLine.java:166)
	at org.apache.commons.cli.CommandLine.hasOption(CommandLine.java:68)
Either hasOption should reject the null argument, or the function should simply return false.  I think the latter makes more since, as this is how Java collections generally work.


protected StringBuffer renderWrappedText(StringBuffer sb, int width,
                                         int nextLineTabStop, String text)
{
    int pos = findWrapPos(text, width, 0);

    if (pos == -1)
    {
        sb.append(rtrim(text));

        return sb;
    }
    sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);

    // all following lines must be padded with nextLineTabStop space
    // characters
    final String padding = createPadding(nextLineTabStop);

    while (true)
    {
        text = padding + text.substring(pos).trim();
        pos = findWrapPos(text, width, nextLineTabStop);

        if (pos == -1)
        {
            sb.append(text);

            return sb;
        }

        sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
    }
}

protected StringBuffer renderWrappedText(StringBuffer sb, int width,
                                         int nextLineTabStop, String text)
{
    int pos = findWrapPos(text, width, 0);

    if (pos == -1)
    {
        sb.append(rtrim(text));

        return sb;
    }
    sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);

    // all following lines must be padded with nextLineTabStop space
    // characters
    final String padding = createPadding(nextLineTabStop);

    while (true)
    {
        text = padding + text.substring(pos).trim();
        pos = findWrapPos(text, width, 0);

        if (pos == -1)
        {
            sb.append(text);

            return sb;
        }

        sb.append(rtrim(text.substring(0, pos))).append(defaultNewLine);
    }
}

HelpFormatter wraps incorrectly on every line beyond the first

The method findWrapPos(...) in the HelpFormatter is a couple of bugs in the way that it deals with the "startPos" variable.  This causes it to format every line beyond the first line by "startPos" to many characters, beyond the specified width.
To see this, create an option with a long description, and then use the help formatter to print it.  The first line will be the correct length.  The 2nd, 3rd, etc lines will all be too long.
I don't have a patch (sorry) - but here is a corrected version of the method.
I fixed it in two places - both were using "width + startPos" when they should have been using width.

 protected int findWrapPos(String text, int width, int startPos)
    {
        int pos = -1;

        // the line ends before the max wrap pos or a new line char found
        if (((pos = text.indexOf('\n', startPos)) != -1 && pos <= width)
            || ((pos = text.indexOf('\t', startPos)) != -1 && pos <= width))
        {
            return pos+1;
        }
        else if ((width) >= text.length())
        {
            return -1;
        }

        // look for the last whitespace character before startPos+width
        pos = width;

        char c;

        while ((pos >= startPos) && ((c = text.charAt(pos)) != ' ')
               && (c != '\n') && (c != '\r'))
        {
            --pos;
        }

        // if we found it - just return
        if (pos > startPos)
        {
            return pos;
        }

        // must look for the first whitespace chearacter after startPos
        // + width
        pos = startPos + width;

        while ((pos <= text.length()) && ((c = text.charAt(pos)) != ' ')
               && (c != '\n') && (c != '\r'))
        {
            ++pos;
        }

        return (pos == text.length())        ? (-1) : pos;
    }


protected void checkRequiredOptions()
    throws MissingOptionException
{
    // if there are required options that have not been
    // processsed
    if (getRequiredOptions().size() > 0)
    {
        Iterator iter = getRequiredOptions().iterator();
        StringBuffer buff = new StringBuffer("Missing required option");
        buff.append(getRequiredOptions().size() == 1 ? "" : "s");
        buff.append(": ");

        // loop through the required options
        while (iter.hasNext())
        {
            buff.append(iter.next());
        }

        throw new MissingOptionException(buff.toString());
    }
}

protected void checkRequiredOptions()
    throws MissingOptionException
{
    // if there are required options that have not been
    // processsed
    if (getRequiredOptions().size() > 0)
    {
        Iterator iter = getRequiredOptions().iterator();
        StringBuffer buff = new StringBuffer("Missing required option");
        buff.append(getRequiredOptions().size() == 1 ? "" : "s");
        buff.append(": ");

        // loop through the required options
        while (iter.hasNext())
        {
            buff.append(iter.next());
            buff.append(", ");
        }

        throw new MissingOptionException(buff.substring(0, buff.length() - 2));
    }
}

MissingOptionException.getMessage() changed from CLI 1.0 > 1.1

The MissingOptionException.getMessage() string changed from CLI 1.0 > 1.1.
CLI 1.0 was poorly formatted but readable:
Missing required options: -format-source-properties
CLI 1.1 is almost unreadable:
Missing required options: formatsourceproperties
In CLI 1.0 Options.addOption(Option) prefixed the stored options with a "-" and in CLI 1.1 it doesn't.
I would suggest changing Parser.checkRequiredOptions() to add the options to the error message with a prefix of " -":
OLD:
            // loop through the required options
            while (iter.hasNext())
            {
                buff.append(iter.next());
            }

NEW:
            // loop through the required options
            while (iter.hasNext())
            {
                buff.append(" -" + iter.next());
            }

Resulting in:
Missing required options: -format -source -properties


private void removeUnreferencedFunctionArgs(Scope fnScope) {
  // Notice that removing unreferenced function args breaks
  // Function.prototype.length. In advanced mode, we don't really care
  // about this: we consider "length" the equivalent of reflecting on
  // the function's lexical source.
  //
  // Rather than create a new option for this, we assume that if the user
  // is removing globals, then it's OK to remove unused function args.
  //
  // See http://code.google.com/p/closure-compiler/issues/detail?id=253

  Node function = fnScope.getRootNode();

  Preconditions.checkState(function.isFunction());
  if (NodeUtil.isGetOrSetKey(function.getParent())) {
    // The parameters object literal setters can not be removed.
    return;
  }

  Node argList = getFunctionArgList(function);
  boolean modifyCallers = modifyCallSites
      && callSiteOptimizer.canModifyCallers(function);
  if (!modifyCallers) {
    // Strip unreferenced args off the end of the function declaration.
    Node lastArg;
    while ((lastArg = argList.getLastChild()) != null) {
      Var var = fnScope.getVar(lastArg.getString());
      if (!referenced.contains(var)) {
        argList.removeChild(lastArg);
        compiler.reportCodeChange();
      } else {
        break;
      }
    }
  } else {
    callSiteOptimizer.optimize(fnScope, referenced);
  }
}

private void removeUnreferencedFunctionArgs(Scope fnScope) {
  // Notice that removing unreferenced function args breaks
  // Function.prototype.length. In advanced mode, we don't really care
  // about this: we consider "length" the equivalent of reflecting on
  // the function's lexical source.
  //
  // Rather than create a new option for this, we assume that if the user
  // is removing globals, then it's OK to remove unused function args.
  //
  // See http://code.google.com/p/closure-compiler/issues/detail?id=253
  if (!removeGlobals) {
    return;
  }

  Node function = fnScope.getRootNode();

  Preconditions.checkState(function.isFunction());
  if (NodeUtil.isGetOrSetKey(function.getParent())) {
    // The parameters object literal setters can not be removed.
    return;
  }

  Node argList = getFunctionArgList(function);
  boolean modifyCallers = modifyCallSites
      && callSiteOptimizer.canModifyCallers(function);
  if (!modifyCallers) {
    // Strip unreferenced args off the end of the function declaration.
    Node lastArg;
    while ((lastArg = argList.getLastChild()) != null) {
      Var var = fnScope.getVar(lastArg.getString());
      if (!referenced.contains(var)) {
        argList.removeChild(lastArg);
        compiler.reportCodeChange();
      } else {
        break;
      }
    }
  } else {
    callSiteOptimizer.optimize(fnScope, referenced);
  }
}

function arguments should not be optimized away

Function arguments should not be optimized away, as this comprimizes the function's length property.

What steps will reproduce the problem?

// ==ClosureCompiler==
// @compilation_level SIMPLE_OPTIMIZATIONS
// @output_file_name default.js
// ==/ClosureCompiler==
function foo (bar, baz) {
  return bar;
}
alert (foo.length);
function foo (bar, baz) {
  return bar;
}
alert (foo.length);

--------------------------------------

What is the expected output?

function foo(a,b){return a}alert(foo.length);

--------------------------------------

What do you see instead?

function foo(a){return a}alert(foo.length);

--------------------------------------

What version of the product are you using? On what operating system?

I'm using the product from the web page http://closure-compiler.appspot.com/home

I'm using Firefox 3.6.10 on Ubuntu 10.0.4

Please provide any additional information below.

The function's length property is essential to many techniques, such as currying functions.


static boolean mayBeString(Node n, boolean recurse) {
  if (recurse) {
    return allResultsMatch(n, MAY_BE_STRING_PREDICATE);
  } else {
    return mayBeStringHelper(n);
  }
}

static boolean mayBeString(Node n, boolean recurse) {
  if (recurse) {
    return anyResultsMatch(n, MAY_BE_STRING_PREDICATE);
  } else {
    return mayBeStringHelper(n);
  }
}

Wrong code generated if mixing types in ternary operator

What steps will reproduce the problem?
1. Use Google Closure Compiler to compile this code:

    var a =(Math.random()>0.5? '1' : 2 ) + 3 + 4;

You can either simple or advanced. It doesn't matter

What is the expected output? What do you see instead?

I'm seeing this as a result:
    var a = (0.5 < Math.random() ? 1 : 2) + 7;

This is obviously wrong as the '1' string literal got converted to a number, and 3+4 got combined into 7 while that's not ok as '1' + 3 + 4 = '134', not '17'.

What version of the product are you using? On what operating system?
Please provide any additional information below.

Seems like this issue happens only when you are mixing types together. If both 1 and 2 are string literals or if they are both numbers it won't happen. I was also a little surprised to see this happening in simple mode as it actually breaks the behavior.


protected CompilerOptions createOptions() {
  CompilerOptions options = new CompilerOptions();
  options.setCodingConvention(new ClosureCodingConvention());
  CompilationLevel level = flags.compilation_level;
  level.setOptionsForCompilationLevel(options);
  if (flags.debug) {
    level.setDebugOptionsForCompilationLevel(options);
  }

  WarningLevel wLevel = flags.warning_level;
  wLevel.setOptionsForWarningLevel(options);
  for (FormattingOption formattingOption : flags.formatting) {
    formattingOption.applyToOptions(options);
  }
  if (flags.process_closure_primitives) {
    options.closurePass = true;
  }

  initOptionsFromFlags(options);
  return options;
}

protected CompilerOptions createOptions() {
  CompilerOptions options = new CompilerOptions();
  options.setCodingConvention(new ClosureCodingConvention());
  CompilationLevel level = flags.compilation_level;
  level.setOptionsForCompilationLevel(options);
  if (flags.debug) {
    level.setDebugOptionsForCompilationLevel(options);
  }

  WarningLevel wLevel = flags.warning_level;
  wLevel.setOptionsForWarningLevel(options);
  for (FormattingOption formattingOption : flags.formatting) {
    formattingOption.applyToOptions(options);
  }

  options.closurePass = flags.process_closure_primitives;
  initOptionsFromFlags(options);
  return options;
}

--process_closure_primitives can't be set to false

What steps will reproduce the problem?
1. compile a file with "--process_closure_primitives false"
2. compile a file with "--process_closure_primitives true" (default)
3. result: primitives are processed in both cases.

What is the expected output? What do you see instead?
The file should still have its goog.provide/require tags in place.
Instead they are processed.

What version of the product are you using? On what operating system?
current SVN (also tried two of the preceding binary releases with same
result)

Please provide any additional information below.
Flag can't be set to false due to a missing "else" in the command-line
parser.


public void process(Node externs, Node root) {
  NodeTraversal.traverse(compiler, root, this);
  if (MAKE_LOCAL_NAMES_UNIQUE) {
    MakeDeclaredNamesUnique renamer = new MakeDeclaredNamesUnique();
    NodeTraversal t = new NodeTraversal(compiler, renamer);
    t.traverseRoots(externs, root);
  }
  removeDuplicateDeclarations(root);
  new PropogateConstantAnnotations(compiler, assertOnChange)
      .process(externs, root);
}

public void process(Node externs, Node root) {
  NodeTraversal.traverse(compiler, root, this);
  removeDuplicateDeclarations(root);
  if (MAKE_LOCAL_NAMES_UNIQUE) {
    MakeDeclaredNamesUnique renamer = new MakeDeclaredNamesUnique();
    NodeTraversal t = new NodeTraversal(compiler, renamer);
    t.traverseRoots(externs, root);
  }
  new PropogateConstantAnnotations(compiler, assertOnChange)
      .process(externs, root);
}

compiler assumes that 'arguments' can be shadowed

The code:
function name() {
   var arguments = Array.prototype.slice.call(arguments, 0);
}

gets compiled to:
function name(){ var c=Array.prototype.slice.call(c,0); }

Thanks to tescosquirrel for the report.


JSType meet(JSType that) {
  UnionTypeBuilder builder = new UnionTypeBuilder(registry);
  for (JSType alternate : alternates) {
    if (alternate.isSubtype(that)) {
      builder.addAlternate(alternate);
    }
  }

  if (that instanceof UnionType) {
    for (JSType otherAlternate : ((UnionType) that).alternates) {
      if (otherAlternate.isSubtype(this)) {
        builder.addAlternate(otherAlternate);
      }
    }
  } else if (that.isSubtype(this)) {
    builder.addAlternate(that);
  }
  JSType result = builder.build();
  if (result != null) {
    return result;
  } else if (this.isObject() && that.isObject()) {
    return getNativeType(JSTypeNative.NO_OBJECT_TYPE);
  } else {
    return getNativeType(JSTypeNative.NO_TYPE);
  }
}

JSType meet(JSType that) {
  UnionTypeBuilder builder = new UnionTypeBuilder(registry);
  for (JSType alternate : alternates) {
    if (alternate.isSubtype(that)) {
      builder.addAlternate(alternate);
    }
  }

  if (that instanceof UnionType) {
    for (JSType otherAlternate : ((UnionType) that).alternates) {
      if (otherAlternate.isSubtype(this)) {
        builder.addAlternate(otherAlternate);
      }
    }
  } else if (that.isSubtype(this)) {
    builder.addAlternate(that);
  }
  JSType result = builder.build();
  if (!result.isNoType()) {
    return result;
  } else if (this.isObject() && that.isObject()) {
    return getNativeType(JSTypeNative.NO_OBJECT_TYPE);
  } else {
    return getNativeType(JSTypeNative.NO_TYPE);
  }
}

Line 354:
CanvasRenderingContext2D.prototype.globalCompositingOperation;

Line 366:
CanvasRenderingContext2D.prototype.mitreLimit;

They should be globalCompositeOperation and miterLimit, respectively.


protected CompilerOptions createOptions() {
  CompilerOptions options = new CompilerOptions();
  if (flags.processJqueryPrimitives) {
    options.setCodingConvention(new JqueryCodingConvention());
  } else {
    options.setCodingConvention(new ClosureCodingConvention());
  }

  options.setExtraAnnotationNames(flags.extraAnnotationName);

  CompilationLevel level = flags.compilationLevel;
  level.setOptionsForCompilationLevel(options);

  if (flags.debug) {
    level.setDebugOptionsForCompilationLevel(options);
  }

  if (flags.useTypesForOptimization) {
    level.setTypeBasedOptimizationOptions(options);
  }

  if (flags.generateExports) {
    options.setGenerateExports(flags.generateExports);
  }

  WarningLevel wLevel = flags.warningLevel;
  wLevel.setOptionsForWarningLevel(options);
  for (FormattingOption formattingOption : flags.formatting) {
    formattingOption.applyToOptions(options);
  }

  options.closurePass = flags.processClosurePrimitives;

  options.jqueryPass = CompilationLevel.ADVANCED_OPTIMIZATIONS == level &&
      flags.processJqueryPrimitives;

  options.angularPass = flags.angularPass;

  if (!flags.translationsFile.isEmpty()) {
    try {
      options.messageBundle = new XtbMessageBundle(
          new FileInputStream(flags.translationsFile),
          flags.translationsProject);
    } catch (IOException e) {
      throw new RuntimeException("Reading XTB file", e);
    }
  } else if (CompilationLevel.ADVANCED_OPTIMIZATIONS == level) {
    // In SIMPLE or WHITESPACE mode, if the user hasn't specified a
    // translations file, they might reasonably try to write their own
    // implementation of goog.getMsg that makes the substitution at
    // run-time.
    //
    // In ADVANCED mode, goog.getMsg is going to be renamed anyway,
    // so we might as well inline it. But shut off the i18n warnings,
    // because the user didn't really ask for i18n.
    options.messageBundle = new EmptyMessageBundle();
  }

  return options;
}

protected CompilerOptions createOptions() {
  CompilerOptions options = new CompilerOptions();
  if (flags.processJqueryPrimitives) {
    options.setCodingConvention(new JqueryCodingConvention());
  } else {
    options.setCodingConvention(new ClosureCodingConvention());
  }

  options.setExtraAnnotationNames(flags.extraAnnotationName);

  CompilationLevel level = flags.compilationLevel;
  level.setOptionsForCompilationLevel(options);

  if (flags.debug) {
    level.setDebugOptionsForCompilationLevel(options);
  }

  if (flags.useTypesForOptimization) {
    level.setTypeBasedOptimizationOptions(options);
  }

  if (flags.generateExports) {
    options.setGenerateExports(flags.generateExports);
  }

  WarningLevel wLevel = flags.warningLevel;
  wLevel.setOptionsForWarningLevel(options);
  for (FormattingOption formattingOption : flags.formatting) {
    formattingOption.applyToOptions(options);
  }

  options.closurePass = flags.processClosurePrimitives;

  options.jqueryPass = CompilationLevel.ADVANCED_OPTIMIZATIONS == level &&
      flags.processJqueryPrimitives;

  options.angularPass = flags.angularPass;

  if (!flags.translationsFile.isEmpty()) {
    try {
      options.messageBundle = new XtbMessageBundle(
          new FileInputStream(flags.translationsFile),
          flags.translationsProject);
    } catch (IOException e) {
      throw new RuntimeException("Reading XTB file", e);
    }
  } else if (CompilationLevel.ADVANCED_OPTIMIZATIONS == level) {
    // In SIMPLE or WHITESPACE mode, if the user hasn't specified a
    // translations file, they might reasonably try to write their own
    // implementation of goog.getMsg that makes the substitution at
    // run-time.
    //
    // In ADVANCED mode, goog.getMsg is going to be renamed anyway,
    // so we might as well inline it. But shut off the i18n warnings,
    // because the user didn't really ask for i18n.
    options.messageBundle = new EmptyMessageBundle();
    options.setWarningLevel(JsMessageVisitor.MSG_CONVENTIONS, CheckLevel.OFF);
  }

  return options;
}

Variable names prefixed with MSG_ cause error with advanced optimizations

Variables named something with MSG_ seem to cause problems with the module system, even if no modules are used in the code.

$ echo "var MSG_foo='bar'" | closure --compilation_level ADVANCED_OPTIMIZATIONS
stdin:1: ERROR - message not initialized using goog.getMsg
var MSG_foo='bar'
    ^

It works fine with msg_foo, MSG2_foo, etc.


private Node parseContextTypeExpression(JsDocToken token) {
        return parseTypeName(token);
}

private Node parseContextTypeExpression(JsDocToken token) {
  if (token == JsDocToken.QMARK) {
    return newNode(Token.QMARK);
  } else {
    return parseBasicTypeExpression(token);
  }
}

Constructor types that return all or unknown fail to parse

Constructor types that return the all type or the unknown type currently fail to parse:

/** @type {function(new:?)} */ var foo = function() {};
/** @type {function(new:*)} */ var bar = function() {};

foo.js:1: ERROR - Bad type annotation. type not recognized due to syntax error
/** @type {function(new:?)} */ var foo = function() {};
                        ^

foo.js:2: ERROR - Bad type annotation. type not recognized due to syntax error
/** @type {function(new:*)} */ var bar = function() {};
                        ^

This is an issue for a code generator that I'm working on.