blob: 9f3fb996e7670191aab8a2a8c83efb821eb65a72 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.sdkmanager;
import com.android.sdklib.ISdkLog;
import java.util.HashMap;
import java.util.Map.Entry;
/**
* Parses the command-line and stores flags needed or requested.
* <p/>
* This is a base class. To be useful you want to:
* <ul>
* <li>override it.
* <li>pass an action array to the constructor.
* <li>define flags for your actions.
* </ul>
* <p/>
* To use, call {@link #parseArgs(String[])} and then
* call {@link #getValue(String, String, String)}.
*/
public class CommandLineProcessor {
/** Internal verb name for internally hidden flags. */
public final static String GLOBAL_FLAG_VERB = "@@internal@@";
/** String to use when the verb doesn't need any object. */
public final static String NO_VERB_OBJECT = "";
/** The global help flag. */
public static final String KEY_HELP = "help";
/** The global verbose flag. */
public static final String KEY_VERBOSE = "verbose";
/** The global silent flag. */
public static final String KEY_SILENT = "silent";
/** Verb requested by the user. Null if none specified, which will be an error. */
private String mVerbRequested;
/** Direct object requested by the user. Can be null. */
private String mDirectObjectRequested;
/**
* Action definitions.
* <p/>
* Each entry is a string array with:
* <ul>
* <li> the verb.
* <li> a direct object (use #NO_VERB_OBJECT if there's no object).
* <li> a description.
* <li> an alternate form for the object (e.g. plural).
* </ul>
*/
private final String[][] mActions;
private static final int ACTION_VERB_INDEX = 0;
private static final int ACTION_OBJECT_INDEX = 1;
private static final int ACTION_DESC_INDEX = 2;
private static final int ACTION_ALT_OBJECT_INDEX = 3;
/**
* The map of all defined arguments.
* <p/>
* The key is a string "verb/directObject/longName".
*/
private final HashMap<String, Arg> mArguments = new HashMap<String, Arg>();
/** Logger */
private final ISdkLog mLog;
public CommandLineProcessor(ISdkLog logger, String[][] actions) {
mLog = logger;
mActions = actions;
define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "v", KEY_VERBOSE,
"Verbose mode: errors, warnings and informational messages are printed.",
false);
define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "s", KEY_SILENT,
"Silent mode: only errors are printed out.",
false);
define(MODE.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "h", KEY_HELP,
"This help.",
false);
}
//------------------
// Helpers to get flags values
/** Helper that returns true if --verbose was requested. */
public boolean isVerbose() {
return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_VERBOSE)).booleanValue();
}
/** Helper that returns true if --silent was requested. */
public boolean isSilent() {
return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_SILENT)).booleanValue();
}
/** Helper that returns true if --help was requested. */
public boolean isHelpRequested() {
return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_HELP)).booleanValue();
}
/** Returns the verb name from the command-line. Can be null. */
public String getVerb() {
return mVerbRequested;
}
/** Returns the direct object name from the command-line. Can be null. */
public String getDirectObject() {
return mDirectObjectRequested;
}
//------------------
/**
* Raw access to parsed parameter values.
* <p/>
* The default is to scan all parameters. Parameters that have been explicitly set on the
* command line are returned first. Otherwise one with a non-null value is returned.
* <p/>
* Both a verb and a direct object filter can be specified. When they are non-null they limit
* the scope of the search.
* <p/>
* If nothing has been found, return the last default value seen matching the filter.
*
* @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. If null, all possible
* verbs that match the direct object condition will be examined and the first
* value set will be used.
* @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. If null,
* all possible direct objects that match the verb condition will be examined and
* the first value set will be used.
* @param longFlagName The long flag name for the given action. Mandatory. Cannot be null.
* @return The current value object stored in the parameter, which depends on the argument mode.
*/
public Object getValue(String verb, String directObject, String longFlagName) {
if (verb != null && directObject != null) {
String key = verb + "/" + directObject + "/" + longFlagName;
Arg arg = mArguments.get(key);
return arg.getCurrentValue();
}
Object lastDefault = null;
for (Arg arg : mArguments.values()) {
if (arg.getLongArg().equals(longFlagName)) {
if (verb == null || arg.getVerb().equals(verb)) {
if (directObject == null || arg.getDirectObject().equals(directObject)) {
if (arg.isInCommandLine()) {
return arg.getCurrentValue();
}
if (arg.getCurrentValue() != null) {
lastDefault = arg.getCurrentValue();
}
}
}
}
}
return lastDefault;
}
/**
* Internal setter for raw parameter value.
* @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}.
* @param directObject The direct object name, including {@link #NO_VERB_OBJECT}.
* @param longFlagName The long flag name for the given action.
* @param value The new current value object stored in the parameter, which depends on the
* argument mode.
*/
protected void setValue(String verb, String directObject, String longFlagName, Object value) {
String key = verb + "/" + directObject + "/" + longFlagName;
Arg arg = mArguments.get(key);
arg.setCurrentValue(value);
}
/**
* Parses the command-line arguments.
* <p/>
* This method will exit and not return if a parsing error arise.
*
* @param args The arguments typically received by a main method.
*/
public void parseArgs(String[] args) {
String needsHelp = null;
String verb = null;
String directObject = null;
try {
int n = args.length;
for (int i = 0; i < n; i++) {
Arg arg = null;
String a = args[i];
if (a.startsWith("--")) {
arg = findLongArg(verb, directObject, a.substring(2));
} else if (a.startsWith("-")) {
arg = findShortArg(verb, directObject, a.substring(1));
}
// No matching argument name found
if (arg == null) {
// Does it looks like a dashed parameter?
if (a.startsWith("-")) {
if (verb == null || directObject == null) {
// It looks like a dashed parameter and we don't have a a verb/object
// set yet, the parameter was just given too early.
needsHelp = String.format(
"Flag '%1$s' is not a valid global flag. Did you mean to specify it after the verb/object name?",
a);
return;
} else {
// It looks like a dashed parameter and but it is unknown by this
// verb-object combination
needsHelp = String.format(
"Flag '%1$s' is not valid for '%2$s %3$s'.",
a, verb, directObject);
return;
}
}
if (verb == null) {
// Fill verb first. Find it.
for (String[] actionDesc : mActions) {
if (actionDesc[ACTION_VERB_INDEX].equals(a)) {
verb = a;
break;
}
}
// Error if it was not a valid verb
if (verb == null) {
needsHelp = String.format(
"Expected verb after global parameters but found '%1$s' instead.",
a);
return;
}
} else if (directObject == null) {
// Then fill the direct object. Find it.
for (String[] actionDesc : mActions) {
if (actionDesc[ACTION_VERB_INDEX].equals(verb)) {
if (actionDesc[ACTION_OBJECT_INDEX].equals(a)) {
directObject = a;
break;
} else if (actionDesc.length > ACTION_ALT_OBJECT_INDEX &&
actionDesc[ACTION_ALT_OBJECT_INDEX].equals(a)) {
// if the alternate form exist and is used, we internally
// only memorize the default direct object form.
directObject = actionDesc[ACTION_OBJECT_INDEX];
break;
}
}
}
// Error if it was not a valid object for that verb
if (directObject == null) {
needsHelp = String.format(
"Expected verb after global parameters but found '%1$s' instead.",
a);
return;
}
}
} else if (arg != null) {
// This argument was present on the command line
arg.setInCommandLine(true);
// Process keyword
String error = null;
if (arg.getMode().needsExtra()) {
if (++i >= n) {
needsHelp = String.format("Missing argument for flag %1$s.", a);
return;
}
error = arg.getMode().process(arg, args[i]);
} else {
error = arg.getMode().process(arg, null);
// If we just toggled help, we want to exit now without printing any error.
// We do this test here only when a Boolean flag is toggled since booleans
// are the only flags that don't take parameters and help is a boolean.
if (isHelpRequested()) {
printHelpAndExit(null);
// The call above should terminate however in unit tests we override
// it so we still need to return here.
return;
}
}
if (error != null) {
needsHelp = String.format("Invalid usage for flag %1$s: %2$s.", a, error);
return;
}
}
}
if (needsHelp == null) {
if (verb == null) {
needsHelp = "Missing verb name.";
} else {
if (directObject == null) {
// Make sure this verb has an optional direct object
for (String[] actionDesc : mActions) {
if (actionDesc[ACTION_VERB_INDEX].equals(verb) &&
actionDesc[ACTION_OBJECT_INDEX].equals(NO_VERB_OBJECT)) {
directObject = NO_VERB_OBJECT;
break;
}
}
if (directObject == null) {
needsHelp = String.format("Missing object name for verb '%1$s'.", verb);
return;
}
}
// Validate that all mandatory arguments are non-null for this action
String missing = null;
boolean plural = false;
for (Entry<String, Arg> entry : mArguments.entrySet()) {
Arg arg = entry.getValue();
if (arg.getVerb().equals(verb) &&
arg.getDirectObject().equals(directObject)) {
if (arg.isMandatory() && arg.getCurrentValue() == null) {
if (missing == null) {
missing = "--" + arg.getLongArg();
} else {
missing += ", --" + arg.getLongArg();
plural = true;
}
}
}
}
if (missing != null) {
needsHelp = String.format(
"The %1$s %2$s must be defined for action '%3$s %4$s'",
plural ? "parameters" : "parameter",
missing,
verb,
directObject);
}
mVerbRequested = verb;
mDirectObjectRequested = directObject;
}
}
} finally {
if (needsHelp != null) {
printHelpAndExitForAction(verb, directObject, needsHelp);
}
}
}
/**
* Finds an {@link Arg} given an action name and a long flag name.
* @return The {@link Arg} found or null.
*/
protected Arg findLongArg(String verb, String directObject, String longName) {
if (verb == null) {
verb = GLOBAL_FLAG_VERB;
}
if (directObject == null) {
directObject = NO_VERB_OBJECT;
}
String key = verb + "/" + directObject + "/" + longName;
return mArguments.get(key);
}
/**
* Finds an {@link Arg} given an action name and a short flag name.
* @return The {@link Arg} found or null.
*/
protected Arg findShortArg(String verb, String directObject, String shortName) {
if (verb == null) {
verb = GLOBAL_FLAG_VERB;
}
if (directObject == null) {
directObject = NO_VERB_OBJECT;
}
for (Entry<String, Arg> entry : mArguments.entrySet()) {
Arg arg = entry.getValue();
if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) {
if (shortName.equals(arg.getShortArg())) {
return arg;
}
}
}
return null;
}
/**
* Prints the help/usage and exits.
*
* @param errorFormat Optional error message to print prior to usage using String.format
* @param args Arguments for String.format
*/
public void printHelpAndExit(String errorFormat, Object... args) {
printHelpAndExitForAction(null /*verb*/, null /*directObject*/, errorFormat, args);
}
/**
* Prints the help/usage and exits.
*
* @param verb If null, displays help for all verbs. If not null, display help only
* for that specific verb. In all cases also displays general usage and action list.
* @param directObject If null, displays help for all verb objects.
* If not null, displays help only for that specific action
* In all cases also display general usage and action list.
* @param errorFormat Optional error message to print prior to usage using String.format
* @param args Arguments for String.format
*/
public void printHelpAndExitForAction(String verb, String directObject,
String errorFormat, Object... args) {
if (errorFormat != null) {
stderr(errorFormat, args);
}
/*
* usage should fit in 80 columns
* 12345678901234567890123456789012345678901234567890123456789012345678901234567890
*/
stdout("\n" +
"Usage:\n" +
" android [global options] action [action options]\n" +
"\n" +
"Global options:");
listOptions(GLOBAL_FLAG_VERB, NO_VERB_OBJECT);
if (verb == null || directObject == null) {
stdout("\nValid actions are composed of a verb and an optional direct object:");
for (String[] action : mActions) {
stdout("- %1$6s %2$-7s: %3$s",
action[ACTION_VERB_INDEX],
action[ACTION_OBJECT_INDEX],
action[ACTION_DESC_INDEX]);
}
}
for (String[] action : mActions) {
if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) {
if (directObject == null || directObject.equals(action[ACTION_OBJECT_INDEX])) {
stdout("\nAction \"%1$s %2$s\":",
action[ACTION_VERB_INDEX],
action[ACTION_OBJECT_INDEX]);
stdout(" %1$s", action[ACTION_DESC_INDEX]);
stdout("Options:");
listOptions(action[ACTION_VERB_INDEX], action[ACTION_OBJECT_INDEX]);
}
}
}
exit();
}
/**
* Internal helper to print all the option flags for a given action name.
*/
protected void listOptions(String verb, String directObject) {
int numOptions = 0;
for (Entry<String, Arg> entry : mArguments.entrySet()) {
Arg arg = entry.getValue();
if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) {
String value = "";
if (arg.getDefaultValue() instanceof String[]) {
for (String v : (String[]) arg.getDefaultValue()) {
if (value.length() > 0) {
value += ", ";
}
value += v;
}
} else if (arg.getDefaultValue() != null) {
value = arg.getDefaultValue().toString();
}
if (value.length() > 0) {
value = " (" + value + ")";
}
String required = arg.isMandatory() ? " [required]" : "";
stdout(" -%1$s %2$-10s %3$s%4$s%5$s",
arg.getShortArg(),
"--" + arg.getLongArg(),
arg.getDescription(),
value,
required);
numOptions++;
}
}
if (numOptions == 0) {
stdout(" No options");
}
}
//----
/**
* The mode of an argument specifies the type of variable it represents,
* whether an extra parameter is required after the flag and how to parse it.
*/
static enum MODE {
/** Argument value is a Boolean. Default value is a Boolean. */
BOOLEAN {
@Override
public boolean needsExtra() {
return false;
}
@Override
public String process(Arg arg, String extra) {
// Toggle the current value
arg.setCurrentValue(! ((Boolean) arg.getCurrentValue()).booleanValue());
return null;
}
},
/** Argument value is an Integer. Default value is an Integer. */
INTEGER {
@Override
public boolean needsExtra() {
return true;
}
@Override
public String process(Arg arg, String extra) {
try {
arg.setCurrentValue(Integer.parseInt(extra));
return null;
} catch (NumberFormatException e) {
return String.format("Failed to parse '%1$s' as an integer: %2%s",
extra, e.getMessage());
}
}
},
/** Argument value is a String. Default value is a String[]. */
ENUM {
@Override
public boolean needsExtra() {
return true;
}
@Override
public String process(Arg arg, String extra) {
StringBuilder desc = new StringBuilder();
String[] values = (String[]) arg.getDefaultValue();
for (String value : values) {
if (value.equals(extra)) {
arg.setCurrentValue(extra);
return null;
}
if (desc.length() != 0) {
desc.append(", ");
}
desc.append(value);
}
return String.format("'%1$s' is not one of %2$s", extra, desc.toString());
}
},
/** Argument value is a String. Default value is a null. */
STRING {
@Override
public boolean needsExtra() {
return true;
}
@Override
public String process(Arg arg, String extra) {
arg.setCurrentValue(extra);
return null;
}
};
/**
* Returns true if this mode requires an extra parameter.
*/
public abstract boolean needsExtra();
/**
* Processes the flag for this argument.
*
* @param arg The argument being processed.
* @param extra The extra parameter. Null if {@link #needsExtra()} returned false.
* @return An error string or null if there's no error.
*/
public abstract String process(Arg arg, String extra);
}
/**
* An argument accepted by the command-line, also called "a flag".
* Arguments must have a short version (one letter), a long version name and a description.
* They can have a default value, or it can be null.
* Depending on the {@link MODE}, the default value can be a Boolean, an Integer, a String
* or a String array (in which case the first item is the current by default.)
*/
static class Arg {
/** Verb for that argument. Never null. */
private final String mVerb;
/** Direct Object for that argument. Never null, but can be empty string. */
private final String mDirectObject;
/** The 1-letter short name of the argument, e.g. -v. */
private final String mShortName;
/** The long name of the argument, e.g. --verbose. */
private final String mLongName;
/** A description. Never null. */
private final String mDescription;
/** A default value. Can be null. */
private final Object mDefaultValue;
/** The argument mode (type + process method). Never null. */
private final MODE mMode;
/** True if this argument is mandatory for this verb/directobject. */
private final boolean mMandatory;
/** Current value. Initially set to the default value. */
private Object mCurrentValue;
/** True if the argument has been used on the command line. */
private boolean mInCommandLine;
/**
* Creates a new argument flag description.
*
* @param mode The {@link MODE} for the argument.
* @param mandatory True if this argument is mandatory for this action.
* @param directObject The action name. Can be #NO_VERB_OBJECT or #INTERNAL_FLAG.
* @param shortName The one-letter short argument name. Cannot be empty nor null.
* @param longName The long argument name. Cannot be empty nor null.
* @param description The description. Cannot be null.
* @param defaultValue The default value (or values), which depends on the selected {@link MODE}.
*/
public Arg(MODE mode,
boolean mandatory,
String verb,
String directObject,
String shortName,
String longName,
String description,
Object defaultValue) {
mMode = mode;
mMandatory = mandatory;
mVerb = verb;
mDirectObject = directObject;
mShortName = shortName;
mLongName = longName;
mDescription = description;
mDefaultValue = defaultValue;
mInCommandLine = false;
if (defaultValue instanceof String[]) {
mCurrentValue = ((String[])defaultValue)[0];
} else {
mCurrentValue = mDefaultValue;
}
}
/** Return true if this argument is mandatory for this verb/directobject. */
public boolean isMandatory() {
return mMandatory;
}
/** Returns the 1-letter short name of the argument, e.g. -v. */
public String getShortArg() {
return mShortName;
}
/** Returns the long name of the argument, e.g. --verbose. */
public String getLongArg() {
return mLongName;
}
/** Returns the description. Never null. */
public String getDescription() {
return mDescription;
}
/** Returns the verb for that argument. Never null. */
public String getVerb() {
return mVerb;
}
/** Returns the direct Object for that argument. Never null, but can be empty string. */
public String getDirectObject() {
return mDirectObject;
}
/** Returns the default value. Can be null. */
public Object getDefaultValue() {
return mDefaultValue;
}
/** Returns the current value. Initially set to the default value. Can be null. */
public Object getCurrentValue() {
return mCurrentValue;
}
/** Sets the current value. Can be null. */
public void setCurrentValue(Object currentValue) {
mCurrentValue = currentValue;
}
/** Returns the argument mode (type + process method). Never null. */
public MODE getMode() {
return mMode;
}
/** Returns true if the argument has been used on the command line. */
public boolean isInCommandLine() {
return mInCommandLine;
}
/** Sets if the argument has been used on the command line. */
public void setInCommandLine(boolean inCommandLine) {
mInCommandLine = inCommandLine;
}
}
/**
* Internal helper to define a new argument for a give action.
*
* @param mode The {@link MODE} for the argument.
* @param verb The verb name. Can be #INTERNAL_VERB.
* @param directObject The action name. Can be #NO_VERB_OBJECT or #INTERNAL_FLAG.
* @param shortName The one-letter short argument name. Cannot be empty nor null.
* @param longName The long argument name. Cannot be empty nor null.
* @param description The description. Cannot be null.
* @param defaultValue The default value (or values), which depends on the selected {@link MODE}.
*/
protected void define(MODE mode,
boolean mandatory,
String verb,
String directObject,
String shortName, String longName,
String description, Object defaultValue) {
assert(mandatory || mode == MODE.BOOLEAN); // a boolean mode cannot be mandatory
if (directObject == null) {
directObject = NO_VERB_OBJECT;
}
String key = verb + "/" + directObject + "/" + longName;
mArguments.put(key, new Arg(mode, mandatory,
verb, directObject, shortName, longName, description, defaultValue));
}
/**
* Exits in case of error.
* This is protected so that it can be overridden in unit tests.
*/
protected void exit() {
System.exit(1);
}
/**
* Prints a line to stdout.
* This is protected so that it can be overridden in unit tests.
*
* @param format The string to be formatted. Cannot be null.
* @param args Format arguments.
*/
protected void stdout(String format, Object...args) {
mLog.printf(format + "\n", args);
}
/**
* Prints a line to stderr.
* This is protected so that it can be overridden in unit tests.
*
* @param format The string to be formatted. Cannot be null.
* @param args Format arguments.
*/
protected void stderr(String format, Object...args) {
mLog.error(null, format, args);
}
}