| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 groovy.cli.picocli |
| |
| import groovy.cli.CliBuilderException |
| import groovy.cli.Option |
| import groovy.cli.TypedOption |
| import groovy.cli.Unparsed |
| import groovy.transform.Undefined |
| import org.codehaus.groovy.runtime.DefaultGroovyMethods |
| import org.codehaus.groovy.runtime.InvokerHelper |
| import org.codehaus.groovy.runtime.MetaClassHelper |
| import picocli.CommandLine |
| import picocli.CommandLine.ITypeConverter |
| import picocli.CommandLine.Model.CommandSpec |
| import picocli.CommandLine.Model.IGetter |
| import picocli.CommandLine.Model.ISetter |
| import picocli.CommandLine.Model.OptionSpec |
| import picocli.CommandLine.Model.ParserSpec |
| import picocli.CommandLine.Model.PositionalParamSpec |
| import picocli.CommandLine.Model.UsageMessageSpec |
| |
| import java.lang.reflect.Field |
| import java.lang.reflect.Method |
| |
| /** |
| * Provides a builder to assist the processing of command line arguments. |
| * Two styles are supported: dynamic api style (declarative method calls provide a mini DSL for describing options) |
| * and annotation style (annotations on an interface or class describe options). |
| * <p> |
| * <b>Dynamic api style</b> |
| * <p> |
| * Typical usage (emulate partial arg processing of unix command: ls -alt *.groovy): |
| * <pre> |
| * def cli = new CliBuilder(name:'ls') |
| * cli.a('display all files') |
| * cli.l('use a long listing format') |
| * cli.t('sort by modification time') |
| * def options = cli.parse(args) |
| * assert options // would be null (false) on failure |
| * assert options.arguments() == ['*.groovy'] |
| * assert options.a && options.l && options.t |
| * </pre> |
| * The usage message for this example (obtained using <code>cli.usage()</code>) is shown below: |
| * <pre> |
| * Usage: ls [-alt] |
| * -a display all files |
| * -l use a long listing format |
| * -t sort by modification time |
| * </pre> |
| * An underlying parser that supports what is called argument 'bursting' is used |
| * by default. Bursting would convert '-alt' into '-a -l -t' provided no long |
| * option exists with value 'alt' and provided that none of 'a', 'l' or 't' |
| * takes an argument (in fact the last one is allowed to take an argument). |
| * The bursting behavior can be turned off by configuring the underlying parser. |
| * The simplest way to achieve this is by setting the posix property on the CliBuilder |
| * to false, i.e. include {@code posix: false} in the constructor call. |
| * <p> |
| * Another example (partial emulation of arg processing for 'ant' command line): |
| * <pre> |
| * def cli = new CliBuilder(usage:'ant [options] [targets]', |
| * header:'Options:') |
| * cli.help('print this message') |
| * cli.logfile(type:File, argName:'file', 'use given file for log') |
| * cli.D(type:Map, argName:'property=value', 'use value for given property') |
| * cli.lib(argName:'path', valueSeparator:',', args: '3', |
| * 'comma-separated list of 3 paths to search for jars and classes') |
| * def options = cli.parse(args) |
| * ... |
| * </pre> |
| * Usage message would be: |
| * <pre> |
| * Usage: ant [options] [targets] |
| * Options: |
| * -D= <property=value> use value for given property |
| * -help print this message |
| * -lib=<path>,<path>,<path> |
| * comma-separated list of 3 paths to search for jars and |
| * classes |
| * -logfile=<file> use given file for log |
| * </pre> |
| * And if called with the following arguments '-logfile foo -Dbar=baz -lib=/tmp,/usr/lib,~/libs target' |
| * then the following assertions would be true: |
| * <pre> |
| * assert options // would be null (false) on failure |
| * assert options.arguments() == ['target'] |
| * assert options.D == ['bar': 'baz'] |
| * assert options.libs == ['/tmp', '/usr/lib', '~/libs'] |
| * assert options.lib == '/tmp' |
| * assert options.logfile == new File('foo') |
| * </pre> |
| * Note the use of some special notation. By adding 's' onto an option |
| * that may appear multiple times and has an argument or as in this case |
| * uses a valueSeparator to separate multiple argument values |
| * causes the list of associated argument values to be returned. |
| * <p> |
| * Another example showing long options (partial emulation of arg processing for 'curl' command line): |
| * <pre> |
| * def cli = new CliBuilder(name:'curl') |
| * cli._(longOpt:'basic', 'Use HTTP Basic Authentication') |
| * cli.d(longOpt:'data', args:1, argName:'data', 'HTTP POST data') |
| * cli.G(longOpt:'get', 'Send the -d data with a HTTP GET') |
| * cli.q('If used as the first parameter disables .curlrc') |
| * cli._(longOpt:'url', type:URL, argName:'URL', 'Set URL to work with') |
| * </pre> |
| * Which has the following usage message: |
| * <pre> |
| * Usage: curl [-Gq] [--basic] [--url=<URL>] [-d=<data>] |
| * --basic Use HTTP Basic Authentication |
| * -d, --data=<data> HTTP POST data |
| * -G, --get Send the -d data with a HTTP GET |
| * -q If used as the first parameter disables .curlrc |
| * --url=<URL> Set URL to work with |
| * </pre> |
| * This example shows a common convention. When mixing short and long names, the |
| * short names are often one character in size. One character options with |
| * arguments don't require a space between the option and the argument, e.g. |
| * <code>-Ddebug=true</code>. The example also shows |
| * the use of '_' when no short option is applicable. |
| * <p> |
| * Also note that '_' was used multiple times. This is supported but if |
| * any other shortOpt or any longOpt is repeated, then the underlying library throws an exception. |
| * <p> |
| * Short option names may not contain a hyphen. If a long option name contains a hyphen, e.g. '--max-wait' then you can either |
| * use the long hand method call <code>options.hasOption('max-wait')</code> or surround |
| * the option name in quotes, e.g. <code>options.'max-wait'</code>. |
| * <p> |
| * Although CliBuilder on the whole hides away the underlying library used |
| * for processing the arguments, it does provide some hooks which let you |
| * make use of the underlying library directly should the need arise. For |
| * example, the last two lines of the 'curl' example above could be replaced |
| * with the following: |
| * <pre> |
| * import picocli.CommandLine.Model.* |
| * ... as before ... |
| * cli << OptionSpec.builder('-q'). |
| * description('If used as the first parameter disables .curlrc').build() |
| * cli << OptionSpec.builder('--url').type(URL.class).paramLabel('<URL>'). |
| * description('Set URL to work with').build() |
| * ... |
| * </pre> |
| * As another example, the <code>usageMessage</code> property gives |
| * fine-grained control over the usage help message (see the |
| * <a href="http://picocli.info/#_usage_help_with_styles_and_colors">picocli user manual</a> |
| * for details): |
| * |
| * <pre> |
| * def cli = new CliBuilder() |
| * cli.name = "myapp" |
| * cli.usageMessage.with { |
| * headerHeading("@|bold,underline Header heading:|@%n") |
| * header("Header 1", "Header 2") // before the synopsis |
| * synopsisHeading("%n@|bold,underline Usage:|@ ") |
| * descriptionHeading("%n@|bold,underline Description heading:|@%n") |
| * description("Description 1", "Description 2") // after the synopsis |
| * optionListHeading("%n@|bold,underline Options heading:|@%n") |
| * footerHeading("%n@|bold,underline Footer heading:|@%n") |
| * footer("Footer 1", "Footer 2") |
| * }</pre> |
| * |
| * <p> |
| * <b>Supported Option Properties</b>: |
| * <table border="1" cellspacing="0"> |
| * <tr> |
| * <th>Property</th> |
| * <th>Type</th> |
| * <th>Picocli equivalent</th> |
| * <th>Description</th> |
| * </tr> |
| * <tr> |
| * <th><code>argName</code></th> |
| * <td>String</td> |
| * <td><code>names</code></td> |
| * <td>Short name for the option, will be prefixed with a single hyphen.</td> |
| * </tr> |
| * <tr> |
| * <th><code>longOpt</code></th> |
| * <td>String</td> |
| * <td><code>names</code></td> |
| * <td>Long name for the option, will be prefixed with two hyphens |
| * unless {@link CliBuilder#acceptLongOptionsWithSingleHyphen acceptLongOptionsWithSingleHyphen} |
| * is <code>true</code>. |
| * An option must have either a long name or a short name (or both).</td> |
| * </tr> |
| * <tr> |
| * <th><code>args</code></th> |
| * <td>int or String</td> |
| * <td><code>arity</code></td> |
| * <td><code>args</code> indicates the number of parameters for this option. |
| * A String value of '+' indicates at least one up to any number of parameters. |
| * The minimum number of parameters depends on the type (booleans require no parameters) |
| * and the <code>optionalArg</code> setting. |
| * <code>args</code> can often be omitted if a <code>type</code> is specified. |
| * </td> |
| * </tr> |
| * <tr> |
| * <th><code>optionalArg</code></th> |
| * <td>boolean</td> |
| * <td><code>arity</code></td> |
| * <td>If <code>optionalArg=true</code>, then <code>args=3</code> |
| * is the equivalent of <code>arity="0..3"</code> in picocli. |
| * When <code>optionalArg=true</code>, <code>args='+'</code> |
| * is equivalent to <code>arity="0..*"</code>. |
| * </td> |
| * </tr> |
| * <tr> |
| * <th><code>required</code></th> |
| * <td>boolean</td> |
| * <td><code>required</code></td> |
| * <td>If <code>true</code>, this option must be specified on the command line, or an exception is thrown. |
| * </td> |
| * </tr> |
| * <tr> |
| * <th><code>type</code></th> |
| * <td>Class</td> |
| * <td><code>type</code></td> |
| * <td>Option parameters are converted to this type. The underlying library has built-in converters for |
| * <a href="http://picocli.info/#_built_in_types">many types</a>. |
| * A custom converter can be specified with the <code>convert</code> property. |
| * </td> |
| * </tr> |
| * <tr> |
| * <th><code>convert</code></th> |
| * <td>Closure</td> |
| * <td><code>converter</code></td> |
| * <td>A closure that takes a single String parameter and returns an object converted to the <code>type</code> of this option. |
| * The picocli equivalent is the <code><a href="http://picocli.info/#_custom_type_converters">ITypeConverter</a></code> interface. |
| * </td> |
| * </tr> |
| * <tr> |
| * <th><code>valueSeparator</code></th> |
| * <td>char</td> |
| * <td><code>splitRegex</code></td> |
| * <td>The character used to split a single command line argument into parts. |
| * </td> |
| * </tr> |
| * <tr> |
| * <th><code>defaultValue</code></th> |
| * <td>String</td> |
| * <td><code>defaultValue</code></td> |
| * <td>The value the option should have if it did not appear on the command line. |
| * The specified String value will be split into parts with the <code>valueSeparator</code> and |
| * converted to the option <code>type</code> before it is set. |
| * </td> |
| * </tr> |
| * </table> |
| * See {@link groovy.cli.picocli.CliBuilderTest} for further examples. |
| * <p> |
| * <b>@-files</b> |
| * <p> |
| * CliBuilder also supports Argument File processing. If an argument starts with |
| * an '@' character followed by a filename, then the contents of the file with name |
| * filename are placed into the command line. The feature can be turned off by |
| * setting expandArgumentFiles to false. If turned on, you can still pass a real |
| * parameter with an initial '@' character by escaping it with an additional '@' |
| * symbol, e.g. '@@foo' will become '@foo' and not be subject to expansion. As an |
| * example, if the file temp.args contains the content: |
| * <pre> |
| * -arg1 |
| * paramA |
| * paramB paramC |
| * </pre> |
| * Then calling the command line with: |
| * <pre> |
| * someCommand @temp.args -arg2 paramD |
| * </pre> |
| * Is the same as calling this: |
| * <pre> |
| * someCommand -arg1 paramA paramB paramC -arg2 paramD |
| * </pre> |
| * This feature is particularly useful on operating systems which place limitations |
| * on the size of the command line (e.g. Windows). The feature is similar to |
| * the 'Command Line Argument File' processing supported by javadoc and javac. |
| * Consult the corresponding documentation for those tools if you wish to see further examples. |
| * <p> |
| * <b>Annotation style with an interface</b> |
| * <p> |
| * With this style an interface is defined containing an annotated method for each option. |
| * It might look like this (following roughly the earlier 'ls' example): |
| * <pre> |
| * import groovy.cli.Option |
| * import groovy.cli.Unparsed |
| * |
| * interface OptionInterface { |
| * @{@link groovy.cli.Option}(shortName='a', description='display all files') boolean all() |
| * @{@link groovy.cli.Option}(shortName='l', description='use a long listing format') boolean longFormat() |
| * @{@link groovy.cli.Option}(shortName='t', description='sort by modification time') boolean time() |
| * @{@link groovy.cli.Unparsed} List remaining() |
| * } |
| * </pre> |
| * Then this description is supplied to CliBuilder during parsing, e.g.: |
| * <pre> |
| * def args = '-alt *.groovy'.split() // normally from commandline itself |
| * def cli = new CliBuilder(usage:'ls') |
| * def options = cli.parseFromSpec(OptionInterface, args) |
| * assert options.remaining() == ['*.groovy'] |
| * assert options.all() && options.longFormat() && options.time() |
| * </pre> |
| * <p> |
| * <b>Annotation style with a class</b> |
| * <p> |
| * With this style a user-supplied instance is used. Annotations on that instance's class |
| * members (properties and setter methods) indicate how to set options and provide the option details |
| * using annotation attributes. |
| * It might look like this (again using the earlier 'ls' example): |
| * <pre> |
| * import groovy.cli.Option |
| * import groovy.cli.Unparsed |
| * |
| * class OptionClass { |
| * @{@link groovy.cli.Option}(shortName='a', description='display all files') boolean all |
| * @{@link groovy.cli.Option}(shortName='l', description='use a long listing format') boolean longFormat |
| * @{@link groovy.cli.Option}(shortName='t', description='sort by modification time') boolean time |
| * @{@link groovy.cli.Unparsed} List remaining |
| * } |
| * </pre> |
| * Then this description is supplied to CliBuilder during parsing, e.g.: |
| * <pre> |
| * def args = '-alt *.groovy'.split() // normally from commandline itself |
| * def cli = new CliBuilder(usage:'ls') |
| * def options = new OptionClass() |
| * cli.parseFromInstance(options, args) |
| * assert options.remaining == ['*.groovy'] |
| * assert options.all && options.longFormat && options.time |
| * </pre> |
| */ |
| class CliBuilder { |
| /** |
| * The command synopsis displayed as the first line in the usage help message, e.g., when <code>cli.usage()</code> is called. |
| * When not set, a default synopsis is generated that shows the supported options and parameters. |
| * @see #name |
| */ |
| String usage = 'groovy' |
| |
| /** |
| * This property allows customizing the program name displayed in the synopsis when <code>cli.usage()</code> is called. |
| * Ignored if the {@link #usage} property is set. |
| * @since 2.5 |
| */ |
| String name = 'groovy' |
| |
| /** |
| * To disallow clustered POSIX short options, set this to false. |
| */ |
| Boolean posix = true |
| |
| /** |
| * Whether arguments of the form '{@code @}<i>filename</i>' will be expanded into the arguments contained within the file named <i>filename</i> (default true). |
| */ |
| boolean expandArgumentFiles = true |
| |
| /** |
| * Configures what the parser should do when arguments not recognized |
| * as options are encountered: when <code>true</code> (the default), the |
| * remaining arguments are all treated as positional parameters. |
| * When <code>false</code>, the parser will continue to look for options, and |
| * only the unrecognized arguments are treated as positional parameters. |
| */ |
| boolean stopAtNonOption = true |
| |
| /** |
| * For backwards compatibility with Apache Commons CLI, set this property to |
| * <code>true</code> if the parser should recognize long options with both |
| * a single hyphen and a double hyphen prefix. The default is <code>false</code>, |
| * so only long options with a double hyphen prefix (<code>--option</code>) are recognized. |
| * @since 2.5 |
| */ |
| boolean acceptLongOptionsWithSingleHyphen = false |
| |
| /** |
| * The PrintWriter to write the {@link #usage} help message to |
| * when <code>cli.usage()</code> is called. |
| * Defaults to stdout but you can provide your own PrintWriter if desired. |
| */ |
| PrintWriter writer = new PrintWriter(System.out) |
| |
| /** |
| * The PrintWriter to write to when invalid user input was provided to |
| * the {@link #parse(java.lang.String[])} method. |
| * Defaults to stderr but you can provide your own PrintWriter if desired. |
| * @since 2.5 |
| */ |
| PrintWriter errorWriter = new PrintWriter(System.err) |
| |
| /** |
| * Optional additional message for usage; displayed after the usage summary |
| * but before the options are displayed. |
| */ |
| String header = null |
| |
| /** |
| * Optional additional message for usage; displayed after the options. |
| */ |
| String footer = null |
| |
| /** |
| * Allows customisation of the usage message width. |
| */ |
| int width = UsageMessageSpec.DEFAULT_USAGE_WIDTH |
| |
| /** |
| * Not normally accessed directly but allows fine-grained control over the |
| * parser behaviour via the API of the underlying library if needed. |
| * @since 2.5 |
| */ |
| // Implementation note: this object is separate from the CommandSpec. |
| // The values collected here are copied into the ParserSpec of the command. |
| final ParserSpec parser = new ParserSpec() |
| .stopAtPositional(true) |
| .unmatchedOptionsArePositionalParams(true) |
| .aritySatisfiedByAttachedOptionParam(true) |
| .limitSplit(true) |
| .overwrittenOptionsAllowed(true) |
| .toggleBooleanFlags(false) |
| |
| /** |
| * Not normally accessed directly but allows fine-grained control over the |
| * usage help message via the API of the underlying library if needed. |
| * @since 2.5 |
| */ |
| // Implementation note: this object is separate from the CommandSpec. |
| // The values collected here are copied into the UsageMessageSpec of the command. |
| final UsageMessageSpec usageMessage = new UsageMessageSpec() |
| |
| /** |
| * Internal data structure mapping option names to their associated {@link TypedOption} object. |
| */ |
| Map<String, TypedOption> savedTypeOptions = new HashMap<String, TypedOption>() |
| |
| // CommandSpec is the entry point into the picocli object model for a command. |
| // It gives access to a ParserSpec to customize the parser behaviour and |
| // a UsageMessageSpec to customize the usage help message. |
| // Add OptionSpec and PositionalParamSpec objects to this object to define |
| // the options and positional parameters this command recognizes. |
| // |
| // This field is private for now. |
| // It is initialized to an empty spec so options and positional parameter specs |
| // can be added dynamically via the programmatic API. |
| // When a command spec is defined via annotations, the existing instance is |
| // replaced with a new one. This allows the outer CliBuilder instance can be reused. |
| private CommandSpec commandSpec = CommandSpec.create() |
| |
| /** |
| * Sets the {@link #usage usage} property on this <code>CliBuilder</code> and the |
| * <code>customSynopsis</code> on the {@link #usageMessage} used by the underlying library. |
| * @param usage the custom synopsis of the usage help message |
| */ |
| void setUsage(String usage) { |
| this.usage = usage |
| usageMessage.customSynopsis(usage) |
| } |
| |
| /** |
| * Sets the {@link #footer} property on this <code>CliBuilder</code> |
| * and on the {@link #usageMessage} used by the underlying library. |
| * @param footer the footer of the usage help message |
| */ |
| void setFooter(String footer) { |
| this.footer = footer |
| usageMessage.footer(footer) |
| } |
| |
| /** |
| * Sets the {@link #header} property on this <code>CliBuilder</code> and the |
| * <code>description</code> on the {@link #usageMessage} used by the underlying library. |
| * @param header the description text of the usage help message |
| */ |
| void setHeader(String header) { |
| this.header = header |
| // "header" is displayed after the synopsis in previous CliBuilder versions. |
| // The picocli equivalent is the "description". |
| usageMessage.description(header) |
| } |
| |
| /** |
| * Sets the {@link #width} property on this <code>CliBuilder</code> |
| * and on the {@link #usageMessage} used by the underlying library. |
| * @param width the width of the usage help message |
| */ |
| void setWidth(int width) { |
| this.width = width |
| usageMessage.width(width) |
| } |
| |
| /** |
| * Sets the {@link #expandArgumentFiles} property on this <code>CliBuilder</code> |
| * and on the {@link #parser} used by the underlying library. |
| * @param expand whether to expand argument @-files |
| */ |
| void setExpandArgumentFiles(boolean expand) { |
| this.expandArgumentFiles = expand |
| parser.expandAtFiles(expand) |
| } |
| |
| /** |
| * Sets the {@link #posix} property on this <code>CliBuilder</code> and the |
| * <code>posixClusteredShortOptionsAllowed</code> property on the {@link #parser} |
| * used by the underlying library. |
| * @param posix whether to allow clustered short options |
| */ |
| void setPosix(Boolean posix) { |
| this.posix = posix |
| parser.posixClusteredShortOptionsAllowed(posix ?: false) |
| } |
| |
| /** |
| * Sets the {@link #stopAtNonOption} property on this <code>CliBuilder</code> and the |
| * <code>stopAtPositional</code> property on the {@link #parser} |
| * used by the underlying library. |
| * @param stopAtNonOption when <code>true</code> (the default), the |
| * remaining arguments are all treated as positional parameters. |
| * When <code>false</code>, the parser will continue to look for options, and |
| * only the unrecognized arguments are treated as positional parameters. |
| */ |
| void setStopAtNonOption(boolean stopAtNonOption) { |
| this.stopAtNonOption = stopAtNonOption |
| parser.stopAtPositional(stopAtNonOption) |
| parser.unmatchedOptionsArePositionalParams(stopAtNonOption) |
| } |
| |
| /** |
| * For backwards compatibility reasons, if a custom {@code writer} is set, this sets |
| * both the {@link #writer} and the {@link #errorWriter} to the specified writer. |
| * @param writer the writer to initialize both the {@code writer} and the {@code errorWriter} to |
| */ |
| void setWriter(PrintWriter writer) { |
| this.writer = writer |
| this.errorWriter = writer |
| } |
| |
| public <T> TypedOption<T> option(Map args, Class<T> type, String description) { |
| def name = args.opt ?: '_' |
| args.type = type |
| args.remove('opt') |
| "$name"(args, description) |
| } |
| |
| /** |
| * Internal method: Detect option specification method calls. |
| */ |
| def invokeMethod(String name, Object args) { |
| if (args instanceof Object[]) { |
| if (args.size() == 1 && (args[0] instanceof String || args[0] instanceof GString)) { |
| def option = option(name, [:], args[0]) // args[0] is description |
| commandSpec.addOption(option) |
| return create(option, null, null, null) |
| } |
| if (args.size() == 1 && args[0] instanceof OptionSpec && name == 'leftShift') { |
| OptionSpec option = args[0] as OptionSpec |
| commandSpec.addOption(option) |
| return create(option, null, null, null) |
| } |
| if (args.size() == 2 && args[0] instanceof Map) { |
| Map m = args[0] as Map |
| if (m.type && !(m.type instanceof Class)) { |
| throw new CliBuilderException("'type' must be a Class") |
| } |
| def option = option(name, m, args[1]) |
| commandSpec.addOption(option) |
| return create(option, m.type, option.defaultValue(), option.converters()) |
| } |
| } |
| return InvokerHelper.getMetaClass(this).invokeMethod(this, name, args) |
| } |
| |
| private TypedOption create(OptionSpec o, Class theType, defaultValue, convert) { |
| String opt = o.names().sort { a, b -> a.length() - b.length() }.first() |
| opt = opt?.length() == 2 ? opt.substring(1) : null |
| |
| String longOpt = o.names().sort { a, b -> b.length() - a.length() }.first() |
| longOpt = longOpt?.startsWith("--") ? longOpt.substring(2) : null |
| |
| Map<String, Object> result = new TypedOption<Object>() |
| if (opt != null) result.put("opt", opt) |
| result.put("longOpt", longOpt) |
| result.put("cliOption", o) |
| if (defaultValue) { |
| result.put("defaultValue", defaultValue) |
| } |
| if (convert) { |
| if (theType) { |
| throw new CliBuilderException("You can't specify 'type' when using 'convert'") |
| } |
| result.put("convert", convert) |
| result.put("type", convert instanceof Class ? convert : convert.getClass()) |
| } else { |
| result.put("type", theType) |
| } |
| savedTypeOptions[longOpt ?: opt] = result |
| result |
| } |
| |
| /** |
| * Make options accessible from command line args with parser. |
| * Returns null on bad command lines after displaying usage message. |
| */ |
| OptionAccessor parse(args) { |
| CommandLine commandLine = createCommandLine() |
| try { |
| def accessor = new OptionAccessor(commandLine.parseArgs(args as String[])) |
| accessor.savedTypeOptions = savedTypeOptions |
| return accessor |
| } catch (CommandLine.ParameterException pe) { |
| errorWriter.println("error: " + pe.message) |
| printUsage(pe.commandLine, errorWriter) |
| return null |
| } |
| } |
| |
| private CommandLine createCommandLine() { |
| commandSpec.parser(parser) |
| commandSpec.name(name).usageMessage(usageMessage) |
| if (commandSpec.positionalParameters().empty) { |
| commandSpec.addPositional(PositionalParamSpec.builder().type(String[]).arity("*").paramLabel("P").hidden(true).build()) |
| } |
| return new CommandLine(commandSpec) |
| } |
| |
| /** |
| * Prints the usage message with the specified {@link #header header}, {@link #footer footer} and {@link #width width} |
| * to the specified {@link #writer writer} (default: System.out). |
| */ |
| void usage() { |
| printUsage(commandSpec.commandLine() ?: createCommandLine(), writer) |
| } |
| |
| private void printUsage(CommandLine commandLine, PrintWriter pw) { |
| commandLine.usage(pw) |
| pw.flush() |
| } |
| |
| /** |
| * Given an interface containing members with annotations, derive |
| * the options specification. |
| * |
| * @param optionsClass |
| * @param args |
| * @return an instance containing the processed options |
| */ |
| public <T> T parseFromSpec(Class<T> optionsClass, String[] args) { |
| def cliOptions = [:] |
| commandSpec = CommandSpec.create() |
| addOptionsFromAnnotations(optionsClass, cliOptions, true) |
| addPositionalsFromAnnotations(optionsClass, cliOptions, true) |
| parse(args) |
| DefaultGroovyMethods.asType(cliOptions, optionsClass) |
| } |
| |
| /** |
| * Given an instance containing members with annotations, derive |
| * the options specification. |
| * |
| * @param optionInstance |
| * @param args |
| * @return the options instance populated with the processed options |
| */ |
| public <T> T parseFromInstance(T optionInstance, args) { |
| commandSpec = CommandSpec.create() |
| addOptionsFromAnnotations(optionInstance.getClass(), optionInstance, false) |
| addPositionalsFromAnnotations(optionInstance.getClass(), optionInstance, false) |
| def optionAccessor = parse(args) |
| |
| // initialize the boolean properties that were not matched |
| if (optionAccessor) { |
| optionAccessor.parseResult.commandSpec().options().each { option -> |
| if (!optionAccessor.parseResult.hasMatchedOption(option)) { |
| boolean isFlag = option.arity().max == 0 && option.type().simpleName.toLowerCase() == 'boolean' |
| if (isFlag) { option.value = false } // else default has already been applied |
| } |
| } |
| } |
| optionInstance |
| } |
| |
| private void addOptionsFromAnnotations(Class optionClass, Object target, boolean isCoercedMap) { |
| optionClass.methods.findAll{ it.getAnnotation(Option) }.each { Method m -> |
| Option annotation = m.getAnnotation(Option) |
| ArgSpecAttributes attributes = extractAttributesFromMethod(m, isCoercedMap, target) |
| commandSpec.addOption(createOptionSpec(annotation, attributes, target)) |
| } |
| def optionFields = optionClass.declaredFields.findAll { it.getAnnotation(Option) } |
| if (optionClass.isInterface() && !optionFields.isEmpty()) { |
| throw new CliBuilderException("@Option only allowed on methods in interface " + optionClass.simpleName) |
| } |
| optionFields.each { Field f -> |
| Option annotation = f.getAnnotation(Option) |
| ArgSpecAttributes attributes = extractAttributesFromField(f, target) |
| commandSpec.addOption(createOptionSpec(annotation, attributes, target)) |
| } |
| } |
| |
| private void addPositionalsFromAnnotations(Class optionClass, Object target, boolean isCoercedMap) { |
| optionClass.methods.findAll{ it.getAnnotation(Unparsed) }.each { Method m -> |
| Unparsed annotation = m.getAnnotation(Unparsed) |
| ArgSpecAttributes attributes = extractAttributesFromMethod(m, isCoercedMap, target) |
| commandSpec.addPositional(createPositionalParamSpec(annotation, attributes, target)) |
| } |
| def optionFields = optionClass.declaredFields.findAll { it.getAnnotation(Unparsed) } |
| if (optionClass.isInterface() && !optionFields.isEmpty()) { |
| throw new CliBuilderException("@Unparsed only allowed on methods in interface " + optionClass.simpleName) |
| } |
| optionFields.each { Field f -> |
| Unparsed annotation = f.getAnnotation(Unparsed) |
| ArgSpecAttributes attributes = extractAttributesFromField(f, target) |
| commandSpec.addPositional(createPositionalParamSpec(annotation, attributes, target)) |
| } |
| } |
| |
| private static class ArgSpecAttributes { |
| Class type |
| Class[] auxiliaryTypes |
| String label |
| IGetter getter |
| ISetter setter |
| Object initialValue |
| boolean hasInitialValue |
| } |
| |
| private ArgSpecAttributes extractAttributesFromMethod(Method m, boolean isCoercedMap, target) { |
| Class type = isCoercedMap ? m.returnType : (m.parameterTypes.size() > 0 ? m.parameterTypes[0] : m.returnType) |
| type = type && type == Void.TYPE ? null : type |
| |
| Class[] auxTypes = null // TODO extract generic types like List<Integer> or Map<Integer,Double> |
| |
| // If the method is a real setter, we can't invoke it to get its value, |
| // so instead we need to keep track of its current value ourselves. |
| // Additionally, implementation classes may annotate _getter_ methods with @Option; |
| // if the getter returns a Collection or Map, picocli will add parsed values to it. |
| def currentValue = initialValue(type, m, target, isCoercedMap) |
| def getter = { |
| currentValue |
| } |
| def setter = { |
| def old = currentValue |
| currentValue = it |
| if (!isCoercedMap && m.parameterTypes.size() > 0) { |
| m.invoke(target, [currentValue].toArray()) |
| } |
| return old |
| } |
| if (isCoercedMap) { |
| target[m.name] = getter |
| } |
| def label = m.name.startsWith("set") || m.name.startsWith("get") ? MetaClassHelper.convertPropertyName(m.name.substring(3)) : m.name |
| new ArgSpecAttributes(type: type, auxiliaryTypes: auxTypes, label: label, getter: getter, setter: setter, initialValue: currentValue, hasInitialValue: isCoercedMap) |
| } |
| |
| private Object initialValue(Class<?> cls, Method m, Object target, boolean isCoercedMap) { |
| if (m.parameterTypes.size() == 0 && m.returnType != Void.TYPE) { // annotated getter |
| if (!isCoercedMap) { |
| return m.invoke(target) |
| } |
| if (cls.primitive) { |
| if (cls.simpleName.toLowerCase() == 'boolean') { |
| return false |
| } |
| return 0 |
| } |
| return target[m.name] |
| } |
| // annotated setter |
| if (List.class.isAssignableFrom(cls)) { // TODO support other Collections in future |
| return new ArrayList() |
| } |
| if (Map.class.isAssignableFrom(cls)) { |
| return new LinkedHashMap() |
| } |
| null |
| } |
| |
| private ArgSpecAttributes extractAttributesFromField(Field f, target) { |
| def getter = { |
| f.accessible = true |
| f.get(target) |
| } |
| def setter = { newValue -> |
| f.accessible = true |
| def oldValue = f.get(target) |
| f.set(target, newValue) |
| oldValue |
| } |
| Class[] auxTypes = null // TODO extract generic types like List<Integer> or Map<Integer,Double> |
| new ArgSpecAttributes(type: f.type, auxiliaryTypes: auxTypes, label: f.name, getter: getter, setter: setter, initialValue: getter.call(), hasInitialValue: true) |
| } |
| |
| private PositionalParamSpec createPositionalParamSpec(Unparsed unparsed, ArgSpecAttributes attr, Object target) { |
| PositionalParamSpec.Builder builder = PositionalParamSpec.builder() |
| |
| CommandLine.Range arity = CommandLine.Range.valueOf("0..*") |
| if (attr.type == Object) { attr.type = String[] } |
| if (attr.type) { builder.type(attr.type) } // cannot set type to null |
| if (attr.auxiliaryTypes) { builder.auxiliaryTypes(attr.auxiliaryTypes) } // cannot set aux types to null |
| builder.arity(arity) |
| builder.description(unparsed.description()) |
| builder.paramLabel("<$attr.label>") |
| builder.getter(attr.getter) |
| builder.setter(attr.setter) |
| builder.hasInitialValue(attr.hasInitialValue) |
| if (arity.max == 0 && attr.type.simpleName.toLowerCase() == 'boolean' && !attr.initialValue) { |
| attr.initialValue = false |
| } |
| try { |
| builder.initialValue(attr.initialValue) |
| } catch (Exception ex) { |
| throw new CliBuilderException("Could not get initial value of positional parameters: " + ex, ex) |
| } |
| builder.build() |
| } |
| |
| private OptionSpec createOptionSpec(Option annotation, ArgSpecAttributes attr, Object target) { |
| Map names = calculateNames(annotation.longName(), annotation.shortName(), attr.label) |
| String arityString = extractArity(attr.type, annotation.optionalArg(), annotation.numberOfArguments(), annotation.numberOfArgumentsString(), names) |
| CommandLine.Range arity = CommandLine.Range.valueOf(arityString) |
| if (attr.type == Object && arity.max == 0) { attr.type = boolean } |
| OptionSpec.Builder builder = OptionSpec.builder(hyphenate(names)) |
| if (attr.type) { builder.type(attr.type) } // cannot set type to null |
| if (attr.auxiliaryTypes) { builder.auxiliaryTypes(attr.auxiliaryTypes) } // cannot set aux types to null |
| builder.arity(arity) |
| builder.description(annotation.description()) |
| builder.splitRegex(annotation.valueSeparator()) |
| if (annotation.defaultValue()) { builder.defaultValue(annotation.defaultValue()) } // don't default picocli model to empty string |
| builder.paramLabel("<$attr.label>") |
| if (annotation.convert() != Undefined.CLASS) { |
| if (annotation.convert() instanceof Class) { |
| builder.converters(annotation.convert().newInstance(target, target) as ITypeConverter) |
| } |
| } |
| builder.getter(attr.getter) |
| builder.setter(attr.setter) |
| builder.hasInitialValue(attr.hasInitialValue) |
| if (arity.max == 0 && attr.type.simpleName.toLowerCase() == 'boolean' && !attr.initialValue) { |
| attr.initialValue = false |
| } |
| try { |
| builder.initialValue(attr.initialValue) |
| } catch (Exception ex) { |
| throw new CliBuilderException("Could not get initial value of option " + names + ": " + ex, ex) |
| } |
| builder.build() |
| } |
| |
| private String[] hyphenate(Map<String, String> names) { |
| def both = acceptLongOptionsWithSingleHyphen |
| names.values().findAll { it && it != "_" }.collect { it.length() == 1 ? "-$it" : (both ? ["-$it", "--$it"] : ["--$it"]) }.flatten().toArray() |
| } |
| |
| private String extractArity(Class<?> type, boolean optionalArg, int numberOfArguments, String numberOfArgumentsString, Map names) { |
| if (optionalArg && (!type || !isMultiValue(type))) { |
| throw new CliBuilderException("Attempted to set optional argument for single-value type on flag '${names.long ?: names.short}'") |
| } |
| if (numberOfArguments != 1 && numberOfArgumentsString) { |
| throw new CliBuilderException("You can't specify both 'numberOfArguments' and 'numberOfArgumentsString' on flag '${names.long ?: names.short}'") |
| } |
| def isFlag = type.simpleName.toLowerCase() == 'boolean' || |
| (type.simpleName.toLowerCase() == 'object' && (numberOfArguments == 0 || numberOfArgumentsString == "0")) |
| String arity = "0" |
| if (numberOfArgumentsString) { |
| String max = numberOfArgumentsString.replace('+', '*') |
| arity = optionalArg ? "0..$max" : "1..$max" |
| } else { |
| if (!isFlag) { |
| arity = optionalArg ? "0..$numberOfArguments" : "1..$numberOfArguments" |
| } |
| } |
| if (arity == "0" && !(isFlag || type.name == 'java.lang.Object')) { |
| throw new CliBuilderException("Flag '${names.long ?: names.short}' must be Boolean or Object") |
| } |
| arity |
| } |
| private static boolean isMultiValue(Class<?> cls) { |
| cls.isArray() || Collection.class.isAssignableFrom(cls) || Map.class.isAssignableFrom(cls) |
| } |
| |
| private Map calculateNames(String longName, String shortName, String label) { |
| boolean useShort = longName == '_' |
| if (longName == '_') longName = "" |
| def result = longName ?: label |
| [long: useShort ? "" : result, short: (useShort && !shortName) ? result : shortName] |
| } |
| |
| // implementation details ------------------------------------- |
| /** |
| * Internal method: How to create an OptionSpec from the specification. |
| */ |
| OptionSpec option(shortname, Map details, description) { |
| OptionSpec.Builder builder |
| if (shortname == '_') { |
| builder = OptionSpec.builder("--$details.longOpt").description(description) |
| if (acceptLongOptionsWithSingleHyphen) { |
| builder.names("-$details.longOpt", "--$details.longOpt") |
| } |
| details.remove('longOpt') |
| } else { |
| builder = OptionSpec.builder("-$shortname").description(description) |
| } |
| commons2picocli(shortname, details).each { key, value -> |
| if (builder.hasProperty(key)) { |
| builder[key] = value |
| } else if (key != 'opt') { // GROOVY-8607 ignore opt since we already have that |
| builder.invokeMethod(key, value) |
| } |
| } |
| if (!builder.type() && !builder.arity() && builder.converters()?.length > 0) { |
| builder.arity("1").type(details.convert ? Object : String[]) |
| } |
| return builder.build() |
| } |
| |
| /** Commons-cli constant that specifies the number of argument values is infinite */ |
| private static final int COMMONS_CLI_UNLIMITED_VALUES = -2 |
| |
| // - argName: String |
| // - longOpt: String |
| // - args: int or String |
| // - optionalArg: boolean |
| // - required: boolean |
| // - type: Class |
| // - valueSeparator: char |
| // - convert: Closure |
| // - defaultValue: String |
| private Map commons2picocli(shortname, Map m) { |
| if (m.args && m.optionalArg) { |
| m.arity = "0..${m.args}" |
| m.remove('args') |
| m.remove('optionalArg') |
| } |
| if (!m.containsKey('defaultValue')) { // GROOVY-9599 |
| m.remove('defaultValue') // don't default the picocli model to empty string |
| } |
| def result = m.collectMany { k, v -> |
| if (k == 'args' && v == '+') { |
| [[arity: '1..*']] |
| } else if (k == 'args' && v == 0) { |
| [[arity: '0']] |
| } else if (k == 'args') { |
| v == COMMONS_CLI_UNLIMITED_VALUES ? [[arity: "*"]] : [[arity: "$v"]] |
| } else if (k == 'optionalArg') { |
| v ? [[arity: '0..1']] : [[arity: '1']] |
| } else if (k == 'argName') { |
| [[paramLabel: "<$v>"]] |
| } else if (k == 'longOpt') { |
| acceptLongOptionsWithSingleHyphen ? |
| [[names: ["-$shortname", "-$v", "--$v"] as String[] ]] : |
| [[names: ["-$shortname", "--$v"] as String[] ]] |
| } else if (k == 'valueSeparator') { |
| [[splitRegex: "$v"]] |
| } else if (k == 'convert') { |
| [[converters: [v] as ITypeConverter[] ]] |
| } else { |
| [[(k): v]] |
| } |
| }.sum() as Map |
| result |
| } |
| } |