Package com.google.caliper.options

Source Code of com.google.caliper.options.CommandLineParser

/*
* Copyright (C) 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/

package com.google.caliper.options;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.caliper.util.DisplayUsageException;
import com.google.caliper.util.InvalidCommandException;
import com.google.caliper.util.Parser;
import com.google.caliper.util.Parsers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.primitives.Primitives;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.util.Iterator;
import java.util.List;

// based on r135 of OptionParser.java from vogar
// NOTE: this class is still pretty messy but will be cleaned up further and possibly offered to
// Guava.

/**
* Parses command line options.
*
* Strings in the passed-in String[] are parsed left-to-right. Each String is classified as a short
* option (such as "-v"), a long option (such as "--verbose"), an argument to an option (such as
* "out.txt" in "-f out.txt"), or a non-option positional argument.
*
* A simple short option is a "-" followed by a short option character. If the option requires an
* argument (which is true of any non-boolean option), it may be written as a separate parameter,
* but need not be. That is, "-f out.txt" and "-fout.txt" are both acceptable.
*
* It is possible to specify multiple short options after a single "-" as long as all (except
* possibly the last) do not require arguments.
*
* A long option begins with "--" followed by several characters. If the option requires an
* argument, it may be written directly after the option name, separated by "=", or as the next
* argument. (That is, "--file=out.txt" or "--file out.txt".)
*
* A boolean long option '--name' automatically gets a '--no-name' companion. Given an option
* "--flag", then, "--flag", "--no-flag", "--flag=true" and "--flag=false" are all valid, though
* neither "--flag true" nor "--flag false" are allowed (since "--flag" by itself is sufficient, the
* following "true" or "false" is interpreted separately). You can use "yes" and "no" as synonyms
* for "true" and "false".
*
* Each String not starting with a "-" and not a required argument of a previous option is a
* non-option positional argument, as are all successive Strings. Each String after a "--" is a
* non-option positional argument.
*
* The fields corresponding to options are updated as their options are processed. Any remaining
* positional arguments are returned as an ImmutableList<String>.
*
* Here's a simple example:
*
* // This doesn't need to be a separate class, if your application doesn't warrant it. //
* Non-@Option fields will be ignored. class Options {
*
* @Option(names = { "-q", "--quiet" }) boolean quiet = false;
*
* // Boolean options require a long name if it's to be possible to explicitly turn them off. //
* Here the user can use --no-color.
* @Option(names = { "--color" }) boolean color = true;
* @Option(names = { "-m", "--mode" }) String mode = "standard; // Supply a default just by setting
* the field.
* @Option(names = { "-p", "--port" }) int portNumber = 8888;
*
* // There's no need to offer a short name for rarely-used options.
* @Option(names = { "--timeout" }) double timeout = 1.0;
* @Option(names = { "-o", "--output-file" }) String outputFile;
*
* }
*
* See also:
*
* the getopt(1) man page Python's "optparse" module (http://docs.python.org/library/optparse.html)
* the POSIX "Utility Syntax Guidelines" (http://www.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap12.html#tag_12_02)
* the GNU "Standards for Command Line Interfaces" (http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Interfaces)
*/
final class CommandLineParser<T> {
  /**
   * Annotates a field or method in an options class to signify that parsed values should be
   * injected.
   */
  @Retention(RetentionPolicy.RUNTIME)
  @Target({ElementType.FIELD, ElementType.METHOD})
  public @interface Option {
    /**
     * The names for this option, such as { "-h", "--help" }. Names must start with one or two '-'s.
     * An option must have at least one name.
     */
    String[] value();
  }

  /**
   * Annotates a single method in an options class to receive any "leftover" arguments. The method
   * must accept {@code ImmutableList<String>} or a supertype. The method will be invoked even if
   * the list is empty.
   */
  @Retention(RetentionPolicy.RUNTIME)
  @Target({ElementType.FIELD, ElementType.METHOD})
  public @interface Leftovers {}

  public static <T> CommandLineParser<T> forClass(Class<? extends T> c) {
    return new CommandLineParser<T>(c);
  }

  private final InjectionMap injectionMap;
  private T injectee;

  // TODO(kevinb): make a helper object that can be mutated during processing
  private final List<PendingInjection> pendingInjections = Lists.newArrayList();

  /**
   * Constructs a new command-line parser that will inject values into {@code injectee}.
   *
   * @throws IllegalArgumentException if {@code injectee} contains multiple options using the same
   *     name
   */
  private CommandLineParser(Class<? extends T> c) {
    this.injectionMap = InjectionMap.forClass(c);
  }

  /**
   * Parses the command-line arguments 'args', setting the @Option fields of the 'optionSource'
   * provided to the constructor. Returns a list of the positional arguments left over after
   * processing all options.
   */
  public void parseAndInject(String[] args, T injectee) throws InvalidCommandException {
    this.injectee = injectee;
    pendingInjections.clear();
    Iterator<String> argsIter = Iterators.forArray(args);
    ImmutableList.Builder<String> builder = ImmutableList.builder();

    while (argsIter.hasNext()) {
      String arg = argsIter.next();
      if (arg.equals("--")) {
        break; // "--" marks the end of options and the beginning of positional arguments.
      } else if (arg.startsWith("--")) {
        parseLongOption(arg, argsIter);
      } else if (arg.startsWith("-")) {
        parseShortOptions(arg, argsIter);
      } else {
        builder.add(arg);
        // allow positional arguments to mix with options since many linux commands do
      }
    }

    for (PendingInjection pi : pendingInjections) {
      pi.injectableOption.inject(pi.value, injectee);
    }

    ImmutableList<String> leftovers = builder.addAll(argsIter).build();
    invokeMethod(injectee, injectionMap.leftoversMethod, leftovers);
  }

  // Private stuff from here on down

  private abstract static class InjectableOption {
    abstract boolean isBoolean();
    abstract void inject(String valueText, Object injectee) throws InvalidCommandException;
    boolean delayedInjection() {
      return false;
    }
  }

  private static class InjectionMap {
    public static InjectionMap forClass(Class<?> injectedClass) {
      ImmutableMap.Builder<String, InjectableOption> builder = ImmutableMap.builder();

      InjectableOption helpOption = new InjectableOption() {
        @Override boolean isBoolean() {
          return true;
        }
        @Override void inject(String valueText, Object injectee) throws DisplayUsageException {
          throw new DisplayUsageException();
        }
      };
      builder.put("-h", helpOption);
      builder.put("--help", helpOption);

      Method leftoverMethod = null;

      for (Field field : injectedClass.getDeclaredFields()) {
        checkArgument(!field.isAnnotationPresent(Leftovers.class),
            "Sorry, @Leftovers only works for methods at present"); // TODO(kevinb)
        Option option = field.getAnnotation(Option.class);
        if (option != null) {
          InjectableOption injectable = FieldOption.create(field);
          for (String optionName : option.value()) {
            builder.put(optionName, injectable);
          }
        }
      }
      for (Method method : injectedClass.getDeclaredMethods()) {
        if (method.isAnnotationPresent(Leftovers.class)) {
          checkArgument(!isStaticOrAbstract(method),
              "@Leftovers method cannot be static or abstract");
          checkArgument(!method.isAnnotationPresent(Option.class),
              "method has both @Option and @Leftovers");
          checkArgument(leftoverMethod == null, "Two methods have @Leftovers");

          method.setAccessible(true);
          leftoverMethod = method;

          // TODO: check type is a supertype of ImmutableList<String>
        }
        Option option = method.getAnnotation(Option.class);
        if (option != null) {
          InjectableOption injectable = MethodOption.create(method);
          for (String optionName : option.value()) {
            builder.put(optionName, injectable);
          }
        }
      }

      ImmutableMap<String, InjectableOption> optionMap = builder.build();
      return new InjectionMap(optionMap, leftoverMethod);
    }

    final ImmutableMap<String, InjectableOption> optionMap;
    final Method leftoversMethod;

    InjectionMap(ImmutableMap<String, InjectableOption> optionMap, Method leftoversMethod) {
      this.optionMap = optionMap;
      this.leftoversMethod = leftoversMethod;
    }

    InjectableOption getInjectableOption(String optionName) throws InvalidCommandException {
      InjectableOption injectable = optionMap.get(optionName);
      if (injectable == null) {
        throw new InvalidCommandException("Invalid option: %s", optionName);
      }
      return injectable;
    }
  }

  private static class FieldOption extends InjectableOption {
    private static InjectableOption create(Field field) {
      field.setAccessible(true);
      Type type = field.getGenericType();

      if (type instanceof Class) {
        return new FieldOption(field, (Class<?>) type);
      }
      throw new IllegalArgumentException("can't inject parameterized types etc.");
    }

    private Field field;
    private boolean isBoolean;
    private Parser<?> parser;

    private FieldOption(Field field, Class<?> c) {
      this.field = field;
      this.isBoolean = c == boolean.class || c == Boolean.class;
      try {
        this.parser = Parsers.conventionalParser(Primitives.wrap(c));
      } catch (NoSuchMethodException e) {
        throw new IllegalArgumentException("No suitable String-conversion method");
      }
    }

    @Override boolean isBoolean() {
      return isBoolean;
    }

    @Override void inject(String valueText, Object injectee) throws InvalidCommandException {
      Object value = convert(parser, valueText);
      try {
        field.set(injectee, value);
      } catch (IllegalAccessException impossible) {
        throw new AssertionError(impossible);
      }
    }
  }

  private static class MethodOption extends InjectableOption {
    private static InjectableOption create(Method method) {
      checkArgument(!isStaticOrAbstract(method),
          "@Option methods cannot be static or abstract");
      Class<?>[] classes = method.getParameterTypes();
      checkArgument(classes.length == 1, "Method does not have exactly one argument: " + method);
      return new MethodOption(method, classes[0]);
    }

    private Method method;
    private boolean isBoolean;
    private Parser<?> parser;

    private MethodOption(Method method, Class<?> c) {
      this.method = method;
      this.isBoolean = c == boolean.class || c == Boolean.class;
      try {
        this.parser = Parsers.conventionalParser(Primitives.wrap(c));
      } catch (NoSuchMethodException e) {
        throw new IllegalArgumentException("No suitable String-conversion method");
      }

      method.setAccessible(true);
    }

    @Override boolean isBoolean() {
      return isBoolean;
    }

    @Override boolean delayedInjection() {
      return true;
    }

    @Override void inject(String valueText, Object injectee) throws InvalidCommandException {
      invokeMethod(injectee, method, convert(parser, valueText));
    }
  }

  private static Object convert(Parser<?> parser, String valueText) throws InvalidCommandException {
    Object value;
    try {
      value = parser.parse(valueText);
    } catch (ParseException e) {
      throw new InvalidCommandException("wrong datatype: " + e.getMessage());
    }
    return value;
  }

  private void parseLongOption(String arg, Iterator<String> args) throws InvalidCommandException {
    String name = arg.replaceFirst("^--no-", "--");
    String value = null;

    // Support "--name=value" as well as "--name value".
    int equalsIndex = name.indexOf('=');
    if (equalsIndex != -1) {
      value = name.substring(equalsIndex + 1);
      name = name.substring(0, equalsIndex);
    }

    InjectableOption injectable = injectionMap.getInjectableOption(name);

    if (value == null) {
      value = injectable.isBoolean()
          ? Boolean.toString(!arg.startsWith("--no-"))
          : grabNextValue(args, name);
    }
    injectNowOrLater(injectable, value);
  }

  private void injectNowOrLater(InjectableOption injectable, String value)
      throws InvalidCommandException {
    if (injectable.delayedInjection()) {
      pendingInjections.add(new PendingInjection(injectable, value));
    } else {
      injectable.inject(value, injectee);
    }
  }

  private static class PendingInjection {
    InjectableOption injectableOption;
    String value;

    private PendingInjection(InjectableOption injectableOption, String value) {
      this.injectableOption = injectableOption;
      this.value = value;
    }
  }

  // Given boolean options a and b, and non-boolean option f, we want to allow:
  // -ab
  // -abf out.txt
  // -abfout.txt
  // (But not -abf=out.txt --- POSIX doesn't mention that either way, but GNU expressly forbids it.)

  private void parseShortOptions(String arg, Iterator<String> args) throws InvalidCommandException {
    for (int i = 1; i < arg.length(); ++i) {
      String name = "-" + arg.charAt(i);
      InjectableOption injectable = injectionMap.getInjectableOption(name);

      String value;
      if (injectable.isBoolean()) {
        value = "true";
      } else {
        // We need a value. If there's anything left, we take the rest of this "short option".
        if (i + 1 < arg.length()) {
          value = arg.substring(i + 1);
          i = arg.length() - 1; // delayed "break"

        // otherwise the next arg
        } else {
          value = grabNextValue(args, name);
        }
      }
      injectNowOrLater(injectable, value);
    }
  }

  private static void invokeMethod(Object injectee, Method method, Object value)
      throws InvalidCommandException {
    try {
      method.invoke(injectee, value);
    } catch (IllegalAccessException impossible) {
      throw new AssertionError(impossible);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      Throwables.propagateIfPossible(cause, InvalidCommandException.class);
      throw new RuntimeException(e);
    }
  }

  private String grabNextValue(Iterator<String> args, String name)
      throws InvalidCommandException {
    if (args.hasNext()) {
      return args.next();
    } else {
      throw new InvalidCommandException("option '" + name + "' requires an argument");
    }
  }

  private static boolean isStaticOrAbstract(Method method) {
    int modifiers = method.getModifiers();
    return Modifier.isStatic(modifiers) || Modifier.isAbstract(modifiers);
  }
}
TOP

Related Classes of com.google.caliper.options.CommandLineParser

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.