/*
* Copyright (C) 2011 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.sourceforge.argparse4j.internal;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.regex.Pattern;
import net.sourceforge.argparse4j.ArgumentParsers;
import net.sourceforge.argparse4j.annotation.Arg;
import net.sourceforge.argparse4j.helper.ASCIITextWidthCounter;
import net.sourceforge.argparse4j.helper.PrefixPattern;
import net.sourceforge.argparse4j.helper.ReflectHelper;
import net.sourceforge.argparse4j.helper.TextHelper;
import net.sourceforge.argparse4j.helper.TextWidthCounter;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Argument;
import net.sourceforge.argparse4j.inf.ArgumentGroup;
import net.sourceforge.argparse4j.inf.ArgumentParser;
import net.sourceforge.argparse4j.inf.ArgumentParserException;
import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup;
import net.sourceforge.argparse4j.inf.Namespace;
/**
* <strong>The application code must not use this class directly.</strong>
*
*/
public final class ArgumentParserImpl implements ArgumentParser {
private Map<String, ArgumentImpl> optargIndex_ = new HashMap<String, ArgumentImpl>();
private List<ArgumentImpl> optargs_ = new ArrayList<ArgumentImpl>();
private List<ArgumentImpl> posargs_ = new ArrayList<ArgumentImpl>();
private List<ArgumentGroupImpl> arggroups_ = new ArrayList<ArgumentGroupImpl>();
private Map<String, Object> defaults_ = new HashMap<String, Object>();
private SubparsersImpl subparsers_ = new SubparsersImpl(this);
private ArgumentParserImpl mainParser_;
private String command_;
private String prog_;
private String usage_ = "";
private String description_ = "";
private String epilog_ = "";
private String version_ = "";
private PrefixPattern prefixPattern_;
private PrefixPattern fromFilePrefixPattern_;
private boolean defaultHelp_ = false;
private boolean negNumFlag_ = false;
private TextWidthCounter textWidthCounter_;
private static final Pattern NEG_NUM_PATTERN = Pattern.compile("-\\d+");
private static final Pattern SHORT_OPTS_PATTERN = Pattern
.compile("-[^-].*");
public ArgumentParserImpl(String prog) {
this(prog, true, ArgumentParsers.DEFAULT_PREFIX_CHARS, null,
new ASCIITextWidthCounter(), null, null);
}
public ArgumentParserImpl(String prog, boolean addHelp) {
this(prog, addHelp, ArgumentParsers.DEFAULT_PREFIX_CHARS, null,
new ASCIITextWidthCounter(), null, null);
}
public ArgumentParserImpl(String prog, boolean addHelp, String prefixChars) {
this(prog, addHelp, prefixChars, null, new ASCIITextWidthCounter(),
null, null);
}
public ArgumentParserImpl(String prog, boolean addHelp, String prefixChars,
String fromFilePrefix) {
this(prog, addHelp, prefixChars, fromFilePrefix,
new ASCIITextWidthCounter(), null, null);
}
public ArgumentParserImpl(String prog, boolean addHelp, String prefixChars,
String fromFilePrefix, TextWidthCounter textWidthCounter) {
this(prog, addHelp, prefixChars, fromFilePrefix, textWidthCounter,
null, null);
}
public ArgumentParserImpl(String prog, boolean addHelp, String prefixChars,
String fromFilePrefix, TextWidthCounter textWidthCounter,
String command, ArgumentParserImpl mainParser) {
this.prog_ = TextHelper.nonNull(prog);
this.command_ = command;
this.mainParser_ = mainParser;
this.textWidthCounter_ = textWidthCounter;
if (prefixChars == null || prefixChars.isEmpty()) {
throw new IllegalArgumentException(
"prefixChars cannot be a null or empty");
}
this.prefixPattern_ = new PrefixPattern(prefixChars);
if (fromFilePrefix != null) {
this.fromFilePrefixPattern_ = new PrefixPattern(fromFilePrefix);
}
if (addHelp) {
ResourceBundle bundle = ResourceBundle.getBundle(this.getClass().getName());
String prefix = prefixChars.substring(0, 1);
addArgument(prefix + "h", prefix + prefix + "help")
.action(Arguments.help())
.help(bundle.getString("help"))
.setDefault(Arguments.SUPPRESS);
}
}
@Override
public ArgumentImpl addArgument(String... nameOrFlags) {
return addArgument(null, nameOrFlags);
}
public ArgumentImpl addArgument(ArgumentGroupImpl group,
String... nameOrFlags) {
ArgumentImpl arg = new ArgumentImpl(prefixPattern_, group, nameOrFlags);
if (arg.isOptionalArgument()) {
for (String flag : arg.getFlags()) {
ArgumentImpl another = optargIndex_.get(flag);
if (another != null) {
// TODO No conflict handler ATM
throw new IllegalArgumentException(String.format(
TextHelper.LOCALE_ROOT,
"argument %s: conflicting option string(s): %s",
flag, another.textualName()));
}
}
for (String flag : arg.getFlags()) {
if (NEG_NUM_PATTERN.matcher(flag).matches()) {
negNumFlag_ = true;
}
optargIndex_.put(flag, arg);
}
optargs_.add(arg);
} else {
for (ArgumentImpl another : posargs_) {
if (arg.getName().equals(another.getName())) {
// TODO No conflict handler ATM
throw new IllegalArgumentException(String.format(
TextHelper.LOCALE_ROOT,
"argument %s: conflicting option string(s): %s",
arg.getName(), another.textualName()));
}
}
posargs_.add(arg);
}
return arg;
}
@Override
public SubparsersImpl addSubparsers() {
return subparsers_;
}
@Override
public ArgumentGroup addArgumentGroup(String title) {
ArgumentGroupImpl group = new ArgumentGroupImpl(this, title);
group.setIndex(arggroups_.size());
arggroups_.add(group);
return group;
}
@Override
public MutuallyExclusiveGroup addMutuallyExclusiveGroup() {
return addMutuallyExclusiveGroup("");
}
@Override
public MutuallyExclusiveGroup addMutuallyExclusiveGroup(String title) {
ArgumentGroupImpl group = new ArgumentGroupImpl(this, title);
group.setIndex(arggroups_.size());
group.setMutex(true);
arggroups_.add(group);
return group;
}
@Override
public ArgumentParserImpl usage(String usage) {
usage_ = TextHelper.nonNull(usage);
return this;
}
/**
* Set text to display before the argument help.
*
* @param description
* text to display before the argument help
* @return this
*/
@Override
public ArgumentParserImpl description(String description) {
description_ = TextHelper.nonNull(description);
return this;
}
@Override
public ArgumentParserImpl epilog(String epilog) {
epilog_ = TextHelper.nonNull(epilog);
return this;
}
@Override
public ArgumentParserImpl version(String version) {
version_ = TextHelper.nonNull(version);
return this;
}
@Override
public ArgumentParserImpl defaultHelp(boolean defaultHelp) {
defaultHelp_ = defaultHelp;
return this;
}
public boolean isDefaultHelp() {
return defaultHelp_;
}
private void printArgumentHelp(PrintWriter writer, List<ArgumentImpl> args,
int format_width) {
for (ArgumentImpl arg : args) {
if (arg.getArgumentGroup() == null
|| !arg.getArgumentGroup().isSeparateHelp()) {
arg.printHelp(writer, defaultHelp_, textWidthCounter_,
format_width);
}
}
}
@Override
public void printHelp() {
PrintWriter writer = new PrintWriter(System.out);
printHelp(writer);
writer.flush();
}
@Override
public void printHelp(PrintWriter writer) {
int formatWidth = ArgumentParsers.getFormatWidth();
printUsage(writer, formatWidth);
if (!description_.isEmpty()) {
writer.println();
writer.println(TextHelper.wrap(textWidthCounter_, description_,
formatWidth, 0, "", ""));
}
boolean subparsersUntitled = subparsers_.getTitle().isEmpty()
&& subparsers_.getDescription().isEmpty();
if (checkDefaultGroup(posargs_)
|| (subparsers_.hasSubCommand() && subparsersUntitled)) {
writer.println();
writer.println("positional arguments:");
printArgumentHelp(writer, posargs_, formatWidth);
if (subparsers_.hasSubCommand() && subparsersUntitled) {
subparsers_.printSubparserHelp(writer, formatWidth);
}
}
if (checkDefaultGroup(optargs_)) {
writer.println();
writer.println("optional arguments:");
printArgumentHelp(writer, optargs_, formatWidth);
}
if (subparsers_.hasSubCommand() && !subparsersUntitled) {
writer.println();
writer.print(subparsers_.getTitle().isEmpty() ? "subcommands"
: subparsers_.getTitle());
writer.println(":");
if (!subparsers_.getDescription().isEmpty()) {
writer.print(" ");
writer.println(TextHelper.wrap(textWidthCounter_,
subparsers_.getDescription(), formatWidth, 2, "", " "));
writer.println();
}
subparsers_.printSubparserHelp(writer, formatWidth);
}
for (ArgumentGroupImpl group : arggroups_) {
if (group.isSeparateHelp()) {
writer.println();
group.printHelp(writer, formatWidth);
}
}
if (!epilog_.isEmpty()) {
writer.println();
writer.println(TextHelper.wrap(textWidthCounter_, epilog_,
formatWidth, 0, "", ""));
}
}
private boolean checkDefaultGroup(List<ArgumentImpl> args) {
if (args.isEmpty()) {
return false;
}
for (ArgumentImpl arg : args) {
if (arg.getArgumentGroup() == null
|| !arg.getArgumentGroup().isSeparateHelp()) {
return true;
}
}
return false;
}
@Override
public String formatHelp() {
StringWriter writer = new StringWriter();
printHelp(new PrintWriter(writer));
return writer.toString();
}
private void printArgumentUsage(PrintWriter writer, List<String> opts,
int offset, String firstIndent, String subsequentIndent,
int format_width) {
int currentWidth = offset + firstIndent.length();
writer.print(firstIndent);
boolean first = true;
for (String syntax : opts) {
if (!first && currentWidth + syntax.length() + 1 > format_width) {
writer.println();
writer.print(subsequentIndent);
writer.print(" ");
writer.print(syntax);
currentWidth = subsequentIndent.length() + 1 + syntax.length();
} else {
writer.print(" ");
writer.print(syntax);
currentWidth += 1 + syntax.length();
first = false;
}
}
writer.println();
}
@Override
public void printUsage() {
PrintWriter writer = new PrintWriter(System.out);
printUsage(writer);
writer.flush();
}
@Override
public void printUsage(PrintWriter writer) {
printUsage(writer, ArgumentParsers.getFormatWidth());
}
private void printUsage(PrintWriter writer, int format_width) {
if (!usage_.isEmpty()) {
writer.print("usage: ");
writer.println(substitutePlaceholder(usage_));
return;
}
String usageprog = "usage: " + prog_;
writer.print(usageprog);
int offset;
String firstIndent;
String subsequentIndent;
String indent = " ";
int usageprogWidth = textWidthCounter_.width(usageprog);
if (usageprogWidth > indent.length()) {
writer.println();
offset = 6;
firstIndent = subsequentIndent = indent.substring(0, offset);
} else {
offset = usageprogWidth;
firstIndent = "";
subsequentIndent = indent.substring(0, offset);
}
List<String> opts = new ArrayList<String>();
addUpperParserUsage(opts, mainParser_);
if (command_ != null) {
opts.add(command_);
}
for (ArgumentImpl arg : optargs_) {
if (arg.getHelpControl() != Arguments.SUPPRESS
&& (arg.getArgumentGroup() == null || !arg
.getArgumentGroup().isMutex())) {
opts.add(arg.formatShortSyntax());
}
}
for (ArgumentGroupImpl group : arggroups_) {
List<ArgumentImpl> args = filterSuppressedArgs(group.getArgs());
int numArgs = args.size();
if (group.isMutex()) {
if (numArgs > 1) {
opts.add((group.isRequired() ? "(" : "[")
+ args.get(0).formatShortSyntaxNoBracket());
for (int i = 1; i < numArgs - 1; ++i) {
ArgumentImpl arg = args.get(i);
opts.add("|");
opts.add(arg.formatShortSyntaxNoBracket());
}
opts.add("|");
opts.add(args.get(numArgs - 1).formatShortSyntaxNoBracket()
+ (group.isRequired() ? ")" : "]"));
} else if (numArgs == 1) {
if (group.isRequired()) {
opts.add(args.get(0).formatShortSyntaxNoBracket());
} else {
opts.add(args.get(0).formatShortSyntax());
}
}
}
}
for (ArgumentImpl arg : posargs_) {
if (arg.getHelpControl() != Arguments.SUPPRESS) {
opts.add(arg.formatShortSyntax());
}
}
if (subparsers_.hasSubCommand()) {
opts.add(subparsers_.formatShortSyntax());
opts.add("...");
}
printArgumentUsage(writer, opts, offset, firstIndent, subsequentIndent,
format_width);
}
/**
* Returns arguments in {@code args} whose {@link Argument#getHelpControl()}
* do not return {@link Arguments#SUPPRESS}.
*
* @param args
* @return filtered list of arguments
*/
private static List<ArgumentImpl> filterSuppressedArgs(
Collection<ArgumentImpl> args) {
ArrayList<ArgumentImpl> res = new ArrayList<ArgumentImpl>();
for (ArgumentImpl arg : args) {
if (arg.getHelpControl() != Arguments.SUPPRESS) {
res.add(arg);
}
}
return res;
}
/**
* Appends command, required optional arguments and positional arguments in
* {@code parser} to {@code opts} recursively. Most upper parser stores
* first, just like post order traversal.
*
* @param opts
* Command, required optional arguments and positional arguments.
* @param parser
* The parser
*/
private void addUpperParserUsage(List<String> opts,
ArgumentParserImpl parser) {
if (parser == null) {
return;
}
addUpperParserUsage(opts, parser.mainParser_);
if (parser.command_ != null) {
opts.add(parser.command_);
}
for (ArgumentImpl arg : parser.optargs_) {
if (arg.getHelpControl() != Arguments.SUPPRESS
&& arg.isRequired()
&& (arg.getArgumentGroup() == null || !arg
.getArgumentGroup().isMutex())) {
opts.add(arg.formatShortSyntax());
}
}
for (ArgumentGroupImpl group : parser.arggroups_) {
List<ArgumentImpl> args = filterSuppressedArgs(group.getArgs());
int numArgs = args.size();
if (group.isMutex()) {
if (numArgs > 1) {
if (group.isRequired()) {
opts.add("(" + args.get(0).formatShortSyntaxNoBracket());
for (int i = 1; i < numArgs - 1; ++i) {
ArgumentImpl arg = args.get(i);
opts.add("|");
opts.add(arg.formatShortSyntaxNoBracket());
}
opts.add("|");
opts.add(args.get(numArgs - 1)
.formatShortSyntaxNoBracket() + ")");
}
} else if (numArgs == 1) {
if (group.isRequired()) {
opts.add(args.get(0).formatShortSyntaxNoBracket());
} else if (args.get(0).isRequired()) {
opts.add(args.get(0).formatShortSyntax());
}
}
}
}
for (ArgumentImpl arg : parser.posargs_) {
if (arg.getHelpControl() != Arguments.SUPPRESS) {
opts.add(arg.formatShortSyntax());
}
}
}
@Override
public String formatUsage() {
StringWriter writer = new StringWriter();
printUsage(new PrintWriter(writer));
return writer.toString();
}
@Override
public ArgumentParserImpl setDefault(String dest, Object value) {
defaults_.put(dest, value);
return this;
}
@Override
public ArgumentParserImpl setDefaults(Map<String, Object> attrs) {
defaults_.putAll(attrs);
return this;
}
/**
* Returns default value set by {@link ArgumentImpl#setDefault(Object)} or
* {@link ArgumentParserImpl#setDefault(String, Object)}. Please note that
* while parser-level defaults always override argument-level defaults while
* parsing, this method examines argument-level defaults first. If no
* default value is found, then check parser-level defaults.
*
* @param dest
* attribute name of default value to get.
* @return default value of given dest.
*/
@Override
public Object getDefault(String dest) {
for (ArgumentImpl arg : optargs_) {
if (dest.equals(arg.getDest()) && arg.getDefault() != null) {
return arg.getDefault();
}
}
for (ArgumentImpl arg : posargs_) {
if (dest.equals(arg.getDest()) && arg.getDefault() != null) {
return arg.getDefault();
}
}
return defaults_.get(dest);
}
@Override
public Namespace parseArgsOrFail(String args[]) {
try {
Namespace ns = parseArgs(args);
return ns;
} catch (ArgumentParserException e) {
handleError(e);
System.exit(1);
}
return null;
}
@Override
public Namespace parseArgs(String args[]) throws ArgumentParserException {
Map<String, Object> attrs = new HashMap<String, Object>();
parseArgs(args, attrs);
return new Namespace(attrs);
}
@Override
public void parseArgs(String[] args, Map<String, Object> attrs)
throws ArgumentParserException {
parseArgs(args, 0, attrs);
}
@Override
public void parseArgs(String[] args, Object userData)
throws ArgumentParserException {
Map<String, Object> opts = new HashMap<String, Object>();
parseArgs(args, opts, userData);
}
@Override
public void parseArgs(String[] args, Map<String, Object> attrs,
Object userData) throws ArgumentParserException {
parseArgs(args, 0, attrs);
Class userClass = userData.getClass();
while (userClass != null) {
for (final Field field : userClass.getDeclaredFields()) {
Arg ann = field.getAnnotation(Arg.class);
if (ann != null) {
String argDest = ann.dest();
if (argDest.isEmpty()) {
argDest = field.getName();
}
if (!attrs.containsKey(argDest)) {
continue;
}
Object val = attrs.get(argDest);
try {
AccessController
.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
field.setAccessible(true);
return null;
}
});
field.set(userData,
ReflectHelper.list2Array(field.getType(), val));
} catch (RuntimeException e) {
if (!ann.ignoreError()) {
throw e;
}
} catch (Exception e) {
if (!ann.ignoreError()) {
throw new IllegalArgumentException(String.format(
TextHelper.LOCALE_ROOT,
"Could not set %s to field %s", val,
field.getName()), e);
}
}
}
}
for (final Method method : userClass.getDeclaredMethods()) {
Arg ann = method.getAnnotation(Arg.class);
if (ann != null) {
String argDest = ann.dest();
if (argDest.isEmpty()) {
argDest = method.getName();
}
if (!attrs.containsKey(argDest)) {
continue;
}
Object val = attrs.get(argDest);
Class<?>[] fargs = method.getParameterTypes();
if (fargs.length != 1) {
throw new IllegalArgumentException(String.format(
TextHelper.LOCALE_ROOT,
"Method %s must have one formal parameter",
method.getName()));
}
try {
AccessController
.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
method.setAccessible(true);
return null;
}
});
method.invoke(userData,
ReflectHelper.list2Array(fargs[0], val));
} catch (RuntimeException e) {
if (!ann.ignoreError()) {
throw e;
}
} catch (Exception e) {
if (!ann.ignoreError()) {
throw new IllegalArgumentException(String.format(
TextHelper.LOCALE_ROOT,
"Could not call method %s with %s",
method.getName(), val), e);
}
}
}
}
userClass = userClass.getSuperclass();
}
}
public void parseArgs(String args[], int offset, Map<String, Object> attrs)
throws ArgumentParserException {
ParseState state = new ParseState(args, offset, negNumFlag_);
parseArgs(state, attrs);
if (state.deferredException != null) {
throw state.deferredException;
}
}
/**
* Check that term forms a valid concatenated short options. Note that this
* option does not actually process arguments. Therefore, true from this
* function does not mean all arguments in term are acceptable.
*
* @param term
* string to inspect
* @return true if term forms a valid concatenated short options.
*/
private boolean checkConcatenatedShortOpts(String term) {
if (SHORT_OPTS_PATTERN.matcher(term).matches()) {
for (int i = 1, termlen = term.length(); i < termlen; ++i) {
String shortFlag = "-" + term.charAt(i);
ArgumentImpl arg = optargIndex_.get(shortFlag);
if (arg == null) {
return false;
}
if (arg.getAction().consumeArgument()) {
return true;
}
}
return true;
} else {
return false;
}
}
/**
* Returns optional argument ArgumentImpl which matches given flag. This
* function handles abbreviation as well. If flag is ambiguous,
* {@link ArgumentParserException} will be thrown. If flag does not match
* nay ArgumentImpl, this function returns null.
*
* @param flag
* flag to match
* @return ArgumentImpl which matches flag if it succeeds, or null
* @throws ArgumentParserException
* if flag is ambiguous
*/
private ArgumentImpl resolveNextFlag(String flag)
throws ArgumentParserException {
ArgumentImpl arg = optargIndex_.get(flag);
if (arg != null) {
return arg;
}
List<String> cand = TextHelper.findPrefix(optargIndex_.keySet(), flag);
if (cand.isEmpty()) {
return null;
} else if (checkConcatenatedShortOpts(flag)) {
// Get first short option
cand.add(flag.substring(0, 2));
} else if (cand.size() == 1) {
return optargIndex_.get(cand.get(0));
}
// At this point, more than 1 flags were found from optargIndex_
// and/or flag forms concatenated short options.
// Sort in order to make unit test easier.
Collections.sort(cand);
throw new ArgumentParserException(String.format(TextHelper.LOCALE_ROOT,
"ambiguous option: %s could match %s", flag,
TextHelper.concat(cand, 0, ", ")), this);
}
public void parseArgs(ParseState state, Map<String, Object> attrs)
throws ArgumentParserException {
populateDefaults(attrs);
Set<ArgumentImpl> used = new HashSet<ArgumentImpl>();
ArgumentImpl[] groupUsed = new ArgumentImpl[arggroups_.size()];
int posargIndex = 0;
int posargsLen = posargs_.size();
while (state.isArgAvail()) {
// We first evaluate flagFound(state) before comparing arg to "--"
// in order to expand arguments from file.
if (flagFound(state) && !"--".equals(state.getArg())) {
String term = state.getArg();
int p = term.indexOf("=");
String flag;
String embeddedValue;
if (p == -1) {
flag = term;
embeddedValue = null;
} else {
flag = term.substring(0, p);
embeddedValue = term.substring(p + 1);
}
ArgumentImpl arg = resolveNextFlag(flag);
if (arg == null) {
// Assign null for clarity
embeddedValue = null;
boolean shortOptsFound = false;
if (SHORT_OPTS_PATTERN.matcher(term).matches()) {
shortOptsFound = true;
// Possible concatenated short options
for (int i = 1, termlen = term.length(); i < termlen; ++i) {
String shortFlag = "-" + term.charAt(i);
arg = optargIndex_.get(shortFlag);
if (arg == null) {
shortOptsFound = false;
break;
}
if (arg.getAction().consumeArgument()) {
flag = shortFlag;
shortOptsFound = true;
if (term.length() > i + 1) {
embeddedValue = term.substring(i + 1);
}
break;
}
checkMutex(arg, groupUsed);
arg.run(this, attrs, shortFlag, null);
used.add(arg);
// Set null to avoid using it twice.
arg = null;
}
}
if (!shortOptsFound) {
throw new UnrecognizedArgumentException(
formatUnrecognizedArgumentErrorMessage(state,
term), this, term);
}
}
++state.index;
if (arg != null) {
checkMutex(arg, groupUsed);
processArg(attrs, state, arg, flag, embeddedValue);
used.add(arg);
}
} else if ("--".equals(state.getArg()) && !state.consumedSeparator) {
state.consumedSeparator = true;
state.negNumFlag = false;
++state.index;
} else if (posargIndex < posargsLen) {
ArgumentImpl arg = posargs_.get(posargIndex++);
processArg(attrs, state, arg, null, null);
} else if (!state.consumedSeparator && subparsers_.hasSubCommand()) {
checkRequiredArgument(state, used, posargIndex);
checkRequiredMutex(state, groupUsed);
subparsers_.parseArg(state, attrs);
return;
} else {
throw new ArgumentParserException(
formatUnrecognizedArgumentErrorMessage(state,
TextHelper.concat(state.args, state.index, " ")),
this);
}
}
if (subparsers_.hasSubCommand()) {
throw new ArgumentParserException("too few arguments", this);
}
while (posargIndex < posargsLen) {
ArgumentImpl arg = posargs_.get(posargIndex++);
processArg(attrs, state, arg, null, null);
}
checkRequiredArgument(state, used, posargIndex);
checkRequiredMutex(state, groupUsed);
}
/**
* Format message for "Unrecognized arguments" error.
*
* @param state
* Current parser state
* @param args
* Textual representation of unrecognized arguments to be
* included in the message as is.
* @return formatted error message
*/
private String formatUnrecognizedArgumentErrorMessage(ParseState state,
String args) {
return String
.format(TextHelper.LOCALE_ROOT,
"unrecognized arguments: '%s'%s",
args,
state.index > state.lastFromFileArgIndex ? ""
: String.format(
TextHelper.LOCALE_ROOT,
"%nChecking trailing white spaces or new lines in %sfile may help.",
fromFilePrefixPattern_.getPrefixChars()
.length() == 1 ? fromFilePrefixPattern_
.getPrefixChars() : "["
+ fromFilePrefixPattern_
.getPrefixChars() + "]"));
}
/**
* Check that another option in mutually exclusive group has already been
* specified. If so, throw an exception.
*
* @param arg
* The argument currently processed
* @param groupUsed
* The cache of used argument in each groups.
* @throws ArgumentParserException
* If another option in mutually exclusive group has already
* been used.
*/
private void checkMutex(ArgumentImpl arg, ArgumentImpl[] groupUsed)
throws ArgumentParserException {
if (arg.getArgumentGroup() != null) {
if (arg.getArgumentGroup().isMutex()) {
ArgumentImpl usedMutexArg = groupUsed[arg.getArgumentGroup()
.getIndex()];
if (usedMutexArg == null) {
groupUsed[arg.getArgumentGroup().getIndex()] = arg;
} else if (usedMutexArg != arg) {
throw new ArgumentParserException(String.format(
TextHelper.LOCALE_ROOT,
"not allowed with argument %s",
usedMutexArg.textualName()), this, arg);
}
}
}
}
/**
* @param res
* @param state
* state.offset points to the argument next to flag
* @param arg
* @param flag
* @param embeddedValue
* If optional argument is given as "foo=bar" or "-fbar" (short
* option), embedded value is "bar". Otherwise {@code null}
* @throws ArgumentParserException
*/
private void processArg(Map<String, Object> res, ParseState state,
ArgumentImpl arg, String flag, String embeddedValue)
throws ArgumentParserException {
if (!arg.getAction().consumeArgument()) {
if (embeddedValue == null) {
arg.run(this, res, flag, null);
return;
} else {
throw new ArgumentParserException(String.format(
TextHelper.LOCALE_ROOT,
"ignore implicit argument '%s'", embeddedValue), this,
arg);
}
}
if (arg.getMinNumArg() == -1
|| (arg.getMinNumArg() == 0 && arg.getMaxNumArg() == 1)) {
// In case of: option takes exactly one argument, or nargs("?")
String argval = null;
if (embeddedValue == null) {
if (state.isArgAvail() && !flagFound(state)) {
argval = state.getArg();
++state.index;
}
} else {
argval = embeddedValue;
}
if (argval == null) {
if (arg.getMinNumArg() == -1) {
if (arg.isOptionalArgument()) {
throw new ArgumentParserException(
"expected one argument", this, arg);
} else {
throw new ArgumentParserException("too few arguments",
this);
}
} else if (arg.isOptionalArgument()) {
// This is a special treatment for nargs("?"). If flag is
// given but no argument follows, produce const value.
arg.run(this, res, flag, arg.getConst());
}
} else {
arg.run(this, res, flag, arg.convert(this, argval));
}
} else {
List<Object> list = new ArrayList<Object>();
if (embeddedValue == null) {
for (int i = 0; i < arg.getMaxNumArg() && state.isArgAvail(); ++i, ++state.index) {
if (flagFound(state)) {
break;
}
list.add(arg.convert(this, state.getArg()));
}
} else {
list.add(arg.convert(this, embeddedValue));
}
if (list.size() < arg.getMinNumArg()) {
if (arg.isOptionalArgument()) {
throw new ArgumentParserException(String.format(
TextHelper.LOCALE_ROOT, "expected %d argument(s)",
arg.getMinNumArg()), this, arg);
} else {
throw new ArgumentParserException("too few arguments", this);
}
}
// For optional arguments, always process the list even if it is
// empty.
// For positional arguments, empty list means no positional argument
// is given. In this case, we want to keep default value, so
// don't process the list.
if (arg.isOptionalArgument() || !list.isEmpty()) {
arg.run(this, res, flag, list);
}
}
}
/**
* Returns true if state.getArg() is flag. Note that if "--" is met and not
* consumed, this function returns true, because "--" is treated as special
* optional argument. If prefixFileChar is found in prefix of argument, read
* arguments from that file and expand arguments in state necessary.
*
* @param state
* @return
* @throws ArgumentParserException
*/
private boolean flagFound(ParseState state) throws ArgumentParserException {
while (fromFileFound(state)) {
extendArgs(state,
fromFilePrefixPattern_.removePrefix(state.getArg()));
}
String term = state.getArg();
if (state.consumedSeparator) {
return false;
} else if ("--".equals(term)) {
return true;
}
return prefixPattern_.match(term)
&& (state.negNumFlag || !NEG_NUM_PATTERN.matcher(term)
.matches());
}
private boolean fromFileFound(ParseState state) {
return fromFilePrefixPattern_ != null
&& fromFilePrefixPattern_.match(state.getArg());
}
/**
* Extends arguments by reading additional arguments from file.
*
* @param state
* Current parser state.
* @param file
* File from which additional arguments are read.
* @throws ArgumentParserException
*/
private void extendArgs(ParseState state, String file)
throws ArgumentParserException {
List<String> list = new ArrayList<String>();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream(file), "utf-8"));
String line;
while ((line = reader.readLine()) != null) {
list.add(line);
}
} catch (IOException e) {
throw new ArgumentParserException(String.format(
TextHelper.LOCALE_ROOT,
"Could not read arguments from file '%s'", file), e, this);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
}
}
int offset = state.index + 1;
String[] newargs = new String[list.size() + state.args.length - offset];
list.toArray(newargs);
System.arraycopy(state.args, offset, newargs, list.size(),
state.args.length - offset);
if (state.lastFromFileArgIndex < offset) {
state.lastFromFileArgIndex = list.size() - 1;
} else {
state.lastFromFileArgIndex += -offset + list.size();
}
state.resetArgs(newargs);
}
private void checkRequiredArgument(ParseState state,
Set<ArgumentImpl> used, int posargIndex)
throws ArgumentParserException {
if (state.deferredException != null) {
return;
}
for (ArgumentImpl arg : optargs_) {
if (arg.isRequired() && !used.contains(arg)) {
state.deferredException = new ArgumentParserException(
String.format(TextHelper.LOCALE_ROOT,
"argument %s is required", arg.textualName()),
this);
}
}
if (posargs_.size() > posargIndex) {
state.deferredException = new ArgumentParserException(
"too few arguments", this);
}
}
private void checkRequiredMutex(ParseState state, ArgumentImpl[] used)
throws ArgumentParserException {
if (state.deferredException != null) {
return;
}
for (int i = 0; i < arggroups_.size(); ++i) {
ArgumentGroupImpl group = arggroups_.get(i);
if (group.isMutex() && group.isRequired() && used[i] == null) {
StringBuilder sb = new StringBuilder();
for (ArgumentImpl arg : group.getArgs()) {
if (arg.getHelpControl() != Arguments.SUPPRESS) {
sb.append(arg.textualName()).append(" ");
}
}
state.deferredException = new ArgumentParserException(
String.format(TextHelper.LOCALE_ROOT,
"one of the arguments %sis required",
sb.toString()), this);
}
}
}
private void populateDefaults(Map<String, Object> opts) {
for (ArgumentImpl arg : posargs_) {
if (arg.getDefaultControl() != Arguments.SUPPRESS) {
opts.put(arg.getDest(), arg.getDefault());
}
}
for (ArgumentImpl arg : optargs_) {
if (arg.getDefaultControl() != Arguments.SUPPRESS) {
opts.put(arg.getDest(), arg.getDefault());
}
}
for (Map.Entry<String, Object> entry : defaults_.entrySet()) {
opts.put(entry.getKey(), entry.getValue());
}
}
public String getProg() {
return prog_;
}
@Override
public void printVersion() {
PrintWriter writer = new PrintWriter(System.out);
printVersion(writer);
writer.flush();
}
@Override
public void printVersion(PrintWriter writer) {
writer.println(formatVersion());
}
@Override
public String formatVersion() {
return substitutePlaceholder(version_);
}
@Override
public void handleError(ArgumentParserException e) {
if (e.getParser() != this) {
e.getParser().handleError(e);
return;
}
// if --help triggered, just return (help info displayed by other
// method)
if (e instanceof HelpScreenException) {
return;
}
PrintWriter writer = new PrintWriter(System.err);
printUsage(writer);
writer.write(TextHelper.wrap(textWidthCounter_, String.format(
TextHelper.LOCALE_ROOT, "%s: error: %s%n", prog_,
e.getMessage()), ArgumentParsers.getFormatWidth(), 0, "", ""));
if (e instanceof UnrecognizedArgumentException) {
UnrecognizedArgumentException ex = (UnrecognizedArgumentException) e;
String argument = ex.getArgument();
if (prefixPattern_.match(argument)) {
String flagBody = prefixPattern_.removePrefix(argument);
if (flagBody.length() >= 2) {
printFlagCandidates(flagBody, writer);
}
}
} else if (e instanceof UnrecognizedCommandException) {
UnrecognizedCommandException ex = (UnrecognizedCommandException) e;
String command = ex.getCommand();
printCommandCandidates(command, writer);
}
writer.flush();
}
/**
* Calculates Damerau–Levenshtein distance between string {@code a} and
* {@code b} with given costs.
*
* @param a
* String
* @param b
* String
* @param swap
* Cost to swap 2 adjacent characters.
* @param sub
* Cost to substitute character.
* @param add
* Cost to add character.
* @param del
* Cost to delete character.
* @return Damerau–Levenshtein distance between {@code a} and {@code b}
*/
private int levenshtein(String a, String b, int swap, int sub, int add,
int del) {
int alen = a.length();
int blen = b.length();
int[][] dp = new int[3][blen + 1];
for (int i = 0; i <= blen; ++i) {
dp[1][i] = i;
}
for (int i = 1; i <= alen; ++i) {
dp[0][0] = i;
for (int j = 1; j <= blen; ++j) {
dp[0][j] = dp[1][j - 1]
+ (a.charAt(i - 1) == b.charAt(j - 1) ? 0 : sub);
if (i >= 2 && j >= 2 && a.charAt(i - 1) != b.charAt(j - 1)
&& a.charAt(i - 2) == b.charAt(j - 1)
&& a.charAt(i - 1) == b.charAt(j - 2)) {
dp[0][j] = Math.min(dp[0][j], dp[2][j - 2] + swap);
}
dp[0][j] = Math.min(dp[0][j],
Math.min(dp[1][j] + del, dp[0][j - 1] + add));
}
int[] temp = dp[2];
dp[2] = dp[1];
dp[1] = dp[0];
dp[0] = temp;
}
return dp[1][blen];
}
private static class SubjectBody {
public String subject;
public String body;
public SubjectBody(String subject, String body) {
this.subject = subject;
this.body = body;
}
}
// Made public for unit test
public static class Candidate implements Comparable<Candidate> {
public int similarity;
public String subject;
public Candidate(int similarity, String subject) {
this.similarity = similarity;
this.subject = subject;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!this.getClass().equals(obj.getClass())) {
return false;
}
Candidate other = (Candidate) obj;
if (subject == null) {
if (other.subject != null) {
return false;
}
} else if (other.subject == null) {
return false;
} else if (!subject.equals(other.subject)) {
return false;
}
if (similarity != other.similarity) {
return false;
}
return true;
}
@Override
public int hashCode() {
int prime = 31;
int hash = 1;
hash = hash * prime + (subject == null ? 0 : subject.hashCode());
hash = hash * prime + similarity;
return hash;
}
@Override
public int compareTo(Candidate rhs) {
if (similarity < rhs.similarity) {
return -1;
} else if (similarity == rhs.similarity) {
return subject.compareTo(rhs.subject);
} else {
return 1;
}
}
}
private void printFlagCandidates(String flagBody, PrintWriter writer) {
List<SubjectBody> subjects = new ArrayList<SubjectBody>();
for (ArgumentImpl arg : optargs_) {
String[] flags = arg.getFlags();
for (int i = 0, len = flags.length; i < len; ++i) {
String body = prefixPattern_.removePrefix(flags[i]);
if (body.length() <= 1) {
continue;
}
subjects.add(new SubjectBody(flags[i], body));
}
}
printCandidates(flagBody, subjects, writer);
}
private void printCommandCandidates(String command, PrintWriter writer) {
List<SubjectBody> subjects = new ArrayList<SubjectBody>();
for (String com : subparsers_.getCommands()) {
subjects.add(new SubjectBody(com, com));
}
printCandidates(command, subjects, writer);
}
/**
* Prints most similar subjects in subjects to body. Similarity is
* calculated between body and each {@link SubjectBody#body} in subjects.
*
* @param body
* String to compare.
* @param subjects
* Target to be compared.
* @param writer
* Output
*/
private void printCandidates(String body, List<SubjectBody> subjects,
PrintWriter writer) {
List<Candidate> candidates = new ArrayList<Candidate>();
for (SubjectBody sub : subjects) {
if (sub.body.startsWith(body)) {
candidates.add(new Candidate(0, sub.subject));
continue;
} else {
// Cost values were borrowed from git, help.c
candidates.add(new Candidate(levenshtein(body, sub.body, 0, 2,
1, 4), sub.subject));
}
}
if (candidates.isEmpty()) {
return;
}
Collections.sort(candidates);
int threshold = candidates.get(0).similarity;
// Magic number 7 was borrowed from git, help.c
if (threshold >= 7) {
return;
}
writer.println();
writer.println("Did you mean:");
for (Candidate cand : candidates) {
if (cand.similarity > threshold) {
break;
}
writer.print("\t");
writer.println(cand.subject);
}
}
/**
* Replace placeholder in src with actual value. The only known placeholder
* is <tt>${prog}</tt>, which is replaced with {@link #prog_}.
*
* @param src
* string to be processed
* @return the substituted string
*/
private String substitutePlaceholder(String src) {
return src.replaceAll(Pattern.quote("${prog}"), prog_);
}
public String getCommand() {
return command_;
}
public TextWidthCounter getTextWidthCounter() {
return textWidthCounter_;
}
public String getPrefixChars() {
return prefixPattern_.getPrefixChars();
}
public String getFromFilePrefixChars() {
return fromFilePrefixPattern_ == null ? null : fromFilePrefixPattern_
.getPrefixChars();
}
/**
* Returns main (parent) parser.
*
* @return The main (parent) parser. null if this object is a root parser.
*/
public ArgumentParserImpl getMainParser() {
return mainParser_;
}
}