blob: b126eafa1e1c63ad2e5b6a44ce65e15d1f4eb77e [file] [log] [blame]
/*
* Copyright 2003-2010 the original author or authors.
*
* 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 groovy.util
import org.apache.commons.cli.*
import org.codehaus.groovy.runtime.InvokerHelper
/**
* Provides a builder to assist the processing of command line arguments.
* <p>
* Typical usage (emulate partial arg processing of unix command: ls -alt *.groovy):
* <pre>
* def cli = new CliBuilder(usage:'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
* -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 using an
* alternate 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</code> 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(args:1, argName:'file', 'use given file for log')
* cli.D(args:2, valueSeparator:'=', argName:'property=value',
* 'use value for given property')
* def options = cli.parse(args)
* ...
* </pre>
* Usage message would be:
* <pre>
* usage: ant [options] [targets]
* Options:
* -D &lt;property=value> use value for given property
* -help print this message
* -logfile &lt;file> use given file for log
* </pre>
* And if called with the following arguments '-logfile foo -Dbar=baz target'
* then the following assertions would be true:
* <pre>
* assert options // would be null (false) on failure
* assert options.arguments() == ['target']
* assert options.Ds == ['bar', 'baz']
* assert options.logfile == '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(usage:'curl [options] &lt;url&gt;')
* 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', args:1, argName:'URL', 'Set URL to work with')
* </pre>
* Which has the following usage message:
* <pre>
* usage: curl [options] &lt;url>
* --basic Use HTTP Basic Authentication
* -d,--data &lt;data> HTTP POST data
* -G,--get Send the -d data with a HTTP GET
* -q If used as the first parameter disables .curlrc
* --url &lt;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 behavior is undefined.
* <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 org.apache.commons.cli.*
* ... as before ...
* cli << new Option('q', false, 'If used as the first parameter disables .curlrc')
* cli << OptionBuilder.withLongOpt('url').hasArg().withArgName('URL').
* withDescription('Set URL to work with').create()
* ...
* </pre>
*
* 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>Supported Option Properties</b>:
* <pre>
* argName: String
* longOpt: String
* args: int
* optionalArg: boolean
* required: boolean
* type: Object (not currently used)
* valueSeparator: char
* </pre>
* See {@link org.apache.commons.cli.Option} for the meaning of these properties
* and {@link CliBuilderTest} for further examples.
* <p/>
*
* @author Dierk Koenig
* @author Paul King
*/
class CliBuilder {
/**
* Usage summary displayed as the first line when <code>cli.usage()</code> is called.
*/
String usage = 'groovy'
/**
* Normally set internally but allows you full customisation of the underlying processing engine.
*/
CommandLineParser parser = null
/**
* To change from the default PosixParser to the GnuParser, set this to false. Ignored if the parser is explicitly set.
*/
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
/**
* Normally set internally but can be overridden if you want to customise how the usage message is displayed.
*/
HelpFormatter formatter = new HelpFormatter()
/**
* Defaults to stdout but you can provide your own PrintWriter if desired.
*/
PrintWriter writer = new PrintWriter(System.out)
/**
* Optional additional message for usage; displayed after the usage summary but before the options are displayed.
*/
String header = ''
/**
* Optional additional message for usage; displayed after the options are displayed.
*/
String footer = ''
/**
* Indicates that option processing should continue for all arguments even
* if arguments not recognized as options are encountered (default true).
*/
boolean stopAtNonOption = true
/**
* Allows customisation of the usage message width.
*/
int width = formatter.defaultWidth
/**
* Not normally accessed directly but full access to underlying options if needed.
*/
Options options = new Options()
/**
* 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)) {
options.addOption(option(name, [:], args[0]))
return null
}
if (args.size() == 1 && args[0] instanceof Option && name == 'leftShift') {
options.addOption(args[0])
return null
}
if (args.size() == 2 && args[0] instanceof Map) {
options.addOption(option(name, args[0], args[1]))
return null
}
}
return InvokerHelper.getMetaClass(this).invokeMethod(this, name, args)
}
/**
* Make options accessible from command line args with parser (default: Posix).
* Returns null on bad command lines after displaying usage message.
*/
OptionAccessor parse(args) {
if (expandArgumentFiles) args = expandArgumentFiles(args)
if (!parser) {
parser = posix ? new PosixParser() : new GnuParser()
}
try {
return new OptionAccessor(parser.parse(options, args as String[], stopAtNonOption))
} catch (ParseException pe) {
writer.println("error: " + pe.message)
usage()
return null
}
}
/**
* Print the usage message with writer (default: System.out) and formatter (default: HelpFormatter)
*/
void usage() {
formatter.printHelp(writer, width, usage, header, options, formatter.defaultLeftPad, formatter.defaultDescPad, footer)
writer.flush()
}
// implementation details -------------------------------------
/**
* Internal method: How to create an option from the specification.
*/
Option option(shortname, Map details, info) {
Option option
if (shortname == '_') {
option = OptionBuilder.withDescription(info).withLongOpt(details.longOpt).create()
details.remove('longOpt')
} else {
option = new Option(shortname, info)
}
details.each { key, value -> option[key] = value }
return option
}
static expandArgumentFiles(args) throws IOException {
def result = []
for (arg in args) {
if (arg && arg != '@' && arg[0] == '@') {
arg = arg.substring(1)
if (arg[0] != '@') {
expandArgumentFile(arg, result)
continue
}
}
result << arg
}
return result
}
private static expandArgumentFile(name, args) throws IOException {
def charAsInt = { String s -> s.toCharacter() as int }
new File(name).withReader { r ->
new StreamTokenizer(r).with {
resetSyntax()
wordChars(charAsInt(' '), 255)
whitespaceChars(0, charAsInt(' '))
commentChar(charAsInt('#'))
quoteChar(charAsInt('"'))
quoteChar(charAsInt('\''))
while (nextToken() != StreamTokenizer.TT_EOF) {
args << sval
}
}
}
}
}
class OptionAccessor {
CommandLine inner
OptionAccessor(CommandLine inner) {
this.inner = inner
}
def invokeMethod(String name, Object args) {
return InvokerHelper.getMetaClass(inner).invokeMethod(inner, name, args)
}
def getProperty(String name) {
def methodname = 'getOptionValue'
if (name.size() > 1 && name.endsWith('s')) {
def singularName = name[0..-2]
if (hasOption(singularName)) {
name = singularName
methodname += 's'
}
}
if (name.size() == 1) name = name as char
def result = InvokerHelper.getMetaClass(inner).invokeMethod(inner, methodname, name)
if (null == result) result = inner.hasOption(name)
if (result instanceof String[]) result = result.toList()
return result
}
List<String> arguments() {
inner.args.toList()
}
}