com.github.yin.cli.Cli Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-cli-flags Show documentation
Show all versions of java-cli-flags Show documentation
Easy to use command-line parser, which enables you to
define cmdline flags directly the class they are used in.
The newest version!
package com.github.yin.cli;
import com.github.yin.cli.analysis.UsagePrinter;
import com.github.yin.cli.annotations.ClassScanner;
import com.github.yin.cli.parsing.LongKeyValueParser;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.*;
import java.util.*;
/**
* Stores arguments values and provides {@link Flag>} accessors for the values. Arguments value must be
* initialized before any flag values are accessed by {@link Flag>.get()} method. If arguments are provided as
* {@link String[]} array, the array will be parsed by {@link com.github.yin.cli.parsing.LongKeyValueParser} class,
* but any CLI parsing front-end can be used and arguments can be passed into {@link Cli.init()} method as
* {@link ImmutableMultimap}.
*
* Clients should be using exclusively static methods provided by this class as CLI arguments are expected to be
* immutable during the application runtime. An second attempt to call {@link Cli.init()} will result in an
* {@link IllegalStateException}. For integration testing of application modules, we provide {@link Cli.setArguments()}
* and {@link Cli.clear()} methods.
*
* Example:
* {@code
* @FlagDesc("Processes report in current directory.")
* public static class ReportMain {
*
* @FlagDesc("if 'true', prints additional information")
* static final Flag verbose = Cli.string("verbose");
*
* public static main(String[] args) {
* Cli.init(args);
* if (foo.get().equals("true")) {
* // ...
* } else if (foo.get().equals("false")) {
* // ...
* } else {
* Cli.printUsage("com.example");
* }
* }
* }
* }
*
* @author yin
*/
public class Cli implements ArgumentProvider {
private static Cli instance;
private final ClassScanner classScanner;
private final ClassMetadataIndex classMetadataIndex;
private final FlagIndex> flagIndex;
private final FlagIndex flagMetadataIndex;
private ImmutableMultimap flagValues;
private final TypeConversions typeConversions;
/**
* Initializes flag values from array of command-line arguments.
* @param args Array of command-line arguments as passed to the {@code main()} method
*/
public static void init(String[] args) throws IllegalStateException {
init(new LongKeyValueParser(args).parse());
}
/**
* Initializes flag values from an {@link ImmutableMultimap}.
* @param args Parsed command-line arguments
*/
public static void init(ImmutableMultimap args) throws IllegalStateException {
if (instance().flagValues == null) {
instance().setArguments(args);
} else {
throw new IllegalStateException("Cli cli already has been already initialized.");
}
}
/**
* Returns flag value accessor for {@link String} type. See create().
*/
public static Flag string(String name) {
return create(String.class, name);
}
/**
* Returns flag value accessor, which can be used to retrieve the flag value supplied
* by command line arguments. Preferably the typespecific variants (string()
, ...)
* should be used for readability reasons.
*
* {@code
* &at;Usage("Specifies path to input file")
* private static final Flag<String> inputPathFlag = Cli.create(String.class, "inputPath");
* }
*
* @param type of the provided flag value
* @param name of the flag. This is must match the name the field or name attribute in @Usage annotation
* @param is the same as flag value type
* @return Flag accessor for flag value identified by {@link name}
*/
public static Flag create(Class type, String name) {
return instance().createFlag(type, name);
}
private Flag createFlag(Class type, String name) {
try {
String callerClass = scanCallerClass();
FlagID id = FlagID.create(callerClass, name);
Flag flag = Flag.create(id, type, this, typeConversions);
flagIndex.add(id, flag);
return flag;
} catch (ClassNotFoundException ex) {
Throwables.propagate(ex);
}
// NOTE yin: Not-reachable code - Throwables.propagate() always throws, but IDE can figure this out.
return null;
}
/** Prints user-readable usage help for all cli in a given package */
public static void printUsage(String packagePrefix) {
instance().printUsageForPackage(packagePrefix);
}
/** Returns metadata collected from class @Cli annotations */
@VisibleForTesting
static ClassMetadataIndex classMetadata() {
return instance().classMetadataIndex;
}
/** Returns metadata collected from @Cli annotations. */
@VisibleForTesting
static FlagIndex flagMetadata() {
return instance().flagMetadataIndex;
}
/** Sets flag values before test case executes. */
@VisibleForTesting
void setArguments(ImmutableMultimap args) {
this.flagValues = args;
}
/** Clears flag values after test case completes. */
public static void clear() {
instance().clearArguments();
}
/** Type conversions used by Cli. */
public TypeConversions getTypeConversions() {
return typeConversions;
}
private void clearArguments() {
flagValues = null;
}
@Override
public String singleArgument(FlagID flagID) {
ImmutableCollection ret = this.allArguments(flagID);
if (ret != null) {
Iterator iter = ret.iterator();
if (iter.hasNext()) {
return iter.next();
}
}
return null;
}
@Override
public ImmutableCollection allArguments(FlagID flagID) {
ImmutableCollection ret = flagValues.get(flagID.className() + '.' + flagID.flagName());
if (ret == null || ret.isEmpty()) {
ret = flagValues.get(flagID.flagName());
}
return ret;
}
private void printUsageForPackage(String packagePrefix) {
// TODO yin: Subsequent calls will always print previously scanned packages, fix
synchronized (this) {
classScanner.scanPackage(packagePrefix, flagMetadataIndex, classMetadataIndex);
}
new UsagePrinter().printUsage(flagMetadataIndex, classMetadataIndex, System.out);
}
@VisibleForTesting
private String scanCallerClass() throws ClassNotFoundException {
String className = getCallerClassName();
synchronized (this) {
classScanner.scanClass(className, flagMetadataIndex, classMetadataIndex);
}
return className;
}
private Cli(ClassScanner classScanner, ClassMetadataIndex classMetadataIndex, FlagIndex> flagIndex,
FlagIndex flagMetadataIndex, TypeConversions typeConversions) {
this.classScanner = classScanner;
this.classMetadataIndex = classMetadataIndex;
this.flagIndex = flagIndex;
this.flagMetadataIndex = flagMetadataIndex;
this.flagValues = null;
this.typeConversions = typeConversions;
}
private static Cli instance() {
synchronized (Cli.class) {
if (instance == null) {
instance = createCli();
}
}
return instance;
}
@VisibleForTesting
static Cli createCli() {
return createCli(new ClassScanner(), new ClassMetadataIndex(), new FlagIndex>(),
new FlagIndex(), new TypeConversionsImpl());
}
@VisibleForTesting
static Cli createCli(ClassScanner classScanner, ClassMetadataIndex classMetadataIndex,
FlagIndex> flagFlagIndex, FlagIndex flagMetadataFlagIndex,
TypeConversions typeConversion) {
return new Cli(classScanner, classMetadataIndex, flagFlagIndex, flagMetadataFlagIndex, typeConversion);
}
private String getCallerClassName() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
String myType = Cli.class.getCanonicalName();
String threadType = Thread.class.getCanonicalName();
for (StackTraceElement e : stackTrace) {
if (!e.getClassName().equals(myType) && !e.getClassName().equals(threadType)) {
return e.getClassName();
}
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy