-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #125 from gdgib/G2-1551-ArgParse
G2-1551 Generalized argument parser
- Loading branch information
Showing
18 changed files
with
696 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/target/ | ||
/.settings/ | ||
/.project | ||
/.classpath | ||
/.factorypath |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
|
||
<modelVersion>4.0.0</modelVersion> | ||
<artifactId>gb-argparse</artifactId> | ||
|
||
<parent> | ||
<groupId>com.g2forge.gearbox</groupId> | ||
<artifactId>gb-project</artifactId> | ||
<version>0.0.10-SNAPSHOT</version> | ||
<relativePath>../gb-project/pom.xml</relativePath> | ||
</parent> | ||
|
||
<name>Gearbox ArgParse</name> | ||
<description>A simple command line argument parsing library.</description> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>com.g2forge.habitat</groupId> | ||
<artifactId>ha-metadata</artifactId> | ||
<version>${habitat.version}</version> | ||
</dependency> | ||
</dependencies> | ||
</project> |
15 changes: 15 additions & 0 deletions
15
gb-argparse/src/main/java/com/g2forge/gearbox/argparse/ArgumentHelp.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.g2forge.gearbox.argparse; | ||
|
||
import static java.lang.annotation.RetentionPolicy.RUNTIME; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.Target; | ||
|
||
@Documented | ||
@Retention(RUNTIME) | ||
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) | ||
public @interface ArgumentHelp { | ||
public String value(); | ||
} |
9 changes: 9 additions & 0 deletions
9
gb-argparse/src/main/java/com/g2forge/gearbox/argparse/ArgumentHelpException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.g2forge.gearbox.argparse; | ||
|
||
public class ArgumentHelpException extends RuntimeException { | ||
public ArgumentHelpException(String message) { | ||
super(message); | ||
} | ||
|
||
private static final long serialVersionUID = 8436426521191021803L; | ||
} |
243 changes: 243 additions & 0 deletions
243
gb-argparse/src/main/java/com/g2forge/gearbox/argparse/ArgumentParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
package com.g2forge.gearbox.argparse; | ||
|
||
import java.lang.reflect.Constructor; | ||
import java.lang.reflect.InvocationTargetException; | ||
import java.lang.reflect.Parameter; | ||
import java.util.ArrayList; | ||
import java.util.EnumSet; | ||
import java.util.HashMap; | ||
import java.util.LinkedHashMap; | ||
import java.util.List; | ||
import java.util.ListIterator; | ||
import java.util.Map; | ||
import java.util.Set; | ||
|
||
import com.g2forge.alexandria.java.core.helpers.HCollection; | ||
import com.g2forge.alexandria.java.core.helpers.HStream; | ||
import com.g2forge.alexandria.java.fluent.optional.IOptional; | ||
import com.g2forge.alexandria.java.function.IFunction1; | ||
import com.g2forge.alexandria.java.text.HString; | ||
import com.g2forge.habitat.metadata.value.predicate.IPredicate; | ||
import com.g2forge.habitat.metadata.value.subject.ISubject; | ||
|
||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import lombok.Data; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public class ArgumentParser<T> { | ||
@Data | ||
@Builder(toBuilder = true) | ||
@RequiredArgsConstructor | ||
protected static class ParameterParserInfo { | ||
protected final int index; | ||
|
||
protected final IParameterParser parser; | ||
} | ||
|
||
protected static final Set<String> STANDARD_HELP_ARGUMENTS = HCollection.asSet("/h", "/?", "-h", "-help", "--help"); | ||
|
||
public enum HelpArguments { | ||
STANDARD { | ||
@Override | ||
public boolean isHelp(List<String> arguments) { | ||
if (arguments.size() == 1) return STANDARD_HELP_ARGUMENTS.contains(arguments.get(0)); | ||
return false; | ||
} | ||
}, | ||
EMPTY { | ||
@Override | ||
public boolean isHelp(List<String> arguments) { | ||
return arguments.isEmpty(); | ||
} | ||
}; | ||
|
||
public abstract boolean isHelp(List<String> arguments); | ||
} | ||
|
||
protected final Class<T> type; | ||
|
||
protected final Set<HelpArguments> help; | ||
|
||
public ArgumentParser(Class<T> type) { | ||
this(type, EnumSet.of(HelpArguments.STANDARD)); | ||
} | ||
|
||
@Getter(lazy = true, value = AccessLevel.PROTECTED) | ||
private final Constructor<T> constructor = findConstructor(); | ||
|
||
public T parse(List<String> arguments) { | ||
final IArgumentsParser argumentsParser = getArgumentsParser(); | ||
|
||
final boolean help = getHelp().stream().filter(helpArguments -> helpArguments.isHelp(arguments)).findAny().isPresent(); | ||
if (help) throw new ArgumentHelpException(argumentsParser.generateHelp()); | ||
|
||
final Object[] parsed = argumentsParser.apply(arguments); | ||
return create(parsed); | ||
} | ||
|
||
@Getter(lazy = true, value = AccessLevel.PROTECTED) | ||
private final IArgumentsParser argumentsParser = computeArgumentsParser(); | ||
|
||
protected ArgumentsParser computeArgumentsParser() { | ||
final Parameter[] parameterActuals = getConstructor().getParameters(); | ||
final List<IParameterInfo> parameterInfos = new ArrayList<>(); | ||
for (int i = 0; i < parameterActuals.length; i++) { | ||
parameterInfos.add(new IParameterInfo.ParameterInfoAdapter(i, parameterActuals[i])); | ||
} | ||
final ArgumentsParser argumentsParser = new ArgumentsParser(parameterInfos); | ||
return argumentsParser; | ||
} | ||
|
||
private T create(final Object[] parsed) { | ||
final Constructor<T> constructor = getConstructor(); | ||
try { | ||
return constructor.newInstance(parsed); | ||
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
private Constructor<T> findConstructor() { | ||
final Constructor<?>[] constructors = type.getDeclaredConstructors(); | ||
if (constructors.length != 1) throw new IllegalArgumentException(String.format("Argument type %1$s has %2$d constructors, only single constructor types are supported (for now).", type, constructors.length)); | ||
|
||
@SuppressWarnings({ "rawtypes", "unchecked" }) | ||
final Constructor<T> constructor = (Constructor) constructors[0]; | ||
return constructor; | ||
} | ||
|
||
public static <T> T parse(Class<T> type, List<String> arguments) { | ||
return new ArgumentParser<>(type).parse(arguments); | ||
} | ||
|
||
protected interface IArgumentsParser extends IFunction1<List<String>, Object[]> { | ||
public String generateHelp(); | ||
} | ||
|
||
protected static class ArgumentsParser implements IArgumentsParser { | ||
protected final List<? extends IParameterInfo> parameters; | ||
|
||
/** An in-order list of the positional parameters. May have different size than the orignal parameters as some may be named. */ | ||
protected final List<ParameterParserInfo> positional; | ||
|
||
/** A map from their names to the named parameters. May have different size than the original parameters as some may be positional. */ | ||
protected final Map<String, ParameterParserInfo> named; | ||
|
||
/** A list of parsers, one for each input parameter. */ | ||
protected final List<IParameterParser> parsers; | ||
|
||
public ArgumentsParser(final List<? extends IParameterInfo> parameters) { | ||
this(StandardParameterParserFactory.create(), parameters); | ||
} | ||
|
||
public ArgumentsParser(final IParameterParserFactory parameterParserFactory, final List<? extends IParameterInfo> parameters) { | ||
// Parse the parameter model from the constructor | ||
this.parameters = parameters; | ||
positional = new ArrayList<>(); | ||
named = new HashMap<>(); | ||
parsers = new ArrayList<>(); | ||
for (int i = 0; i < parameters.size(); i++) { | ||
final IParameterInfo parameter = parameters.get(i); | ||
|
||
final IParameterParser parameterTypeParser = parameterParserFactory.apply(parameter); | ||
parsers.add(parameterTypeParser); | ||
|
||
final ParameterParserInfo info = new ParameterParserInfo(i, parameterTypeParser); | ||
final ISubject subject = parameter.getSubject(); | ||
final NamedParameter annotation = subject.get(NamedParameter.class); | ||
if (annotation != null) named.put(annotation.value(), info); | ||
else positional.add(info); | ||
} | ||
} | ||
|
||
public Object[] apply(List<String> arguments) { | ||
final Object[] parsed = new Object[parameters.size()]; | ||
final boolean[] set = new boolean[parameters.size()]; | ||
int p = 0; | ||
// Parse the arguments | ||
for (final ListIterator<String> argumentIterator = arguments.listIterator(); argumentIterator.hasNext();) { | ||
final int argumentIndex = argumentIterator.nextIndex(); | ||
final String argument = argumentIterator.next(); | ||
try { | ||
boolean foundNamed = false; | ||
for (Map.Entry<String, ParameterParserInfo> entry : named.entrySet()) { | ||
if (argument.startsWith(entry.getKey())) { | ||
final ParameterParserInfo info = entry.getValue(); | ||
final int parameterIndex = info.getIndex(); | ||
parsed[parameterIndex] = info.getParser().parse(parameters.get(parameterIndex), argumentIterator); | ||
set[parameterIndex] = true; | ||
foundNamed = true; | ||
break; | ||
} | ||
} | ||
if (!foundNamed) { | ||
argumentIterator.previous(); | ||
final ParameterParserInfo info = positional.get(p++); | ||
final int index = info.getIndex(); | ||
parsed[index] = info.getParser().parse(parameters.get(index), argumentIterator); | ||
set[index] = true; | ||
} | ||
} catch (Throwable throwable) { | ||
throw new UnparseableArgumentException(argumentIndex, argument, throwable); | ||
} | ||
} | ||
|
||
// Fill in any unparsed parameters with defaults | ||
for (IParameterInfo parameter : parameters) { | ||
if (!set[parameter.getIndex()]) { | ||
final IOptional<Object> defaultValue = parsers.get(parameter.getIndex()).getDefault(parameter); | ||
if (defaultValue.isEmpty()) throw new UnspecifiedParameterException(parameter); | ||
else { | ||
parsed[parameter.getIndex()] = defaultValue.get(); | ||
set[parameter.getIndex()] = true; | ||
} | ||
} | ||
} | ||
return parsed; | ||
} | ||
|
||
@Override | ||
public String generateHelp() { | ||
final StringBuilder retVal = new StringBuilder(); | ||
final Map<String, String> positionalHelp = new LinkedHashMap<>(); | ||
for (ParameterParserInfo info : positional) { | ||
final IParameterInfo parameter = parameters.get(info.getIndex()); | ||
if (!retVal.isEmpty()) retVal.append(' '); | ||
retVal.append('<').append(parameter.getName()).append('>'); | ||
|
||
final IPredicate<ArgumentHelp> predicate = parameter.getSubject().bind(ArgumentHelp.class); | ||
if (predicate.isPresent()) positionalHelp.put(parameter.getName(), predicate.get0().value()); | ||
} | ||
final boolean hasNamed = named.isEmpty(); | ||
if (!hasNamed && !retVal.isEmpty()) retVal.append(" [...]"); | ||
|
||
if (!positionalHelp.isEmpty() || !hasNamed) { | ||
if (!retVal.isEmpty()) retVal.append("\n"); | ||
final int padded = HStream.concat(positionalHelp.keySet().stream(), named.keySet().stream()).mapToInt(String::length).max().getAsInt(); | ||
|
||
if (!positionalHelp.isEmpty()) { | ||
for (Map.Entry<String, String> entry : positionalHelp.entrySet()) { | ||
if (!retVal.isEmpty()) retVal.append('\n'); | ||
retVal.append(entry.getKey()).append(' ').append(entry.getValue()); | ||
} | ||
} | ||
|
||
if (!hasNamed) { | ||
for (Map.Entry<String, ParameterParserInfo> entry : named.entrySet()) { | ||
if (!retVal.isEmpty()) retVal.append('\n'); | ||
final IParameterInfo parameter = parameters.get(entry.getValue().getIndex()); | ||
retVal.append(HString.pad(entry.getKey(), " ", padded)); | ||
final ArgumentHelp argumentHelp = parameter.getSubject().get(ArgumentHelp.class); | ||
if (argumentHelp != null) retVal.append(' ').append(argumentHelp.value()); | ||
} | ||
} | ||
} | ||
|
||
return retVal.toString(); | ||
} | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
gb-argparse/src/main/java/com/g2forge/gearbox/argparse/BasicParameterParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package com.g2forge.gearbox.argparse; | ||
|
||
import java.nio.file.Paths; | ||
import java.util.ListIterator; | ||
|
||
import com.g2forge.alexandria.java.fluent.optional.IOptional; | ||
import com.g2forge.alexandria.java.fluent.optional.NullableOptional; | ||
|
||
public enum BasicParameterParser implements IParameterParser { | ||
BOOLEAN { | ||
@Override | ||
public IOptional<Object> getDefault(IParameterInfo parameter) { | ||
if (parameter.getSubject().bind(NamedParameter.class).isPresent()) return NullableOptional.of(false); | ||
return NullableOptional.empty(); | ||
} | ||
|
||
@Override | ||
public Object parse(IParameterInfo parameter, ListIterator<String> argumentIterator) { | ||
if (parameter.getSubject().bind(NamedParameter.class).isPresent()) return true; | ||
else return Boolean.valueOf(argumentIterator.next()); | ||
} | ||
}, | ||
PATH { | ||
@Override | ||
public IOptional<Object> getDefault(IParameterInfo parameter) { | ||
if (parameter.getSubject().bind(NamedParameter.class).isPresent()) return NullableOptional.of(null); | ||
return NullableOptional.empty(); | ||
} | ||
|
||
@Override | ||
public Object parse(IParameterInfo parameter, ListIterator<String> argumentIterator) { | ||
return Paths.get(argumentIterator.next()); | ||
} | ||
}, | ||
STRING { | ||
@Override | ||
public IOptional<Object> getDefault(IParameterInfo parameter) { | ||
if (parameter.getSubject().bind(NamedParameter.class).isPresent()) return NullableOptional.of(null); | ||
return NullableOptional.empty(); | ||
} | ||
|
||
@Override | ||
public Object parse(IParameterInfo parameter, ListIterator<String> argumentIterator) { | ||
return argumentIterator.next(); | ||
} | ||
}; | ||
} |
6 changes: 6 additions & 0 deletions
6
gb-argparse/src/main/java/com/g2forge/gearbox/argparse/IArgumentsType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.g2forge.gearbox.argparse; | ||
|
||
/** | ||
* An optional marker interface for all types meant to be parsed as command line arguments. | ||
*/ | ||
public interface IArgumentsType {} |
Oops, something went wrong.