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

de.unkrig.commons.util.CommandLineOptions Maven / Gradle / Ivy

There is a newer version: 1.2.19
Show newest version

/*
 * de.unkrig.commons - A general-purpose Java class library
 *
 * Copyright (c) 2015, Arno Unkrig
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package de.unkrig.commons.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.ObjectUtil;
import de.unkrig.commons.lang.protocol.ProducerUtil;
import de.unkrig.commons.lang.protocol.ProducerUtil.FromArrayProducer;
import de.unkrig.commons.nullanalysis.NotNullByDefault;
import de.unkrig.commons.nullanalysis.Nullable;
import de.unkrig.commons.text.Notations;
import de.unkrig.commons.text.Notations.Phrase;
import de.unkrig.commons.text.StringStream;
import de.unkrig.commons.text.StringStream.UnexpectedElementException;
import de.unkrig.commons.text.pattern.Glob;
import de.unkrig.commons.text.pattern.Pattern2;
import de.unkrig.commons.text.pattern.PatternUtil;
import de.unkrig.commons.util.CommandLineOptionException.ArgumentConversionFailed;
import de.unkrig.commons.util.CommandLineOptionException.ConflictingOptions;
import de.unkrig.commons.util.CommandLineOptionException.DuplicateOption;
import de.unkrig.commons.util.CommandLineOptionException.OptionProcessingException;
import de.unkrig.commons.util.CommandLineOptionException.OptionArgumentMissing;
import de.unkrig.commons.util.CommandLineOptionException.RequiredOptionGroupMissing;
import de.unkrig.commons.util.CommandLineOptionException.RequiredOptionMissing;
import de.unkrig.commons.util.CommandLineOptionException.UnrecognizedOption;
import de.unkrig.commons.util.annotation.CommandLineOption;
import de.unkrig.commons.util.annotation.CommandLineOptionGroup;
import de.unkrig.commons.util.annotation.RegexFlags;

/**
 * Parses "command line options" from the args of your {@code main()} method and configures a Java bean
 * accordingly.
 *
 * @see #parse(String[], Object)
 */
public final
class CommandLineOptions {

    static { AssertionUtil.enableAssertionsForThisClass(); }

    /**
     * Detects a "command line option", i.e. a "-" followed by at least one character.
     */
    private static final Pattern REGEX_OPTION = Pattern.compile("-.+");

    /**
     * Detects a "compact command lime option", like "-lar" for the UNIX "ls" command.
     * 
*
{@code group(1)}:
The first option letter (for "-lar": "l")
*
{@code group(2)}:
The following option letters (for "-lar": "ar")
*
*/ private static final Pattern REGEX_COMPACT_OPTIONS = Pattern.compile("-([^\\-])(.*)"); private CommandLineOptions() {} /** * Invokes methods of the target object based on the args. *

* All public methods of the target (including those declared by superclasses) are regarded candidates iff * they are annotated with the {@link de.unkrig.commons.util.annotation.CommandLineOption} annotation. *

*

* The possible "names" of the command line option are derived from the {@link * de.unkrig.commons.util.annotation.CommandLineOption#name() name} element of the {@link * de.unkrig.commons.util.annotation.CommandLineOption} annotation, or, if that is missing, from the method name * (see examples below). *

*

* When an element of args equals such a name, then the following elements in args are * converted to match the parameter types of the method (see example code below). After that, the method is * invoked with the arguments. *

*

* Parsing terminates iff either *

*
    *
  • The args are exhausted
  • *
  • The special arg {@code "--"} is reached (which is consumed)
  • *
  • The special arg {@code "-"} is reached (which is notconsumed)
  • *
  • A "normal" command line arguments appears, i.e. one that does not start with "-"
  • *
*

* Example: *

*
     *   public
     *   class MyMain {
     *
     *       // ...
     *
     *       // This one maps to "-font-size <x>" and "--font-size <x>".
     *       @CommandLineOption public static void
     *       setFontSize(double size) { System.out.println("fontSize=" + size); }
     *
     *       // This one maps to "-help" and "--help".
     *       @CommandLineOption public static void
     *       help() {}
     *
     *       // This one maps to "-alpha" and "--alpha".
     *       @CommandLineOption(name = "alpha") public static void
     *       method1() {}
     *
     *       // This one maps to "-beta" and "--gamma".
     *       @CommandLineOption(name = { "-beta", "--gamma" }) public static void
     *       method2() {}
     *
     *       // This one maps to "-foo <a> <b> <c>". (A single dashes may be used instead of the double dash.)
     *       @CommandLineOption public static void
     *       foo(int one, String two, java.util.Pattern three) {}
     *
     *       // This one maps to "--person [ --name <>name> ] [ --age <age> ]". (Single dashes may be used
     *       // instead of the double dashes.)
     *       @CommandLineOption public static void
     *       addPerson(Person p) {}
     *
     *       public static
     *       class Person {
     *           public void setName(String name) {}
     *           public void setAge(int age) {}
     *       }
     *   }
     *
     *   ...
     *
     *   final MyMain main = new MyMain();
     *
     *   String[] args = { "--font-size", "17.5", "a", "b" };
     *   System.out.println(Arrays.toString(args));           // Prints "[--font-size, 17.5, a, b]".
     *
     *   args = MainBean.parseCommandLineOptions(args, main); // Prints "fontSize=17.5".
     *
     *   System.out.println(Arrays.toString(args);            // Prints "[a, b]".
     * 
* *

Option cardinality

* *

* To enforce that a particular command line option must be given a specific number of times, use the * {@link de.unkrig.commons.util.annotation.CommandLineOption#cardinality() cardinality()} element of the {@link * de.unkrig.commons.util.annotation.CommandLineOption @CommandLineOption} annotation: *

*
     *   @CommandLineOption(cardinality = CommandLineOption.Cardinality.MANDATORY)
     *   setColor(String color) {
     *       this.color = color;
     *   }
     * 
*

* For this example, it is now guaranteed that {@link #parse(String[], Object)} will invoke the "{@code * setColor()}" method exactly once. *

*

* The default value for the command line option cardinality is {@link * de.unkrig.commons.util.annotation.CommandLineOption.Cardinality#OPTIONAL}. *

* *

Option groups

* *

* To enforce that a particular number of a set of command line options must be given, use the {@link * de.unkrig.commons.util.annotation.CommandLineOption#group() group()} element of the {@link * de.unkrig.commons.util.annotation.CommandLineOption @CommandLineOption} annotation: *

*
     *   @CommandLineOption(group = Sources.class)
     *   setFile(File file) {
     *       this.file = file;
     *   }
     *
     *   @CommandLineOption(group = Sources.class)
     *   setStdin() {
     *       this.stdin = true;
     *   }
     *
     *   // This interface solely serves as the "connector" between the related command line options; it is (typically)
     *   // not used otherwise.
     *   @CommandLineOptionGroup(cardinality = CommandLineOptionGroup.Cardinality.EXACTLY_ONE)
     *   interface Sources {}
     * 
*

* For this example, it is now guaranteed that {@link #parse(String[], Object)} will invoke exactly one of the * methods "{@code setFile()}" and "{@code setStdin()}". *

*

* The default value for the command line option group cardinality is {@link * de.unkrig.commons.util.annotation.CommandLineOptionGroup.Cardinality#ZERO_OR_ONE ZERO_OR_ONE}. *

* *

Argument conversion

* *

* args strings are converted to match the parameter types of the methods as follows: *

*
    *
  • *
* * @return The args, less the elements that were parsed as command line * options * @throws CommandLineOptionException An error occurred during the parsing; typically a command-line application * would print the message of the exception to STDERR and call "{@code * System.exit(1)}" */ public static String[] parse(String[] args, Object target) throws CommandLineOptionException { StringStream ss = new StringStream(ProducerUtil.fromArray(args)); Parser p = new Parser(target.getClass()); // Parse all command line options and apply them to the "target". p.parseOptions(ss, target); // Consume the optional separator "--" between the command line options and the "normal" command line // arguments. if (!ss.peekRead("--") && ss.peek(CommandLineOptions.REGEX_OPTION)) { throw new UnrecognizedOption(AssertionUtil.notNull(ss.group(0))); } // Return the remaining command line arguments. return ss.readRest(); } private static class Parser { // CONFIGURATION /** Option name => {@link Method} */ private final Map allOptions = new LinkedHashMap(); /** The options that must appear at most once. */ private final Set singularOptions = new HashSet(); /** The options that must appear at least once. */ private final List requiredOptions = new ArrayList(); /** The option groups with cardinality 1 or less. */ private final Set> singularOptionGroups = new HashSet>(); /** The option groups with cardinality 0 or more. */ private final List> requiredOptionGroups = new ArrayList>(); /** Maps option methods to the set of groups that each is a member of. */ private final Map>> optionToOptionGroups = new HashMap>>(); // PARSING STATE /** Options that were parsed so far. */ private final Set actualOptions = new HashSet(); /** Options groups that were parsed so far. */ private final Set> actualOptionGroups = new HashSet>(); /** * Analyzes the targetClass for any command line option setters. * * @throws AssertionError A {@link CommandLineOption @CommandLineOption} refers to a type that is not * annotated with {@link CommandLineOptionGroup @CommandLineOptionGroup} * @throws AssertionError Two methods map to the same name */ Parser(Class targetClass) { // Identify all command line options that apply to the target class, and populate the "methodCache". Method[] methods = targetClass.getMethods(); Arrays.sort(methods, new Comparator() { @NotNullByDefault(false) @Override public int compare(Method m1, Method m2) { return m1.toString().compareTo(m2.toString()); } }); for (Method m : methods) { CommandLineOption clo = m.getAnnotation(CommandLineOption.class); if (clo == null) continue; // Determine the "names" of the command-line option - either from the "name" element of the // "@CommandLineOption" annotation, or from the method name. String[] names = clo.name(); if (names.length == 0) { String n = m.getName(); if (n.startsWith("set")) { n = n.substring(3); } else if (n.startsWith("add")) { n = n.substring(3); } names = new String[] { Notations.fromCamelCase(n).toLowerCaseHyphenated() }; } for (String name : names) { for (String name2 : ( name.startsWith("-") ? new String[] { name } : new String[] { "-" + name, "--" + name } )) { Method prev = this.allOptions.put(name2, m); assert prev == null : "Two methods map to option \"" + name2 + "\": \"" + prev + "\" and \"" + m + "\""; // SUPPRESS CHECKSTYLE LineLength } } // Process the option cardinality. { CommandLineOption.Cardinality c = clo.cardinality(); if (c == CommandLineOption.Cardinality.MANDATORY || c == CommandLineOption.Cardinality.OPTIONAL) { this.singularOptions.add(m); } if ( c == CommandLineOption.Cardinality.MANDATORY || c == CommandLineOption.Cardinality.ONCE_OR_MORE ) { this.requiredOptions.add(m); } } // Process the option group cardinality. for (Class optionGroup : clo.group()) { CommandLineOptionGroup clog = optionGroup.getAnnotation(CommandLineOptionGroup.class); if (clog == null) { throw new AssertionError( "Option group class \"" + optionGroup + "\" lacks the \"@CommandLineOptionGroup\" annotation" ); } Set> optionGroups = this.optionToOptionGroups.get(m); if (optionGroups == null) { this.optionToOptionGroups.put(m, (optionGroups = new HashSet>())); } optionGroups.add(optionGroup); CommandLineOptionGroup.Cardinality c = clog.cardinality(); if ( c == CommandLineOptionGroup.Cardinality.EXACTLY_ONE || c == CommandLineOptionGroup.Cardinality.ZERO_OR_ONE ) this.singularOptionGroups.add(optionGroup); if ( c == CommandLineOptionGroup.Cardinality.EXACTLY_ONE || c == CommandLineOptionGroup.Cardinality.ONE_OR_MORE ) this.requiredOptionGroups.add(optionGroup); } } } /** * Parses tokens on the ss that map to options of the target. * * @return Whether the next token(s) on ss were parsed as an option of * the target * @throws CommandLineOptionException An error occurred during the parsing * @throws EX ss threw an exception */ private void parseOptions(StringStream ss, Object target) throws EX, CommandLineOptionException { // Parse as many command line options as possible. while (this.parseNextOption(ss, target)); // Verify that all "required" options were parsed. for (Method option : this.requiredOptions) { if (!this.actualOptions.contains(option)) { throw new RequiredOptionMissing(option, this.optionNames(option)); } } for (Class optionGroup : this.requiredOptionGroups) { if (!this.actualOptionGroups.contains(optionGroup)) { throw new RequiredOptionGroupMissing(optionGroup, this.optionNames(optionGroup)); } } } /** * @return All names of the option */ private String[] optionNames(Method option) { List result = new ArrayList(); for (Entry e : this.allOptions.entrySet()) { final String optionName = e.getKey(); final Method o = e.getValue(); if (o != option) continue; result.add(optionName); } return result.toArray(new String[result.size()]); } /** * @return All names of all options that are members of the optionGroup */ private String[] optionNames(Class optionGroup) { List result = new ArrayList(); for (Entry e : this.allOptions.entrySet()) { final String optionName = e.getKey(); final Method option = e.getValue(); for (Class g : option.getAnnotation(CommandLineOption.class).group()) { if (g == optionGroup) { result.add(optionName); break; } } } return result.toArray(new String[result.size()]); } /** * Parses an option of the target iff the next token on the ss maps to an option of the * target. * * @return Whether the next token(s) on ss were parsed as an option of the * target * @throws CommandLineOptionException An error occurred during the parsing * @throws EX ss threw an exception */ private boolean parseNextOption(StringStream ss, Object target) throws CommandLineOptionException, EX { if (ss.atEnd()) return false; // Special option "--" indicates the end of the command line options. if (ss.peek("--")) return false; // "-" is not a command line option, but a normal command line argument (typically means "STDIN" or // "STDOUT"). if (ss.peek("-")) return false; // Is it a "verbose" (non-compact) option? if (this.parseNextVerboseOption(ss, target)) return true; // Iff the first letter after "-" can be interpreted as a single-letter option, then interpret ALL letters // as such. // This feature is called "compact command line options": E.g. "-lar" is equivalent with "-l -a -r". // Even options with arguments are possible, e.g. "-abc AA BB CC" in place of "-a AA -b BB -c CC". COMPACT_OPTIONS: if (ss.peek(CommandLineOptions.REGEX_COMPACT_OPTIONS)) { char firstOptionLetter = AssertionUtil.notNull(ss.group(1)).charAt(0); String followingOptionLetters = AssertionUtil.notNull(ss.group(2)); { String firstOptionName = "-" + firstOptionLetter; Method firstOption = this.getOptionByName(firstOptionName); if (firstOption == null) break COMPACT_OPTIONS; // Only now that we have verified that the first letter can be interpreted as a single-letter // option, we consume the token. ss.consume(); this.applyCommandLineOption(firstOptionName, firstOption, ss, target); } for (int i = 0; i < followingOptionLetters.length(); i++) { String optionName = "-" + followingOptionLetters.charAt(i); Method optionMethod = this.getOptionByName(optionName); if (optionMethod == null) { throw new UnrecognizedOption(optionName); } this.applyCommandLineOption(optionName, optionMethod, ss, target); } return true; } return false; } /** * @return Whether the next tokens on ss could be parsed as an option for * the target * @throws CommandLineOptionException An error occurred during the parsing * @throws EX ss threw an exception */ private boolean parseNextVerboseOption(StringStream ss, Object target) throws CommandLineOptionException, EX { String s = ss.next(); if (s == null) return false; // Special handling for verbose option syntax "--regex=abc". ALTERNATE_VERBOSE_SYNTAX: { int ioeq = s.indexOf('='); if (ioeq == -1) break ALTERNATE_VERBOSE_SYNTAX; final String optionName = s.substring(0, ioeq); final String singleArgument = s.substring(ioeq + 1); Method option = this.getOptionByName(optionName); if (option == null) return false; // Notice: "Method.getParameterCount()" is only available in Java 8+. if (option.getParameterTypes().length != 1) throw new UnrecognizedOption(s); ss.consume(); this.applyCommandLineOption( optionName, // optionName option, // option new StringStream(ProducerUtil.fromElements(singleArgument)), // stringStream target // target ); return true; } // Parse syntax "-foo arg1 arg2 arg3". String optionName = s; Method option = this.getOptionByName(optionName); if (option == null) return false; ss.consume(); this.applyCommandLineOption( optionName, // optionName option, // option ss, // stringStream target // target ); return true; } /** * @return {@code null} iff there is no applicable method */ @Nullable public Method getOptionByName(String optionName) { return this.allOptions.get(optionName); } /** * Parses the command line option's arguments from the args and invokes the method. *

* The arguments for the method are parsed from the command line, see {@link #getArgument(String[], int[], * Annotation[], Class)}. *

*

* Iff the method is a {@code varargs} method, then all remaining arguments are converted to an * array. *

* * @throws CommandLineOptionException An error occurred during the parsing * @throws EX The stringStream threw an exception */ public void applyCommandLineOption( String optionName, Method option, StringStream stringStream, @Nullable Object target ) throws CommandLineOptionException, EX { if (this.singularOptions.contains(option) && this.actualOptions.contains(option)) { throw new DuplicateOption(option, optionName, this.optionNames(option)); } Set> optionGroups = this.optionToOptionGroups.get(option); if (optionGroups != null) { for (Class optionGroup : optionGroups) { if ( this.singularOptionGroups.contains(optionGroup) && this.actualOptionGroups.contains(optionGroup) ) { throw new ConflictingOptions(optionGroup, option, optionName); } } } Class[] methodParametersTypes = option.getParameterTypes(); Annotation[][] methodParametersAnnotations = option.getParameterAnnotations(); assert methodParametersTypes.length == methodParametersAnnotations.length; // Convert the command line option arguments into method call arguments. Object[] methodArgs = new Object[methodParametersTypes.length]; for (int i = 0; i < methodArgs.length; i++) { try { methodArgs[i] = this.getArgument( stringStream, // stringStream methodParametersAnnotations[i], // annotations methodParametersTypes[i] // targetType ); } catch (UnexpectedElementException uee) { throw new OptionArgumentMissing(option, optionName, i); } } // Now that the "methodArgs" array is filled, invoke the method. try { option.invoke(target, methodArgs); } catch (InvocationTargetException ite) { throw new OptionProcessingException(optionName, ite.getTargetException()); } catch (Exception e) { throw new OptionProcessingException(optionName, e); } this.actualOptions.add(option); if (optionGroups != null) { for (Class optionGroup : optionGroups) { this.actualOptionGroups.add(optionGroup); } } } /** * Creates and returns an object of the given targetType from tokens from ss. *
    *
  • * For an array target type, all remaining tokens from ss are parsed as array elements. *
  • *
  • * For {@link Pattern} target type, the next token from ss is converted through {@link * Pattern2#compile(String, int)}, with the flags as configured by the {@link RegexFlags} annotation of * the method. *
  • *
  • * For {@link Glob} target type, the next token from ss is converted through {@link * Glob#compile(String, int)}, with the flags as configured by the {@link RegexFlags} annotation of * the method. *
  • *
  • * For a bean target type (a class that has only the zero-arg constructor), the bean is instantiated and as * many tokens from ss as possible are parsed as options of the bean. *
  • *
  • * For {@link InetAddress} target type, the next token from ss is converted through {@link * InetAddress#getByName(String)}, except "any", which maps to {@code null} (the "wildcard address"). *
  • *
  • * For any other target type, the next token from ss is converted to the target type, as * described {@link ObjectUtil#fromString(String, Class) here}. *
  • *
* * @throws CommandLineOptionException An error occurred during the parsing * @throws UnexpectedElementException The ss has too few tokens * @throws EX The ss threw an exception * @throws AssertionError Bean creation failed */ @Nullable private Object getArgument(StringStream ss, Annotation[] annotations, Class targetType) throws CommandLineOptionException, UnexpectedElementException, EX { // Special case: The target type is an array type. (This handles also the case of a VARARGS method.) if (targetType.isArray()) { final Class componentType = targetType.getComponentType(); // Treat all remaining arguments as array elements (even those starting with a dash!). List elements = new ArrayList(); while (!ss.atEnd()) { elements.add(this.getArgument(ss, annotations, componentType)); } int size = elements.size(); Object result = Array.newInstance(componentType, size); if (componentType.isPrimitive()) { // Can't use "System.arraycopy()" to copy to array of primitive. for (int i = 0; i < size; i++) Array.set(result, i, elements.get(i)); } else { System.arraycopy(elements.toArray(), 0, result, 0, size); } return result; } // Special case: Target type "java.util.Pattern". if (targetType == Pattern.class) { // Use "Pattern2", so that the Pattern2.WILDCARD flag can also be used. return Pattern2.compile(ss.read(), CommandLineOptions.getRegexFlags(annotations)); } // Special case: Target type "de.unkrig.commons.text.pattern.Glob". if (targetType == Glob.class) { return Glob.compile(ss.read(), CommandLineOptions.getRegexFlags(annotations)); } // Special case: Target type is a Java bean. Constructor[] cs = targetType.getConstructors(); if (cs.length == 1 && cs[0].getParameterTypes().length == 0) { Object bean; try { bean = cs[0].newInstance(); } catch (Exception e) { throw new AssertionError(e); } // Invoke as many setter methods on the bean as possible. new Parser(targetType).parseOptions(ss, bean); return bean; } // Special case: Target type "java.net.InetAddress". if (targetType == InetAddress.class) { String host = ss.read(); if ("any".equals(host)) return null; try { return InetAddress.getByName(host); } catch (UnknownHostException uhe) { throw new ArgumentConversionFailed(host, InetAddress.class, uhe); } } // Fall back to "ObjectUtil.fromString()". String arg = ss.read(); try { return ObjectUtil.fromString(arg, targetType); } catch (IllegalArgumentException iae) { throw new ArgumentConversionFailed(arg, targetType, iae); } } } /** * Checks for a {@link RegexFlags} annotation and returns its value. */ private static int getRegexFlags(Annotation[] annotations) { for (Annotation a : annotations) { if (a.annotationType() == RegexFlags.class) { return ((RegexFlags) a).value(); } } return 0; } /** * Determines the method of {@code target.getClass()} that is applicable for the given optionName. The * rules for matching are as follows: *
    *
  • The method must be annotated with {@link CommandLineOption @CommandLineOption}.
  • *
  • * If that annotation has the "{@code name}" element: *
      *
    • * If the {@code name} value starts with "-": *
        *
      • * The optionName equals the {@code name} value. *
      • *
      *
    • *
    • * Otherwise, if the {@code name} value does not start with "-": *
        *
      • * The optionName equals {@code "-"+name} or {@code "--"+name} *
      • *
      *
    • *
    * Example: *
    * @CommandLineOption(name = { "alpha", "-beta" }) public void method() { ... *
    * matches if optionName is {@code "-alpha"}, {@code "--alpha"} and {@code "-beta"}. *
  • *
  • * Otherwise, if that annotation does not have the "{@code name}" element: *
      *
    • * The method name, with an optional "set" or "add" prefix removed, then converted to {@link * Phrase#toLowerCaseHyphenated() lower-case-hyphenated}, then prefixed with "-" and "--", equals the * optionName. *
      * Example: Method name "setFooBar()" matches if optionName is "-foo-bar" or "--foo-bar". *
    • *
    *
  • *
* * @return {@code null} iff there is no applicable method */ @Nullable public static Method getMethodForOption(String optionName, Class targetClass) { return new Parser(targetClass).getOptionByName(optionName); } /** * Parses the command line option's arguments from the args and invokes the method. *

* Iff the method is a {@code varargs} method, then all remaining arguments are converted to an array. *

* * @param optionArgumentIndex The position of the first option argument in args * @return The position of the last option argument in args plus one * @throws CommandLineOptionException An error occurred during the parsing * @throws AssertionError The method is not annotated with {@link * CommandLineOption @CommandLineOption} */ public static int applyCommandLineOption( String optionName, Method option, String[] args, int optionArgumentIndex, @Nullable Object target ) throws CommandLineOptionException { Class targetClass = target != null ? target.getClass() : option.getDeclaringClass(); FromArrayProducer fap = ProducerUtil.fromArray(args, optionArgumentIndex, args.length); new Parser(targetClass).applyCommandLineOption( optionName, option, new StringStream(fap), target ); return fap.index(); } /** * Reads (and decodes) the contents of a resource, replaces all occurrences of * "${system-property-name}" with the value of the designated system property, * and writes the result to the outputStream (typically {@code System.out}). *

* The resource is found through the baseClass's class loader and the name {@code * "}package-name{@code /}simple-class-name{@code .}relativeResourceName{@code * "}, where all periods in the package-name have been replaced with slashes. *

*

* To ensure that the resource is decoded with the same charset as it was encoded, you should not use the JVM * charset (which could be different in the build environment and the runtime environment). *

* * @param baseClass Poses the base for the resource name * @param relativeResourceName The name of the resource, relative to the baseClass * @param resourceCharset The charset to use for decoding the contents of the resource; {@code null} for the * JVM default charset * @throws FileNotFoundException The resource could not be found * @see Class#getResource(String) */ public static void printResource( Class baseClass, String relativeResourceName, @Nullable Charset resourceCharset, OutputStream outputStream ) throws IOException { String resourceName = baseClass.getSimpleName() + "." + relativeResourceName; InputStream is = baseClass.getResourceAsStream(resourceName); if (is == null) throw new FileNotFoundException(resourceName); CommandLineOptions.replaceSystemProperties( is, // inputStream resourceCharset, // inputCharset true, // closeInputStream outputStream, // outputStream null, // outputCharset false // closeOutputStream ); } /** * Reads (and decodes) the contents of a resource, replaces all occurrences of * "${system-property-name}" with the value of the designated system property, * and writes the result to the outputStream (typically {@code System.out}). *

* The resource is found through the classLoader and (absolute) resourceName. *

*

* To ensure that the resource is decoded with the same charset as it was encoded, you should not use the JVM * charset (which could be different in the build environment and the runtime environment). *

* * @param resourceCharset The charset to use for decoding the contents of the resource; {@code null} for the * JVM default charset * @throws FileNotFoundException The resource could not be found * @see ClassLoader#getResource(String) */ public static void printResource( ClassLoader classLoader, String resourceName, @Nullable Charset resourceCharset, OutputStream outputStream ) throws IOException { InputStream is = classLoader.getResourceAsStream(resourceName); if (is == null) throw new FileNotFoundException(resourceName); CommandLineOptions.replaceSystemProperties( is, // inputStream resourceCharset, // inputCharset true, // closeInputStream outputStream, // outputStream null, // outputCharset false // closeOutputStream ); } private static void replaceSystemProperties( InputStream inputStream, @Nullable Charset inputCharset, boolean closeInputStream, OutputStream outputStream, @Nullable Charset outputCharset, boolean closeOutputStream ) throws IOException { try { if (outputCharset == null) outputCharset = Charset.defaultCharset(); Writer w = new OutputStreamWriter(outputStream, outputCharset); if (inputCharset == null) inputCharset = Charset.defaultCharset(); Reader r = new InputStreamReader(inputStream, inputCharset); PatternUtil.replaceSystemProperties(r, w); w.flush(); } finally { if (closeInputStream) try { inputStream.close(); } catch (Exception e) {} if (closeOutputStream) try { outputStream.close(); } catch (Exception e) {} } } }