/*
* Parts of this code are:
* Copyright 2002 Sun Microsystems, Inc. All Rights Reserved.
*
* The contents of this file are subject to the Sun Community
* Source License v 3.0/Jini Technology Specific Attachment v1.0
* (the "License"). You may not use this file except in compliance
* with the License. You may obtain a copy of the License
* at http://www.sun.com/jini/ . Software distributed under the
* License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF
* ANY KIND, either express or implied. See the License for the
* specific language governing rights and limitations under the
* License.
*
* The Reference Code is Jini Technology Core Platform code, v
* 1.2. The Developer of the Reference Code is Sun Microsystems,
* Inc.
*
* Contributor(s): Sun Microsystems, Inc.
*
* The contents of this file comply with the Jini Technology Core
* Platform Compatibility Kit, v 1.2A.
*
* Tester(s): Sun Microsystems, Inc.
* Java 2 SDK, Standard Edition, Version 1.4.0 and Version 1.3.1_02 for Solaris SPARC/x86
* Java 2 SDK, Standard Edition, Version 1.4.0 and Version 1.3.1_02 for Linux (Intel x86)
* Java 2 SDK, Standard Edition, Version 1.4.0 and Version 1.3.1_02 for Microsoft Windows
*
*/
// Note: The license allows this use -arnold
package org.simplx.args;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Comparator;
import java.util.List;
import java.util.StringTokenizer;
/**
* This class parses a command line that uses multi-character options, such as
* {@code -verbose} or {@code -help}.
*
* To use {@code MainArgs}, create a {@code MainArgs} object with the array of
* strings you wish to parse (typically the array passed to the program's {@code
* main} method), and then consume options from it, providing default values in
* case the option is not specified by the user. When you have consumed all the
* options, you invoke the {@code MainArgs} object's {@link #getOperands} method
* to return the remaining operands on the command line. If ``{@code --}'' is
* specified it is neither an option nor an operand, just a separator between
* the two lists. The {@link ProgramOptionException} is used to signal errors in
* the construction of the strings, that is, a user error, such as specifying a
* option that takes an argument but forgetting to provide that argument.
*
* Here is an example:
*
* public static void main(String[] args) throws IOException {
* MainArgs line = new MainArgs("flicker", args);
* try {
* verbose = line.getBoolean("verbose");
* max = line.getInt("max", Integer.MAX_VALUE);
* Writer out = line.getWriter("out", (Writer) null);
* if (out != null)
* recordOut = new PrintWriter(out);
* String[] files = line.getOperands();
* for (String file : files) {
* flick(file);
* }
* } catch (HelpOnlyException e) {
* System.exit(0);
* } finally {
* if (recordOut != null)
* recordOut.close();
* }
* }
*
* This program has three possible options: - "verbose", which is a
* boolean that says whether to generate extra output. The field {@code verbose}
* will store whether this option was specified or not.
- "max", which is the
* maximum number of lines to process in each file. If no maximum is specified,
* then the maximum will be the largest possible integer, which in effect means
* "no maximum". The field {@code max} will store this value.
- "out", which
* names a file that will be used to record what happens. The field {@code
* recordOut} will be {@code null} if this is not specified, or a {@code
* PrintWriter} to that file if it is.
After any options, the arguments
* will contain files that need to be processed.
*
* So here is a possible invocation of the program:
*
* flicker -verbose -out history f1 f2 f3
*
* In this case, after processing the arguments, the {@code verbose} field will
* be {@code true}, there will be no maximum, and records will be made to the
* file "history". The operation will be run on three files: f1, f2, and f3.
*
* The following invocation will print out the usage:
*
* flicker -help
*
* This will print
*
* flicker [-verbose] [-max int] [-out file] [-help] file ...
*
*
* You must call {@link #getOperands} for proper behavior, even if you do not
* use any operands in your command. {@code getOperands} checks for several user
* errors, including unknown options. If you do not expect to use operands, you
* should check the return value of {@code getOperands} and complain if any are
* specified.
*
* The order that the options are gathered by "get" calls is not relevant. You
* can order them in any way that makes sense to you.
*
* The order that the user puts the options in is only relevant when the same
* option is fetched more than once. In this case, the first invocation will
* return the first value for the option, the second the second value, and so
* on. If you ask for the value of the option more times than the user provided
* it, your extra requests will act as if the option is not specified. (After
* all, it wasn't specified the third, fourth, and fifth times, for example.)
* You can use this to gather options in a loop, such as:
*
* In other words, "-verbose -out file" and "-out file -verbose" are the same.
* But "-user pat -user robin" and "-user robin -user pat" are not.
*
* You can use multiple invocations to set a verbosity level:
*
* MainArgs line = new MainArgs(args);
* int verbosity = 0;
* while (line.getBoolean("verbose"))
* verbosity++;
*
* or to collect a list of specifications:
*
* MainArgs line = new MainArgs(args);
* List users = new ArrayList();
* String user;
* while ((user = line.getString("user", null)) != null)
* users.add(user);
*
* No options can be consumed after {@code getOperands} is invoked. Failure to
* follow this rule is a programmer error that will result in an {@link
* IllegalStateException}.
*
* {@code MainArgs} provides you several methods to get I/O streams from the
* command line. If these do not suffice for your particular needs, you can get
* the argument as a {@code String} and do your own processing.
*
* Combined Multiple and Single Charater Options
*
* Many programs want to allow the user to specify single character shortcuts
* for the most common options. This class handles that by allowing the option
* specification to have both, separated by a {@code |} character. For example,
* {@code getBoolean("v|verbose")} means that the verbose option can be
* specified as either {@code -v} or {@code --verbose}. (In such commands,
* {@code "--"} is used for the multi-char version of the option.)
*
* If an option does not have a single-character version, you can simply leave
* out that part of the option specification: {@code "|version"} means that the
* option {@code --version} has no single character equivalent. Similary,
* {@code "v|"} means that there is no multi-character version of the {@code -v}
* option. (Single-character-only options are painful to the user and
* unnecessary, because there is an unlimited number of choices for
* multi-character equivalents.)
*
* Single character options can be combined in a shorter form. If the command
* has single character options {@code x}, {@code y}, and {@code z}, you can
* specify all three together, as in {@code -xyz}, which is equivalent to {@code
* -x -y -z}. If an option takes a parameter, you can merge it in as well. If
* the command also had a {@code o} option for an output file, you could say
* {@code -xyzofile}, {@code -xyzo file}, {@code -xyz -ofile}, or {@code -xyz -o
* file}.
*
* Usage Descriptions
*
* When you ask for the value of options, the object builds up knowledge of the
* expected usage. For example, when you call {@code getBoolean("verbose")},
* the class knows there is a boolean option named "verbose". From this kind of
* information, you can get a usage message for the user.
*
* @author Ken Arnold
* @see StringTokenizer
*/
public class MainArgs {
/** The args provided. */
private final String[] args;
/** The arguments ones have been used. */
private final BitSet used;
/** The list of known options for the usage message. */
private final List options;
/** The program name (if specified). */
private final String programName;
/** This command line specifies single character equivalents. */
private boolean hasSingles;
/** The current group name for options. */
private int curGroup;
/** The list of option group names. */
private final List groups;
/** Description of the overall program. */
private String[] programDesc = new String[0];
/** Has some description text. */
private boolean hasDescs;
/** The operands have been fetched via getOperands(). */
private boolean operandsFetched;
/** The operands description. */
private String[] operandsDesc;
private static int nextOptOrderNum = 0;
// I wouldn't do this stateful stuff if I could return more than one
// value from a method -- it didn't seem worth creating a new object
// to hold the necessary values on each call to findOpt(). So I've
// ensured that only one parsing method can be executing at a time
// and "returned" values via this side effect. YUCK!
private int foundStr; // found in which string
private String foundOpt; // which String was found
/**
* Creates a new {@link MainArgs} object that will return specified options,
* arguments, and operands. The program name will be the simple name of the
* class, that is, the class name with the package name stripped off.
*
* @param mainClass The class that has the {@code main} method.
* @param args The command line arguments.
*
* @see #MainArgs(String,String[])
*/
public MainArgs(Class mainClass, String... args) {
this(mainClass.getSimpleName(), args);
}
/**
* Creates a new {@link MainArgs} object that will return specified options,
* arguments, and operands. The {@code prog} parameter is the program name.
*
* @param programName The name to use for the program.
* @param args The command line arguments.
*/
public MainArgs(String programName, String... args) {
this.programName = programName;
this.args = args;
used = new BitSet(args.length);
options = new ArrayList();
curGroup = 0;
groups = new ArrayList();
groups.add("");
}
/**
* Add descriptive text for the program itself. This will be included in
* the usage message.
*
* @param desc The desriptive text. Each string will be shown on a line of
* its own.
*/
public void programDescription(String... desc) {
programDesc = desc;
hasDescs |= desc.length > 0;
}
/** Used to store known option types so we can generate a usage message. */
private class Opt {
/** This particular option has no single-char equivalent. */
static final char HAS_NO_SINGLE = '\uffff';
/** The option. */
final String multi;
/** The single char of it. */
final char single;
/** The argument type. */
final String argType;
/** Option can be specified more than once. */
boolean repeatable;
final String paramName;
final String[] desc;
final int group;
final int order;
Opt(String option, String argType, String... doc) {
this.argType = argType;
int or = option.indexOf('|');
if (or < 0) {
single = HAS_NO_SINGLE;
multi = option;
} else if (or == 0) {
single = HAS_NO_SINGLE;
hasSingles =
true; // this has no singles, but singles are specified
multi = option.substring(1);
} else if (or == 1) {
hasSingles = true;
single = option.charAt(or - 1);
multi = option.substring(2);
} else {
throw new IllegalArgumentException("'|' at illegal position");
}
group = curGroup;
order = nextOptOrderNum++;
if (argType == null) {
paramName = "";
desc = doc;
} else {
paramName = nullToEmpty(doc.length > 0 ? doc[0] : argType);
desc = (String[]) ArrayUtils.subarray(doc, 1, doc.length);
}
hasDescs |= desc.length > 0;
}
boolean matches(String arg) {
if (arg.charAt(0) != '-') {
return false;
}
int dashLen = 1;
if (hasSingles) {
if (arg.length() == 2 && arg.charAt(1) == single) {
return true;
}
if (arg.charAt(1) != '-') {
return false;
}
dashLen = 2;
}
return arg.length() - dashLen == multi.length() &&
arg.regionMatches(dashLen, multi, 0, multi.length());
}
String helpString(String prefix) {
StringBuilder sb = new StringBuilder();
sb.append(prefix);
toString(sb);
if (desc.length > 0) {
String spaces = prefix + StringUtils.repeat(" ", 16);
if (sb.length() < spaces.length()) {
sb.append(spaces.substring(sb.length()));
}
sb.append(desc[0]);
for (int i = 1; i < desc.length; i++) {
sb.append('\n');
sb.append(prefix).append(desc[i]);
}
}
sb.append('\n');
return sb.toString();
}
private void toString(StringBuilder sb) {
if (!hasSingles) {
sb.append('-').append(multi);
} else {
if (single != HAS_NO_SINGLE) {
sb.append('-').append(single).append(", ");
}
sb.append("--").append(multi);
}
if (argType != null && argType.length() != 0) {
sb.append(' ').append(paramName);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
toString(sb);
return sb.toString();
}
}
private static String nullToEmpty(String s) {
return s == null ? "" : s;
}
/**
* Sets the current option group. The option group name is used for usage
* messages. When you ask for the value of an option, that option is placed
* in the current option group. In a usage message, options will be shown by
* group, and within a group, in the order they are requested. You can set
* the option group back to one used previously.
*
* @param groupName The name of the current option group.
*/
public void optionGroup(String groupName) {
curGroup = groups.indexOf(groupName);
if (curGroup < 0) {
groups.add(groupName);
curGroup = groups.size() - 1;
}
hasDescs |= groups.size() > 0;
}
/**
* Returns {@code true} if the given option is specified on the command
* line.
*
* @param option The name of the option.
* @param doc Usage documentation for the option. Each string will be
* printed on its own line, exept the first which will be put
* on the same line as the option itself if it fits.
*
* @return {@code true} if the option is specified; otherwise {@code
* false}.
*/
public boolean getBoolean(String option, String... doc) {
Opt o = addOpt(option, null, doc);
boolean retval = false;
if (findOpt(o, option)) {
retval = true;
}
return retval;
}
/**
* Returns the argument for the given option. This is a workhorse routine
* shared by all the methods that get options with arguments.
*
* @param option The name of the option.
* @param type The type of the option for the usage message.
* @param doc Usage documentation for the option.
*
* @return If the option has been specified and is still unused, returns the
* argument for the option; otherwise return {@code null}.
*
* @throws ProgramOptionException No argument was present.
*/
private String getArgument(String option, String type, String... doc)
throws ProgramOptionException {
Opt o = addOpt(option, type, doc);
if (findOpt(o, option)) {
return optArg();
}
return null;
}
/**
* Returns the argument of the given string option from the command line. If
* the option is not specified, return {@code defaultValue}.
*
* @param option The name of the option.
* @param defaultValue The value to return if the option is not specified.
* @param doc Usage documentation for the option. The first string
* will be the name of the parameter in the usage. If
* there are any other strings, each string will be
* printed on its own line, exept the first which will
* be put on the same line as the option itself if it
* fits.
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws ProgramOptionException No argument was present.
*/
public String getString(String option, String defaultValue, String... doc)
throws ProgramOptionException {
String str = getArgument(option, "str", doc);
return str != null ? str : defaultValue;
}
/**
* Returns the argument of the given {@code int} option from the command
* line. If the option is not specified, return {@code defaultValue}. The
* number is parsed according to {@link Integer#parseInt(String)}, except
* that a leading plus sign is accepted.
*
* @param option The name of the option.
* @param defaultValue The value to return if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws NumberFormatException The argument is not a valid number.
* @throws ProgramOptionException No argument was present.
*/
public int getInt(String option, int defaultValue, String... doc)
throws ProgramOptionException, NumberFormatException {
String str = getArgument(option, "int", doc);
try {
if (str == null) {
return defaultValue;
}
// ignore leading plus
if (str.length() > 0 && str.charAt(0) == '+') {
str = str.substring(1);
}
return Integer.decode(str);
} catch (NumberFormatException e) {
throw numException(e, option);
}
}
/**
* Returns the argument of the given {@code long} option from the command
* line. If the option is not specified, return {@code defaultValue}. The
* number is parsed according to {@link Long#decode(String)}, except that a
* leading plus sign is accepted.
*
* @param option The option specification.
* @param defaultValue The value to return if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws NumberFormatException The argument is not a valid number.
* @throws ProgramOptionException No argument was present.
*/
public long getLong(String option, long defaultValue, String... doc)
throws ProgramOptionException, NumberFormatException {
String str = getArgument(option, "long", doc);
try {
if (str == null) {
return defaultValue;
}
// ignore leading plus
if (str.length() > 0 && str.charAt(0) == '+') {
str = str.substring(1);
}
return Long.decode(str);
} catch (NumberFormatException e) {
throw numException(e, option);
}
}
/**
* Returns the value of the given {@code double} from the command line. If
* the option is not specified, return {@code defaultValue}. The number is
* parsed according to {@link Double#valueOf(String)}.
*
* @param option The option specification.
* @param defaultValue The value to return if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws NumberFormatException The argument is not a valid number.
* @throws ProgramOptionException No argument was present.
*/
public double getDouble(String option, double defaultValue, String... doc)
throws ProgramOptionException, NumberFormatException {
String str = getArgument(option, "val", doc);
try {
if (str == null) {
return defaultValue;
}
return Double.parseDouble(str);
} catch (NumberFormatException e) {
throw numException(e, option);
}
}
/**
* Returns a {@link Writer} that is the result of creating a new {@link
* FileWriter} object for the file named by the given option. If the option
* is {@code "-"}, the returned writer writes to {@code System.out}, and is
* not a {@link FileWriter}. (You can therefore test if it is
* writing to a file by checking if the returned writer is an instance of
* {@link FileWriter}.) If the option is not specified, return {@code
* defaultValue}.
*
* @param option The option specification.
* @param defaultValue The value to return if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public Writer getWriter(String option, Writer defaultValue, String... doc)
throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
return defaultValue;
}
if (path.equals("-")) {
return new OutputStreamWriter(System.out);
}
return new FileWriter(path);
} catch (IOException e) {
throw ioException(option, path, e);
}
}
/**
* Returns a {@link Writer} that is the result of creating a new {@link
* FileWriter} object for the file named by the given option. If the option
* is {@code "-"}, the returned writer writes to {@code System.out}, and is
* not a {@link FileWriter}. (You can therefore test if it is
* writing to a file by checking if the returned writer is an instance of
* {@link FileWriter}.) If the option is not specified, the string {@code
* path} is used as the file name. If {@code path} is {@code null} then
* {@code null} is returned.
*
* @param option The option specification.
* @param defaultPath The path to use if the option is not specified or
* {@code null} if no reader is to be returned in that
* case.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or the file specified by {@code
* defaultPath}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public Writer getWriter(String option, String defaultPath, String... doc)
throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
if (defaultPath == null) {
return null;
}
path = defaultPath;
}
if (path.equals("-")) {
return new OutputStreamWriter(System.out);
}
return new FileWriter(path);
} catch (IOException e) {
throw ioException(option, path, e);
}
}
/**
* Returns a {@link Reader} that is the result of creating a new {@link
* FileReader} object for the file named by the given option. If the option
* is {@code "-"}, the returned reader reads from {@code System.in}, and is
* not a {@link FileReader}. (You can therefore test if it is
* reading from a file by checking if the returned reader is an instance of
* {@link FileReader}.) If the option is not specified, returns {@code
* defaultValue}.
*
* @param option The option specification.
* @param defaultValue The value to return if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
public Reader getReader(String option, Reader defaultValue, String... doc)
throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
return defaultValue;
}
if (path.equals("-")) {
return new InputStreamReader(System.in);
}
return new FileReader(path);
} catch (FileNotFoundException e) {
throw ioException(option, path, e);
}
}
/**
* Returns a {@link Reader} that is the result of creating a new {@link
* FileReader} object for the file named by the given option. If the option
* is {@code "-"}, the returned reader reads from {@code System.in}, and is
* not a {@link FileReader}. (You can therefore test if it is
* reading from a file by checking if the returned reader is an instance of
* {@link FileReader}.) If the option is not specified, the string {@code
* path} is used as the file name. If {@code path} is {@code null} then
* {@code null} is returned.
*
* @param option The option specification.
* @param defaultPath The path to use if the option is not specified or
* {@code null} if no reader is to be returned in that
* case.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or the file specified by {@code
* defaultPath}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
public Reader getReader(String option, String defaultPath, String... doc)
throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
if (defaultPath == null) {
return null;
}
path = defaultPath;
}
if (path.equals("-")) {
return new InputStreamReader(System.in);
}
return new FileReader(path);
} catch (FileNotFoundException e) {
throw ioException(option, path, e);
}
}
/**
* Returns an {@link OutputStream} that is the result of creating a new
* {@link FileOutputStream} object for the file named by the given option.
* If the option is {@code "-"}, the returned stream writes to {@code
* System.out}, and is not a {@link FileOutputStream}. (You can
* therefore test if it is writing to a file by checking if the returned
* stream is an instance of {@link FileOutputStream}.) If the option is not
* specified, returns {@code defaultValue}.
*
* @param option The option specification.
* @param defaultValue The value to return if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public OutputStream getOutputStream(String option,
OutputStream defaultValue, String... doc)
throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
return defaultValue;
}
if (path.equals("-")) {
return System.out;
}
return new FileOutputStream(path);
} catch (FileNotFoundException e) {
throw ioException(option, path, e);
}
}
/**
* Returns an {@link OutputStream} that is the result of creating a new
* {@link FileOutputStream} object for the file named by the given option.
* If the option is {@code "-"}, the returned stream writes to {@code
* System.out}, and is not a {@link FileOutputStream}. (You can
* therefore test if it is writing to a file by checking if the returned
* stream is an instance of {@link FileOutputStream}.) If the option is not
* specified, the string {@code path} is used as the file name. If {@code
* path} is {@code null} then {@code null} is returned.
*
* @param option The option specification.
* @param defaultPath The path to use if the option is not specified or
* {@code null} if no reader is to be returned in that
* case.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or the file specified by {@code
* defaultPath}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
@SuppressWarnings({"UseOfSystemOutOrSystemErr"})
public OutputStream getOutputStream(String option, String defaultPath,
String... doc) throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
if (defaultPath == null) {
return null;
}
path = defaultPath;
}
if (path.equals("-")) {
return System.out;
}
return new FileOutputStream(path);
} catch (FileNotFoundException e) {
throw ioException(option, path, e);
}
}
/**
* Returns an {@link InputStream} that is the result of creating a new
* {@link FileInputStream} object for the file named by the given option. If
* the option is {@code "-"}, the returned stream reads from {@code
* System.in}, and is not a {@link FileInputStream}. (You can
* therefore test if it is reading from a file by checking if the returned
* stream is an instance of {@link FileInputStream}.) If the option is not
* specified, returns {@code defaultValue}.
*
* @param option The option.
* @param defaultValue The value to return if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
public InputStream getInputStream(String option, InputStream defaultValue,
String... doc) throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
return defaultValue;
}
if (path.equals("-")) {
return System.in;
}
return new FileInputStream(path);
} catch (FileNotFoundException e) {
throw ioException(option, path, e);
}
}
/**
* Returns an {@link InputStream} that is the result of creating a new
* {@link FileInputStream} object for the file named by the given option. If
* the option is {@code "-"}, the returned stream reads from {@code
* System.in}, and is not a {@link FileInputStream}. (You can
* therefore test if it is reading from a file by checking if the returned
* stream is an instance of {@link FileInputStream}.) If the option is not
* specified, the string {@code path} is used as the file name. If {@code
* path} is {@code null} then {@code null} is returned.
*
* @param option The option.
* @param defaultPath The path to use if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or the file specified by {@code
* defaultPath}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
public InputStream getInputStream(String option, String defaultPath,
String... doc) throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
if (defaultPath == null) {
return null;
}
path = defaultPath;
}
if (path.equals("-")) {
return System.in;
}
return new FileInputStream(path);
} catch (FileNotFoundException e) {
throw ioException(option, path, e);
}
}
/**
* Returns a {@link RandomAccessFile} that is the result of creating a new
* {@link RandomAccessFile} object for the file named by the given option,
* using the given {@code mode}. If the option is not specified, return
* {@code defaultValue}.
*
* @param option The option.
* @param defaultValue The value to return if the option is not specified.
* @param mode The mode parameter for {@link RandomAccessFile#RandomAccessFile(String,String)}
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
public RandomAccessFile getRandomAccessFile(String option,
RandomAccessFile defaultValue, String mode, String... doc)
throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
return defaultValue;
}
return new RandomAccessFile(path, mode);
} catch (FileNotFoundException e) {
throw ioException(option, path, e);
}
}
/**
* Returns a {@link RandomAccessFile} that is the result of creating a new
* {@link RandomAccessFile} object for the file named by the given option,
* using the given {@code mode}. If the option is not specified, the string
* {@code path} is used as the file name. If {@code path} is {@code null}
* then {@code null} is returned.
*
* @param option The option.
* @param defaultPath The path to use if the option is not specified.
* @param mode The mode parameter for {@link RandomAccessFile#RandomAccessFile(String,String)}
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws IOException There was a problem opening the file.
* @throws ProgramOptionException No argument was present.
*/
public RandomAccessFile getRandomAccessFile(String option,
String defaultPath, String mode, String... doc)
throws IOException, ProgramOptionException {
String path = getArgument(option, "file", doc);
try {
if (path == null) {
if (defaultPath == null) {
return null;
}
path = defaultPath;
}
return new RandomAccessFile(path, mode);
} catch (FileNotFoundException e) {
throw ioException(option, path, e);
}
}
/**
* Returns a directory specified by the user. If the option is not
* specified, {@code path} is used. If the path is of an existing entity in
* the file system, it must be a directory. If {@code path} is {@code null}
* then {@code null} is returned and no directory is created.
*
* @param option The option.
* @param defaultPath The path to use if the option is not specified.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws ProgramOptionException No argument was present.
*/
public String getDirectory(String option, String defaultPath, String... doc)
throws ProgramOptionException {
String dir = getArgument(option, "dir", doc);
return parseDirectory(option, dir, defaultPath, MissingDirAction.NONE);
}
/**
* Returns a directory specified by the user. If the option is not
* specified, {@code path} is used. If the path is of an existing entity in
* the file system, it must be a directory.
*
* If the path is for a non-existing directory, the {@code ifMissing}
* parameter says what to do: {@link MissingDirAction#NONE}: Simply
* return the path. {@link MissingDirAction#EXCEPTION}: Throw a {@link
* ProgramOptionException}. {@link MissingDirAction#CREATE}: Create the
* directory. If this is not posible, throw a {@link
* ProgramOptionException}.
*
* @param option The option.
* @param defaultPath The value to return if the option is not specified.
* @param ifMissing The action to take if the directory is missing.
* @param doc Usage documentation for the option (@see {@link
* #getString(String,String,String[]) getString}).
*
* @return The value for the option, or {@code defaultValue}.
*
* @throws ProgramOptionException No argument was present.
*/
public String getDirectory(String option, String defaultPath,
MissingDirAction ifMissing, String... doc)
throws ProgramOptionException {
String path = getArgument(option, "dir", doc);
return parseDirectory(option, path, defaultPath, ifMissing);
}
/**
* Finds the given option somewhere in the command line. If the option is
* not found, return {@code false}. Otherwise set {@code str}, {@code pos},
* and {@code opt} fields, mark the option character as used, and then
* return {@code true}.
*
* @param o The option object.
* @param lookFor The option.
*
* @return {@code true} if the option is specified.
*/
private boolean findOpt(Opt o, String lookFor) {
if (o == null) {
return false;
}
for (int i = 0; i < args.length; i++) {
if (used.get(i)) {
// already consumed
continue;
}
String arg = args[i];
if (arg.charAt(0) != '-') {
// not an option
continue;
}
if (arg.equals("--")) {
// "--" ends the list
break;
}
if (o.matches(arg)) {
foundStr = i;
foundOpt = lookFor;
used.set(i);
return true;
}
}
return false;
}
/**
* Return the current option's argument, marking its characters as used.
*
* @return The current option's argument.
*
* @throws ProgramOptionException No argument is given.
*/
private String optArg() throws ProgramOptionException {
if (foundStr + 1 >= args.length) {
String msg = "Argument missing for -" + foundOpt;
throw new ProgramOptionException(msg);
}
used.set(foundStr + 1);
return args[foundStr + 1];
}
/**
* Returns the command line operands that come after the options. This
* checks to make sure that all specified options have been consumed -- any
* options remaining at this point are assumed to be unknown options. If no
* operands remain, an empty array is returned.
*
* This is also where {@code -help} is handled. If the user specifies {@code
* -help}, and that option is not manually process by the code using {@link
* #getBoolean(String, String[])} getBoolean}, then the method {@link
* #usage} is invoked and {@link HelpOnlyException} is thrown. The program
* is expected to catch this exception and simply exit successfully.
*
* @param operandsDesc Operand descriptions. In usage messages, this will be
* printed out after the options usage is described.
* Typically this ends with {@code "..."} if an
* arbitrary number of operands can be specified.
*
* @return The operands that follow the options.
*
* @throws ProgramOptionException An unknown option was specified.
* @throws HelpOnlyException The user asked for usage/help
* information.
* @see #synopsis()
* @see #usage()
*/
public String[] getOperands(String... operandsDesc)
throws ProgramOptionException, HelpOnlyException {
operandsFetched = true;
this.operandsDesc = operandsDesc;
checkForHelp();
StringBuilder unused = new StringBuilder();
int count = 0;
int a;
for (a = 0; a < args.length; a++) {
if (used.get(a)) { // skip used parameters
continue;
}
if (!args[a].startsWith("-")) { // first non-option argument
break;
}
if (args[a].equals("--")) { // "--" ends things
a++; // skip the "--"
break;
}
if (unused.length() > 1) {
unused.append(' ');
}
unused.append(args[a]);
count++;
}
if (unused.length() != 0) {
String ustr = unused.toString();
String plural = count > 0 ? "s" : "";
String msg = "unknown/unused option" + plural + ": " + ustr;
throw new ProgramOptionException(msg);
}
String[] remains = new String[args.length - a];
System.arraycopy(args, a, remains, 0, remains.length);
return remains;
}
private void checkForHelp() {
// see if the user has already checked for and handled "help"
for (Opt o : options) {
if (o.multi.equals("help")) {
return;
}
}
// They haven't, so we will check
if (groups.size() > 1) {
optionGroup("Help");
}
boolean wantsHelp = hasDescs ? getBoolean("help",
"Print help message") : getBoolean("help");
if (wantsHelp) {
usage();
throw new HelpOnlyException();
}
}
/**
* Adds the given option of the given type to the list of known options;
* {@code -help} is handled separately in {@link #getOperands}.
*
* @param opt The option to add as known.
* @param optType The type of option.
* @param doc Usage documentation for the option.
*
* @return The option object.
*
* @see #getOperands(String[])
* @see #usage()
*/
private Opt addOpt(String opt, String optType, String... doc) {
// ensure this is a new, not a redundant, option.
for (Opt o : options) {
if (o.multi.equals(opt)) {
o.repeatable = true;
return o; // already known
}
}
Opt o = new Opt(opt, optType, doc);
options.add(o);
return o;
}
/**
* Prints out the command's usage, inferred from the requested options. You
* can override this to provide a more specific summary, or you can handle
* "help" as a boolean option yourself. This implementation is only valid
* after all known options have been requested and {@link #getOperands} has
* been called.
*
* @param out Stream for the usage message.
*
* @see #getOperands
* @see #synopsis
*/
public void usage(PrintWriter out) {
Opt[] opts = doSynopsis(out);
// If there are no descriptive texts, just return
if (!hasDescs) {
return;
}
for (String desc : programDesc) {
out.println(desc);
}
int lastGrp = -1;
for (Opt opt : opts) {
if (opt.group != lastGrp) {
if (lastGrp != -1 || programDesc.length > 0) {
out.println();
}
String groupName = groups.get(opt.group);
if (groupName.length() > 0) {
out.print(groupName);
out.println(" Options:");
}
lastGrp = opt.group;
}
out.print(opt.helpString(" "));
}
}
/**
* Prints out a synopsis the command's usage, inferred from the requested
* options. You can override this to provide a more specific summary, or you
* can handle "help" as a boolean option yourself. This implementation is
* only valid after all known options have been requested and {@link
* #getOperands} has been called.
*
* @param out Stream for the usage message.
*
* @see #getOperands
*/
public void synopsis(PrintWriter out) {
doSynopsis(out);
}
private Opt[] doSynopsis(PrintWriter out) {
if (!operandsFetched) {
throw new IllegalStateException(
"must call getOperands() before asking for usage");
}
// Order the options
Opt[] opts = options.toArray(new Opt[options.size()]);
Arrays.sort(opts, new Comparator() {
public int compare(Opt o1, Opt o2) {
if (o1.group != o2.group) {
return o1.group - o2.group;
}
return o1.order - o2.order;
}
});
if (programName != null) {
out.print(programName);
}
for (Opt opt : opts) {
out.print(" [");
out.print(opt);
out.print("]");
}
for (String desc : operandsDesc) {
out.print(' ');
out.print(desc);
}
out.println();
return opts;
}
/**
* Prints out the command's usage on {@code System.out}. Equivalent to
*
* usage(System.out, "...");
*
*
* @see #usage(PrintWriter)
*/
@SuppressWarnings({"UseOfSystemOutOrSystemErr"})
public void usage() {
System.out.flush();
PrintWriter pout = new PrintWriter(System.out);
usage(pout);
pout.flush();
}
/**
* Prints out a synposis of the command's usage on {@code System.out}.
*
* @see #usage(PrintWriter)
*/
@SuppressWarnings({"UseOfSystemOutOrSystemErr"})
public void synopsis() {
System.out.flush();
PrintWriter pout = new PrintWriter(System.out);
synopsis(pout);
pout.flush();
}
/**
* Returns the string that would be printed by {@link #usage(PrintWriter)}.
*
* @return The string that would be printed by {@link #usage(PrintWriter)}.
*
* @see #usage(PrintWriter)
*/
public String usageString() {
StringWriter out = new StringWriter();
PrintWriter pout = new PrintWriter(out);
usage(pout);
pout.close();
return out.toString();
}
/**
* Returns the string that would be printed by {@link
* #synopsis(PrintWriter)}.
*
* @return The string that would be printed by {@link #synopsis(PrintWriter)}.
*
* @see #synopsis(PrintWriter)
*/
public String synopsisString() {
StringWriter out = new StringWriter();
PrintWriter pout = new PrintWriter(out);
synopsis(pout);
pout.close();
return out.toString();
}
/**
* Returns the result of parsing the given directory from the command line.
* If {@code path} is {@code null} return {@code defaultPath}. If the path
* is of an existing entity in the file system, it must be a directory. If
* {@code defaultPath} is also {@code null}, this returns {@code null}.
*
* If the path is for a non-existing directory, the {@code ifMissing}
* parameter says what to do: {@link MissingDirAction#NONE}: Simply
* return the path. {@link MissingDirAction#EXCEPTION}: Throw a {@link
* ProgramOptionException}. {@link MissingDirAction#CREATE}: Create the
* directory. If this is not posible, throw a {@link
* ProgramOptionException}.
*
* @param opt The option this is being done for.
* @param path The path to parse.
* @param defaultPath The path to use if {@code str} is {@code null}.
* @param ifMissing What to do if the directory does not exist.
*
* @return The final path.
*
* @throws ProgramOptionException The path exists already, but is not a
* directory.
*/
private static String parseDirectory(String opt, String path,
String defaultPath, MissingDirAction ifMissing)
throws ProgramOptionException {
if (path == null) {
if (defaultPath == null) {
return null;
}
path = defaultPath;
}
File dir = new File(path);
if (dir.exists()) {
if (dir.isDirectory()) {
return path;
}
throw new ProgramOptionException(
"Exists, but is not a directory: " + path);
}
switch (ifMissing) {
case NONE:
break;
case EXCEPTION:
throw new ProgramOptionException("No such directory: " + path);
case CREATE:
try {
mkdirs(path);
} catch (IOException e) {
//noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
throw new ProgramOptionException(
"-" + opt + " " + e.getMessage());
}
break;
}
return path;
}
/**
* Creates the given directory if needed, including any intermediate missing
* directories.
*
* @param path The path for the directory.
*
* @throws IOException The path already exists as a file or {@link
* File#mkdirs} returns {@code false}.
*/
private static void mkdirs(String path) throws IOException {
File dir = new File(path);
if (dir.isDirectory()) {
return;
}
if (dir.exists() && !dir.isDirectory()) {
throw new IOException(
"mkdirs: " + dir + " exists but is not a directory");
}
if (!dir.mkdirs()) {
throw new IOException(
"mkdirs: " + dir + " Cannot create directory");
}
}
@SuppressWarnings({"TypeMayBeWeakened"})
private static NumberFormatException numException(NumberFormatException e,
String option) {
NumberFormatException ne = new NumberFormatException(
"-" + option + " " + e.getMessage());
ne.initCause(e);
return ne;
}
@SuppressWarnings({"TypeMayBeWeakened"})
private static IOException ioException(String opt, String path,
IOException e) {
IOException ne = new IOException("-" + opt + " " + path);
ne.initCause(e);
return ne;
}
}