All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.yin.cli.Cli Maven / Gradle / Ivy

Go to download

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