blob: 493c7b86183b43cd26b3259e67a79d1c4c228187 [file] [log] [blame]
/*
*
* 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 org.apache.qpid.server.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* CommandLineParser provides a utility for specifying the format of a command line and parsing command lines to ensure
* that they fit their specified format. A command line is made up of flags and options, both may be referred to as
* options. A flag is an option that does not take an argument (specifying it means it has the value 'true' and not
* specifying it means it has the value 'false'). Options must take arguments but they can be set up with defaults so
* that they take a default value when not set. Options may be mandatory in which case it is an error not to specify
* them on the command line. Flags are never mandatory because they are implicitly set to false when not specified.
*
* <p>Some example command lines are:
*
* <ul>
* <li>This one has two options that expect arguments:
* <pre>
* cruisecontrol -configfile cruisecontrol.xml -port 9000
* </pre>
* <li>This has one no-arg flag and two 'free' arguments:
* <pre>
* zip -r project.zip project/*
* </pre>
* <li>This one concatenates multiple flags into a single block with only one '-':
* <pre>
* jar -tvf mytar.tar
* </pre>
* </ul>
*
* <p>The parsing rules are:
*
* <ol>
* <li>Flags may be combined after a single '-' because they never take arguments. Normally such flags are single letter
* flags but this is only a convention and not enforced. Flags of more than one letter are usually specified on their own.
* <li>Options expecting arguments must always be on their own.
* <li>The argument to an option may be separated from it by whitespace or appended directly onto the option.
* <li>The argument to an option may never begin with a '-' character.
* <li>All other arguments not beginning with a '-' character are free arguments that do not belong to any option.
* <li>The second or later of a set of duplicate or repeated flags are ignored.
* <li>Options are matched up to the shortest matching option. This is because of the possibility of having no space
* between an option and its argument. This rules out the possibility of using two options where one is an opening
* substring of the other. For example, the options "foo" and "foobar" cannot be used on the same command line because
* it is not possible to distinguish the argument "-foobar" from being the "foobar" option or the "foo" option with
* the "bar" argument.
* </ol>
*
* <p>By default, unknown options are simply ignored if specified on the command line. This behaviour may be changed
* so that the parser reports all unknowns as errors by using the {@link #setErrorsOnUnknowns} method.
*/
public class CommandLineParser
{
/** Holds a mapping from command line option names to detailed information about those options. */
private Map<String, CommandLineOption> optionMap = new HashMap<String, CommandLineOption>();
/** Holds a list of parsing errors. */
private List<String> parsingErrors = new ArrayList<String>();
/** Holds the regular expression matcher to match command line options with. */
private Matcher optionMatcher = null;
/** Holds the parsed command line properties after parsing. */
private Properties parsedProperties = null;
/** Flag used to indicate that errors should be created for unknown options. False by default. */
private boolean errorsOnUnknowns = false;
/**
* Creates a command line options parser from a command line specification. This is passed to this constructor
* as an array of arrays of strings. Each array of strings specifies the command line for a single option. A static
* array may therefore easily be used to configure the command line parser in a single method call with an easily
* readable format.
*
* <p>Each array of strings must be 2, 3, 4 or 5 elements long. If any of the last three elements are missing they
* are assumed to be null. The elements specify the following parameters:
* <ol>
* <li>The name of the option without the leading '-'. For example, "file". To specify the format of the 'free'
* arguments use the option names "1", "2", ... and so on.
* <li>The option comment. A line of text describing the usage of the option. For example, "The file to be processed."
* <li>The options argument. This is a very short description of the argument to the option, often a single word
* or a reminder as to the arguments format. When this element is null the option is a flag and does not
* accept any arguments. For example, "filename" or "(unix | windows)" or null. The actual text specified
* is only used to print in the usage message to remind the user of the usage of the option.
* <li>The mandatory flag. When set to "true" an option must always be specified. Any other value, including null,
* means that the option is mandatory. Flags are always mandatory (see class javadoc for explanation of why) so
* this is ignored for flags.
* <li>A regular expression describing the format that the argument must take. Ignored if null.
* </ol>
* <p>An example call to this constructor is:
*
* <pre>
* CommandLineParser commandLine = new CommandLineParser(
* new String[][] {{"file", "The file to be processed. ", "filename", "true"},
* {"dir", "Directory to store results in. Current dir used if not set.", "out dir"},
* {"os", "Operating system EOL format to use.", "(windows | unix)", null, "windows\|unix"},
* {"v", "Verbose mode. Prints information about the processing as it goes."},
* {"1", "The processing command to run.", "command", "true", "add\|remove\|list"}});
* </pre>
*
* @param config The configuration as an array of arrays of strings.
*/
public CommandLineParser(String[][] config)
{
// Loop through all the command line option specifications creating details for each in the options map.
for (int i = 0; i < config.length; i++)
{
String[] nextOptionSpec = config[i];
addOption(nextOptionSpec[0], nextOptionSpec[1], (nextOptionSpec.length > 2) ? nextOptionSpec[2] : null,
(nextOptionSpec.length > 3) ? ("true".equals(nextOptionSpec[3]) ? true : false) : false,
(nextOptionSpec.length > 4) ? nextOptionSpec[4] : null);
}
}
/**
* Lists all the parsing errors from the most recent parsing in a string.
*
* @return All the parsing errors from the most recent parsing.
*/
public String getErrors()
{
// Return the empty string if there are no errors.
if (parsingErrors.isEmpty())
{
return "";
}
// Concatenate all the parsing errors together.
StringBuilder result = new StringBuilder();
for (String s : parsingErrors)
{
result.append(s);
}
return result.toString();
}
/**
* Lists the properties set from the most recent parsing or an empty string if no parsing has been done yet.
*
* @return The properties set from the most recent parsing or an empty string if no parsing has been done yet.
*/
public String getOptionsInForce()
{
// Check if there are no properties to report and return and empty string if so.
if (parsedProperties == null)
{
return "";
}
// List all the properties.
StringBuilder result = new StringBuilder("Options in force:\n");
for (Map.Entry<Object, Object> property : parsedProperties.entrySet())
{
result.append(property.getKey())
.append(" = ")
.append(property.getValue())
.append('\n');
}
return result.toString();
}
/**
* Generates a usage string consisting of the name of each option and each options argument description and
* comment.
*
* @return A usage string for all the options.
*/
public String getUsage()
{
String result = "Options:\n";
// Print usage on each of the command line options.
for (CommandLineOption optionInfo : optionMap.values())
{
result +=
"-" + optionInfo.option + " " + ((optionInfo.argument != null) ? (optionInfo.argument + " ") : "")
+ optionInfo.comment + "\n";
}
return result;
}
/**
* Control the behaviour of the errors on unkowns reporting. When turned on this reports all unkowns options
* as errors. When turned off, all unknowns are simply ignored.
*
* @param errors The setting of the errors on unkown flag. True to turn it on.
*/
public void setErrorsOnUnknowns(boolean errors)
{
errorsOnUnknowns = errors;
}
/**
* Parses a set of command line arguments into a set of properties, keyed by the argument flag. The free arguments
* are keyed by integers as strings starting at "1" and then "2", ... and so on.
*
* <p>See the class level comment for a description of the parsing rules.
*
* @param args The command line arguments.
*
* @return The arguments as a set of properties.
*
* @throws IllegalArgumentException If the command line cannot be parsed against its specification. If this exception
* is thrown a call to {@link #getErrors} will provide a diagnostic of the command
* line errors.
*/
public Properties parseCommandLine(String[] args) throws IllegalArgumentException
{
Properties options = new Properties();
// Used to keep count of the current 'free' argument.
int free = 1;
// Used to indicate that the most recently parsed option is expecting arguments.
boolean expectingArgs = false;
// The option that is expecting arguments from the next element of the command line.
String optionExpectingArgs = null;
// Used to indicate that the most recently parsed option is a duplicate and should be ignored.
boolean ignore = false;
// Create the regular expression matcher for the command line options.
StringBuilder regexp = new StringBuilder("^(");
int optionsAdded = 0;
for (Iterator<String> i = optionMap.keySet().iterator(); i.hasNext();)
{
String nextOption = i.next();
// Check that the option is not a free argument definition.
boolean notFree = false;
try
{
Integer.parseInt(nextOption);
}
catch (NumberFormatException e)
{
notFree = true;
}
// Add the option to the regular expression matcher if it is not a free argument definition.
if (notFree)
{
regexp.append(nextOption)
.append(i.hasNext() ? "|" : "");
optionsAdded++;
}
}
// There has to be more that one option in the regular expression or else the compiler complains that the close
// cannot be nullable if the '?' token is used to make the matched option string optional.
regexp.append(')')
.append(((optionsAdded > 0) ? "?" : ""))
.append("(.*)");
Pattern pattern = Pattern.compile(regexp.toString());
// Loop through all the command line arguments.
for (int i = 0; i < args.length; i++)
{
// Check if the next command line argument begins with a '-' character and is therefore the start of
// an option.
if (args[i].startsWith("-"))
{
// Extract the value of the option without the leading '-'.
String arg = args[i].substring(1);
// Match up to the longest matching option.
optionMatcher = pattern.matcher(arg);
optionMatcher.matches();
String matchedOption = optionMatcher.group(1);
// Match any argument directly appended onto the longest matching option.
String matchedArg = optionMatcher.group(2);
// Check that a known option was matched.
if ((matchedOption != null) && !"".equals(matchedOption))
{
// Get the command line option information for the matched option.
CommandLineOption optionInfo = optionMap.get(matchedOption);
// Check if this option is expecting arguments.
if (optionInfo.expectsArgs)
{
// The option is expecting arguments so swallow the next command line argument as an
// argument to this option.
expectingArgs = true;
optionExpectingArgs = matchedOption;
}
// Check if the option was matched on its own and is a flag in which case set that flag.
if ("".equals(matchedArg) && !optionInfo.expectsArgs)
{
options.put(matchedOption, "true");
}
// The option was matched as a substring with its argument appended to it or is a flag that is
// condensed together with other flags.
else if (!"".equals(matchedArg))
{
// Check if the option is a flag and therefore is allowed to be condensed together
// with other flags.
if (!optionInfo.expectsArgs)
{
// Set the first matched flag.
options.put(matchedOption, "true");
// Repeat the longest matching process on the remainder but ensure that the remainder
// consists only of flags as only flags may be condensed together in this fashion.
do
{
// Match the remainder against the options.
optionMatcher = pattern.matcher(matchedArg);
optionMatcher.matches();
matchedOption = optionMatcher.group(1);
matchedArg = optionMatcher.group(2);
// Check that an option was matched.
if (matchedOption != null)
{
// Get the command line option information for the next matched option.
optionInfo = optionMap.get(matchedOption);
// Ensure that the next option is a flag or raise an error if not.
if (optionInfo.expectsArgs == true)
{
parsingErrors.add("Option " + matchedOption + " cannot be combined with flags.\n");
}
options.put(matchedOption, "true");
}
// The remainder could not be matched against a flag it is either an unknown flag
// or an illegal argument to a flag.
else
{
parsingErrors.add("Illegal argument to a flag in the option " + arg + "\n");
break;
}
}
// Continue until the remainder of the argument has all been matched with flags.
while (!"".equals(matchedArg));
}
// The option is expecting an argument, so store the unmatched portion against it
// as its argument.
else
{
// Check the arguments format is correct against any specified format.
checkArgumentFormat(optionInfo, matchedArg);
// Store the argument against its option (regardless of its format).
options.put(matchedOption, matchedArg);
// The argument to this flag has already been supplied to it. Do not swallow the
// next command line argument as an argument to this flag.
expectingArgs = false;
}
}
}
else // No matching option was found.
{
// Add this to the list of parsing errors if errors on unkowns is being used.
if (errorsOnUnknowns)
{
parsingErrors.add("Option " + matchedOption + " is not a recognized option.\n");
}
}
}
// The command line argument did not being with a '-' so it is an argument to the previous flag or it
// is a free argument.
else
{
// Check if a previous flag is expecting to swallow this next argument as its argument.
if (expectingArgs)
{
// Get the option info for the option waiting for arguments.
CommandLineOption optionInfo = optionMap.get(optionExpectingArgs);
// Check the arguments format is correct against any specified format.
checkArgumentFormat(optionInfo, args[i]);
// Store the argument against its option (regardless of its format).
options.put(optionExpectingArgs, args[i]);
// Clear the expecting args flag now that the argument has been swallowed.
expectingArgs = false;
optionExpectingArgs = null;
}
// This command line option is not an argument to any option. Add it to the set of 'free' options.
else
{
// Get the option info for the free option, if there is any.
CommandLineOption optionInfo = optionMap.get(Integer.toString(free));
if (optionInfo != null)
{
// Check the arguments format is correct against any specified format.
checkArgumentFormat(optionInfo, args[i]);
}
// Add to the list of free options.
options.put(Integer.toString(free), args[i]);
// Move on to the next free argument.
free++;
}
}
}
// Scan through all the specified options to check that all mandatory options have been set and that all flags
// that were not set are set to false in the set of properties.
for (CommandLineOption optionInfo : optionMap.values())
{
// Check if this is a flag.
if (!optionInfo.expectsArgs)
{
// Check if the flag is not set in the properties and set it to false if so.
if (!options.containsKey(optionInfo.option))
{
options.put(optionInfo.option, "false");
}
}
// Check if this is a mandatory option and was not set.
else if (optionInfo.mandatory && !options.containsKey(optionInfo.option))
{
// Create an error for the missing option.
parsingErrors.add("Option -" + optionInfo.option + " is mandatory but not was not specified.\n");
}
}
// Check if there were any errors.
if (!parsingErrors.isEmpty())
{
// Throw an illegal argument exception to signify that there were parsing errors.
throw new IllegalArgumentException();
}
// Convert any name/value pairs in the free arguments into properties in the parsed options.
options = takeFreeArgsAsProperties(options, 1);
parsedProperties = options;
return options;
}
/**
* If a command line has been parsed, calling this method sets all of its parsed options into the specified properties.
* @param properties properties
*/
public void addCommandLineToProperties(Properties properties)
{
if (parsedProperties != null)
{
for (Object propKey : parsedProperties.keySet())
{
String name = (String) propKey;
String value = parsedProperties.getProperty(name);
properties.setProperty(name, value);
}
}
}
/**
* Resets this command line parser after it has been used to parse a command line. This method will only need
* to be called to use this parser a second time which is not likely seeing as a command line is usually only
* specified once. However, it is exposed as a public method for the rare case where this may be done.
*
* <p>Cleans the internal state of this parser, removing all stored errors and information about the options in
* force.
*/
public void reset()
{
parsingErrors = new ArrayList<String>();
parsedProperties = null;
}
/**
* Adds the option to list of available command line options.
*
* @param option The option to add as an available command line option.
* @param comment A comment for the option.
* @param argument The text that appears after the option in the usage string.
* @param mandatory When true, indicates that this option is mandatory.
* @param formatRegexp The format that the argument must take, defined as a regular expression.
*/
protected void addOption(String option, String comment, String argument, boolean mandatory, String formatRegexp)
{
// Check if usage text has been set in which case this option is expecting arguments.
boolean expectsArgs = ((argument == null) || argument.equals("")) ? false : true;
// Add the option to the map of command line options.
CommandLineOption opt = new CommandLineOption(option, expectsArgs, comment, argument, mandatory, formatRegexp);
optionMap.put(option, opt);
}
/**
* Converts the free arguments into property declarations. After parsing the command line the free arguments
* are numbered from 1, such that the parsed properties contain values for the keys "1", "2", ... This method
* converts any free arguments declared using the 'name=value' syntax into properties with key 'name', value
* 'value'.
*
* <p/>For example the comand line:
* <pre>
* ... debug=true
* </pre>
*
* <p/>After parsing has properties:
* <pre>[[1, debug=true]]</pre>
*
* <p/>After applying this method the properties are:
* <pre>[[1, debug=true], [debug, true]]</pre>
*
* @param properties The parsed command line properties.
* @param from The free argument index to convert to properties from.
*
* @return The parsed command line properties, with free argument name value pairs too.
*/
private Properties takeFreeArgsAsProperties(Properties properties, int from)
{
for (int i = from; true; i++)
{
String nextFreeArg = properties.getProperty(Integer.toString(i));
// Terminate the loop once all free arguments have been consumed.
if (nextFreeArg == null)
{
break;
}
// Split it on the =, strip any whitespace and set it as a system property.
String[] nameValuePair = nextFreeArg.split("=");
if (nameValuePair.length == 2)
{
properties.setProperty(nameValuePair[0], nameValuePair[1]);
}
}
return properties;
}
/**
* Checks the format of an argument to an option against its specified regular expression format if one has
* been set. Any errors are added to the list of parsing errors.
*
* @param optionInfo The command line option information for the option which is havings its argument checked.
* @param matchedArg The string argument to the option.
*/
private void checkArgumentFormat(CommandLineOption optionInfo, String matchedArg)
{
// Check if this option enforces a format for its argument.
if (optionInfo.argumentFormatRegexp != null)
{
Pattern pattern = Pattern.compile(optionInfo.argumentFormatRegexp);
Matcher argumentMatcher = pattern.matcher(matchedArg);
// Check if the argument does not meet its required format.
if (!argumentMatcher.matches())
{
// Create an error for this badly formed argument.
parsingErrors.add("The argument to option -" + optionInfo.option + " does not meet its required format.\n");
}
}
}
/**
* Extracts all name=value pairs from the command line, sets them all as system properties and also returns
* a map of properties containing them.
*
* @param args The command line.
* @param commandLine The command line parser.
* @param properties The properties object to inject all parsed properties into (optional may be <tt>null</tt>).
*
* @return A set of properties containing all name=value pairs from the command line.
*/
public static Properties processCommandLine(String[] args, CommandLineParser commandLine, Properties properties)
{
// Capture the command line arguments or display errors and correct usage and then exit.
Properties options = null;
try
{
options = commandLine.parseCommandLine(args);
// Add all the trailing command line options (name=value pairs) to system properties. They may be picked up
// from there.
commandLine.addCommandLineToProperties(properties);
}
catch (IllegalArgumentException e)
{
System.out.println(commandLine.getErrors());
System.out.println(commandLine.getUsage());
System.exit(1);
}
return options;
}
/**
* Holds information about a command line options. This includes what its name is, whether or not it is a flag,
* whether or not it is mandatory, what its user comment is, what its argument reminder text is and what its
* regular expression format is.
*/
protected static class CommandLineOption
{
/** Holds the text for the flag to match this argument with. */
private String option = null;
/** Holds a string describing how to use this command line argument. */
private String argument = null;
/** Flag that determines whether or not this command line argument can take arguments. */
private boolean expectsArgs = false;
/** Holds a short comment describing what this command line argument is for. */
private String comment = null;
/** Flag that determines whether or not this is an mandatory command line argument. */
private boolean mandatory = false;
/** A regular expression describing what format the argument to this option muist have. */
private String argumentFormatRegexp = null;
/**
* Create a command line option object that holds specific information about a command line option.
*
* @param option The text that matches the option.
* @param expectsArgs Whether or not the option expects arguments. It is a flag if this is false.
* @param comment A comment explaining how to use this option.
* @param argument A short reminder of the format of the argument to this option/
* @param mandatory Set to true if this option is mandatory.
* @param formatRegexp The regular expression that the argument to this option must meet to be valid.
*/
public CommandLineOption(String option, boolean expectsArgs, String comment, String argument, boolean mandatory,
String formatRegexp)
{
this.option = option;
this.expectsArgs = expectsArgs;
this.comment = comment;
this.argument = argument;
this.mandatory = mandatory;
this.argumentFormatRegexp = formatRegexp;
}
}
}