Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.sdklib.util;
     18 
     19 import com.android.sdklib.ISdkLog;
     20 
     21 import java.util.ArrayList;
     22 import java.util.HashMap;
     23 import java.util.List;
     24 import java.util.Map.Entry;
     25 
     26 /**
     27  * Parses the command-line and stores flags needed or requested.
     28  * <p/>
     29  * This is a base class. To be useful you want to:
     30  * <ul>
     31  * <li>override it.
     32  * <li>pass an action array to the constructor.
     33  * <li>define flags for your actions.
     34  * </ul>
     35  * <p/>
     36  * To use, call {@link #parseArgs(String[])} and then
     37  * call {@link #getValue(String, String, String)}.
     38  */
     39 public class CommandLineParser {
     40 
     41     /*
     42      * Steps needed to add a new action:
     43      * - Each action is defined as a "verb object" followed by parameters.
     44      * - Either reuse a VERB_ constant or define a new one.
     45      * - Either reuse an OBJECT_ constant or define a new one.
     46      * - Add a new entry to mAction with a one-line help summary.
     47      * - In the constructor, add a define() call for each parameter (either mandatory
     48      *   or optional) for the given action.
     49      */
     50 
     51     /** Internal verb name for internally hidden flags. */
     52     public final static String GLOBAL_FLAG_VERB = "@@internal@@";   //$NON-NLS-1$
     53 
     54     /** String to use when the verb doesn't need any object. */
     55     public final static String NO_VERB_OBJECT = "";                 //$NON-NLS-1$
     56 
     57     /** The global help flag. */
     58     public static final String KEY_HELP = "help";
     59     /** The global verbose flag. */
     60     public static final String KEY_VERBOSE = "verbose";
     61     /** The global silent flag. */
     62     public static final String KEY_SILENT = "silent";
     63 
     64     /** Verb requested by the user. Null if none specified, which will be an error. */
     65     private String mVerbRequested;
     66     /** Direct object requested by the user. Can be null. */
     67     private String mDirectObjectRequested;
     68 
     69     /**
     70      * Action definitions.
     71      * <p/>
     72      * This list serves two purposes: first it is used to know which verb/object
     73      * actions are acceptable on the command-line; second it provides a summary
     74      * for each action that is printed in the help.
     75      * <p/>
     76      * Each entry is a string array with:
     77      * <ul>
     78      * <li> the verb.
     79      * <li> a direct object (use {@link #NO_VERB_OBJECT} if there's no object).
     80      * <li> a description.
     81      * <li> an alternate form for the object (e.g. plural).
     82      * </ul>
     83      */
     84     private final String[][] mActions;
     85 
     86     private static final int ACTION_VERB_INDEX = 0;
     87     private static final int ACTION_OBJECT_INDEX = 1;
     88     private static final int ACTION_DESC_INDEX = 2;
     89     private static final int ACTION_ALT_OBJECT_INDEX = 3;
     90 
     91     /**
     92      * The map of all defined arguments.
     93      * <p/>
     94      * The key is a string "verb/directObject/longName".
     95      */
     96     private final HashMap<String, Arg> mArguments = new HashMap<String, Arg>();
     97     /** Logger */
     98     private final ISdkLog mLog;
     99 
    100     /**
    101      * Constructs a new command-line processor.
    102      *
    103      * @param logger An SDK logger object. Must not be null.
    104      * @param actions The list of actions recognized on the command-line.
    105      *                See the javadoc of {@link #mActions} for more details.
    106      *
    107      * @see #mActions
    108      */
    109     public CommandLineParser(ISdkLog logger, String[][] actions) {
    110         mLog = logger;
    111         mActions = actions;
    112 
    113         /*
    114          * usage should fit in 80 columns, including the space to print the options:
    115          * "  -v --verbose  7890123456789012345678901234567890123456789012345678901234567890"
    116          */
    117 
    118         define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "v", KEY_VERBOSE,
    119                            "Verbose mode, shows errors, warnings and all messages.",
    120                            false);
    121         define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "s", KEY_SILENT,
    122                            "Silent mode, shows errors only.",
    123                            false);
    124         define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "h", KEY_HELP,
    125                            "Help on a specific command.",
    126                            false);
    127     }
    128 
    129     /**
    130      * Indicates if this command-line can work when no verb is specified.
    131      * The default is false, which generates an error when no verb/object is specified.
    132      * Derived implementations can set this to true if they can deal with a lack
    133      * of verb/action.
    134      */
    135     public boolean acceptLackOfVerb() {
    136         return false;
    137     }
    138 
    139 
    140     //------------------
    141     // Helpers to get flags values
    142 
    143     /** Helper that returns true if --verbose was requested. */
    144     public boolean isVerbose() {
    145         return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_VERBOSE)).booleanValue();
    146     }
    147 
    148     /** Helper that returns true if --silent was requested. */
    149     public boolean isSilent() {
    150         return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_SILENT)).booleanValue();
    151     }
    152 
    153     /** Helper that returns true if --help was requested. */
    154     public boolean isHelpRequested() {
    155         return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_HELP)).booleanValue();
    156     }
    157 
    158     /** Returns the verb name from the command-line. Can be null. */
    159     public String getVerb() {
    160         return mVerbRequested;
    161     }
    162 
    163     /** Returns the direct object name from the command-line. Can be null. */
    164     public String getDirectObject() {
    165         return mDirectObjectRequested;
    166     }
    167 
    168     //------------------
    169 
    170     /**
    171      * Raw access to parsed parameter values.
    172      * <p/>
    173      * The default is to scan all parameters. Parameters that have been explicitly set on the
    174      * command line are returned first. Otherwise one with a non-null value is returned.
    175      * <p/>
    176      * Both a verb and a direct object filter can be specified. When they are non-null they limit
    177      * the scope of the search.
    178      * <p/>
    179      * If nothing has been found, return the last default value seen matching the filter.
    180      *
    181      * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. If null, all possible
    182      *             verbs that match the direct object condition will be examined and the first
    183      *             value set will be used.
    184      * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. If null,
    185      *             all possible direct objects that match the verb condition will be examined and
    186      *             the first value set will be used.
    187      * @param longFlagName The long flag name for the given action. Mandatory. Cannot be null.
    188      * @return The current value object stored in the parameter, which depends on the argument mode.
    189      */
    190     public Object getValue(String verb, String directObject, String longFlagName) {
    191 
    192         if (verb != null && directObject != null) {
    193             String key = verb + '/' + directObject + '/' + longFlagName;
    194             Arg arg = mArguments.get(key);
    195             return arg.getCurrentValue();
    196         }
    197 
    198         Object lastDefault = null;
    199         for (Arg arg : mArguments.values()) {
    200             if (arg.getLongArg().equals(longFlagName)) {
    201                 if (verb == null || arg.getVerb().equals(verb)) {
    202                     if (directObject == null || arg.getDirectObject().equals(directObject)) {
    203                         if (arg.isInCommandLine()) {
    204                             return arg.getCurrentValue();
    205                         }
    206                         if (arg.getCurrentValue() != null) {
    207                             lastDefault = arg.getCurrentValue();
    208                         }
    209                     }
    210                 }
    211             }
    212         }
    213 
    214         return lastDefault;
    215     }
    216 
    217     /**
    218      * Internal setter for raw parameter value.
    219      * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}.
    220      * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}.
    221      * @param longFlagName The long flag name for the given action.
    222      * @param value The new current value object stored in the parameter, which depends on the
    223      *              argument mode.
    224      */
    225     protected void setValue(String verb, String directObject, String longFlagName, Object value) {
    226         String key = verb + '/' + directObject + '/' + longFlagName;
    227         Arg arg = mArguments.get(key);
    228         arg.setCurrentValue(value);
    229     }
    230 
    231     /**
    232      * Parses the command-line arguments.
    233      * <p/>
    234      * This method will exit and not return if a parsing error arise.
    235      *
    236      * @param args The arguments typically received by a main method.
    237      */
    238     public void parseArgs(String[] args) {
    239         String errorMsg = null;
    240         String verb = null;
    241         String directObject = null;
    242 
    243         try {
    244             int n = args.length;
    245             for (int i = 0; i < n; i++) {
    246                 Arg arg = null;
    247                 String a = args[i];
    248                 if (a.startsWith("--")) {                                       //$NON-NLS-1$
    249                     arg = findLongArg(verb, directObject, a.substring(2));
    250                 } else if (a.startsWith("-")) {                                 //$NON-NLS-1$
    251                     arg = findShortArg(verb, directObject, a.substring(1));
    252                 }
    253 
    254                 // No matching argument name found
    255                 if (arg == null) {
    256                     // Does it looks like a dashed parameter?
    257                     if (a.startsWith("-")) {                                    //$NON-NLS-1$
    258                         if (verb == null || directObject == null) {
    259                             // It looks like a dashed parameter and we don't have a a verb/object
    260                             // set yet, the parameter was just given too early.
    261 
    262                             errorMsg = String.format(
    263                                 "Flag '%1$s' is not a valid global flag. Did you mean to specify it after the verb/object name?",
    264                                 a);
    265                             return;
    266                         } else {
    267                             // It looks like a dashed parameter but it is unknown by this
    268                             // verb-object combination
    269 
    270                             errorMsg = String.format(
    271                                     "Flag '%1$s' is not valid for '%2$s %3$s'.",
    272                                     a, verb, directObject);
    273                             return;
    274                         }
    275                     }
    276 
    277                     if (verb == null) {
    278                         // Fill verb first. Find it.
    279                         for (String[] actionDesc : mActions) {
    280                             if (actionDesc[ACTION_VERB_INDEX].equals(a)) {
    281                                 verb = a;
    282                                 break;
    283                             }
    284                         }
    285 
    286                         // Error if it was not a valid verb
    287                         if (verb == null) {
    288                             errorMsg = String.format(
    289                                 "Expected verb after global parameters but found '%1$s' instead.",
    290                                 a);
    291                             return;
    292                         }
    293 
    294                     } else if (directObject == null) {
    295                         // Then fill the direct object. Find it.
    296                         for (String[] actionDesc : mActions) {
    297                             if (actionDesc[ACTION_VERB_INDEX].equals(verb)) {
    298                                 if (actionDesc[ACTION_OBJECT_INDEX].equals(a)) {
    299                                     directObject = a;
    300                                     break;
    301                                 } else if (actionDesc.length > ACTION_ALT_OBJECT_INDEX &&
    302                                         actionDesc[ACTION_ALT_OBJECT_INDEX].equals(a)) {
    303                                     // if the alternate form exist and is used, we internally
    304                                     // only memorize the default direct object form.
    305                                     directObject = actionDesc[ACTION_OBJECT_INDEX];
    306                                     break;
    307                                 }
    308                             }
    309                         }
    310 
    311                         // Error if it was not a valid object for that verb
    312                         if (directObject == null) {
    313                             errorMsg = String.format(
    314                                 "Expected verb after global parameters but found '%1$s' instead.",
    315                                 a);
    316                             return;
    317 
    318                         }
    319                     } else {
    320                         // The argument is not a dashed parameter and we already
    321                         // have a verb/object. Must be some extra unknown argument.
    322                         errorMsg = String.format(
    323                                 "Argument '%1$s' is not recognized.",
    324                                 a);
    325                     }
    326                 } else if (arg != null) {
    327                     // This argument was present on the command line
    328                     arg.setInCommandLine(true);
    329 
    330                     // Process keyword
    331                     Object error = null;
    332                     if (arg.getMode().needsExtra()) {
    333                         if (i+1 >= n) {
    334                             errorMsg = String.format("Missing argument for flag %1$s.", a);
    335                             return;
    336                         }
    337 
    338                         while (i+1 < n) {
    339                             String b = args[i+1];
    340 
    341                             if (arg.getMode() != Mode.STRING_ARRAY) {
    342                                 // We never accept something that looks like a valid argument
    343                                 // unless we see -- first
    344                                 Arg dummyArg = null;
    345                                 if (b.startsWith("--")) {                              //$NON-NLS-1$
    346                                     dummyArg = findLongArg(verb, directObject, b.substring(2));
    347                                 } else if (b.startsWith("-")) {                        //$NON-NLS-1$
    348                                     dummyArg = findShortArg(verb, directObject, b.substring(1));
    349                                 }
    350                                 if (dummyArg != null) {
    351                                     errorMsg = String.format(
    352                                             "Oops, it looks like you didn't provide an argument for '%1$s'.\n'%2$s' was found instead.",
    353                                             a, b);
    354                                     return;
    355                                 }
    356                             }
    357 
    358                             error = arg.getMode().process(arg, b);
    359                             if (error == Accept.CONTINUE) {
    360                                 i++;
    361                             } else if (error == Accept.ACCEPT_AND_STOP) {
    362                                 i++;
    363                                 break;
    364                             } else if (error == Accept.REJECT_AND_STOP) {
    365                                 break;
    366                             } else if (error instanceof String) {
    367                                 // We stop because of an error
    368                                 break;
    369                             }
    370                         }
    371                     } else {
    372                         error = arg.getMode().process(arg, null);
    373 
    374                         if (isHelpRequested()) {
    375                             // The --help flag was requested. We'll continue the usual processing
    376                             // so that we can find the optional verb/object words. Those will be
    377                             // used to print specific help.
    378                             // Setting a non-null error message triggers printing the help, however
    379                             // there is no specific error to print.
    380                             errorMsg = "";                                          //$NON-NLS-1$
    381                         }
    382                     }
    383 
    384                     if (error instanceof String) {
    385                         errorMsg = String.format("Invalid usage for flag %1$s: %2$s.", a, error);
    386                         return;
    387                     }
    388                 }
    389             }
    390 
    391             if (errorMsg == null) {
    392                 if (verb == null && !acceptLackOfVerb()) {
    393                     errorMsg = "Missing verb name.";
    394                 } else if (verb != null) {
    395                     if (directObject == null) {
    396                         // Make sure this verb has an optional direct object
    397                         for (String[] actionDesc : mActions) {
    398                             if (actionDesc[ACTION_VERB_INDEX].equals(verb) &&
    399                                     actionDesc[ACTION_OBJECT_INDEX].equals(NO_VERB_OBJECT)) {
    400                                 directObject = NO_VERB_OBJECT;
    401                                 break;
    402                             }
    403                         }
    404 
    405                         if (directObject == null) {
    406                             errorMsg = String.format("Missing object name for verb '%1$s'.", verb);
    407                             return;
    408                         }
    409                     }
    410 
    411                     // Validate that all mandatory arguments are non-null for this action
    412                     String missing = null;
    413                     boolean plural = false;
    414                     for (Entry<String, Arg> entry : mArguments.entrySet()) {
    415                         Arg arg = entry.getValue();
    416                         if (arg.getVerb().equals(verb) &&
    417                                 arg.getDirectObject().equals(directObject)) {
    418                             if (arg.isMandatory() && arg.getCurrentValue() == null) {
    419                                 if (missing == null) {
    420                                     missing = "--" + arg.getLongArg();              //$NON-NLS-1$
    421                                 } else {
    422                                     missing += ", --" + arg.getLongArg();           //$NON-NLS-1$
    423                                     plural = true;
    424                                 }
    425                             }
    426                         }
    427                     }
    428 
    429                     if (missing != null) {
    430                         errorMsg  = String.format(
    431                                 "The %1$s %2$s must be defined for action '%3$s %4$s'",
    432                                 plural ? "parameters" : "parameter",
    433                                 missing,
    434                                 verb,
    435                                 directObject);
    436                     }
    437 
    438                     mVerbRequested = verb;
    439                     mDirectObjectRequested = directObject;
    440                 }
    441             }
    442         } finally {
    443             if (errorMsg != null) {
    444                 printHelpAndExitForAction(verb, directObject, errorMsg);
    445             }
    446         }
    447     }
    448 
    449     /**
    450      * Finds an {@link Arg} given an action name and a long flag name.
    451      * @return The {@link Arg} found or null.
    452      */
    453     protected Arg findLongArg(String verb, String directObject, String longName) {
    454         if (verb == null) {
    455             verb = GLOBAL_FLAG_VERB;
    456         }
    457         if (directObject == null) {
    458             directObject = NO_VERB_OBJECT;
    459         }
    460         String key = verb + '/' + directObject + '/' + longName;                    //$NON-NLS-1$
    461         return mArguments.get(key);
    462     }
    463 
    464     /**
    465      * Finds an {@link Arg} given an action name and a short flag name.
    466      * @return The {@link Arg} found or null.
    467      */
    468     protected Arg findShortArg(String verb, String directObject, String shortName) {
    469         if (verb == null) {
    470             verb = GLOBAL_FLAG_VERB;
    471         }
    472         if (directObject == null) {
    473             directObject = NO_VERB_OBJECT;
    474         }
    475 
    476         for (Entry<String, Arg> entry : mArguments.entrySet()) {
    477             Arg arg = entry.getValue();
    478             if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) {
    479                 if (shortName.equals(arg.getShortArg())) {
    480                     return arg;
    481                 }
    482             }
    483         }
    484 
    485         return null;
    486     }
    487 
    488     /**
    489      * Prints the help/usage and exits.
    490      *
    491      * @param errorFormat Optional error message to print prior to usage using String.format
    492      * @param args Arguments for String.format
    493      */
    494     public void printHelpAndExit(String errorFormat, Object... args) {
    495         printHelpAndExitForAction(null /*verb*/, null /*directObject*/, errorFormat, args);
    496     }
    497 
    498     /**
    499      * Prints the help/usage and exits.
    500      *
    501      * @param verb If null, displays help for all verbs. If not null, display help only
    502      *          for that specific verb. In all cases also displays general usage and action list.
    503      * @param directObject If null, displays help for all verb objects.
    504      *          If not null, displays help only for that specific action
    505      *          In all cases also display general usage and action list.
    506      * @param errorFormat Optional error message to print prior to usage using String.format
    507      * @param args Arguments for String.format
    508      */
    509     public void printHelpAndExitForAction(String verb, String directObject,
    510             String errorFormat, Object... args) {
    511         if (errorFormat != null && errorFormat.length() > 0) {
    512             stderr(errorFormat, args);
    513         }
    514 
    515         /*
    516          * usage should fit in 80 columns
    517          *   12345678901234567890123456789012345678901234567890123456789012345678901234567890
    518          */
    519         stdout("\n" +
    520             "Usage:\n" +
    521             "  android [global options] %s [action options]\n" +
    522             "\n" +
    523             "Global options:",
    524             verb == null ? "action" :
    525                 verb + (directObject == null ? "" : " " + directObject));           //$NON-NLS-1$
    526         listOptions(GLOBAL_FLAG_VERB, NO_VERB_OBJECT);
    527 
    528         if (verb == null || directObject == null) {
    529             stdout("\nValid actions are composed of a verb and an optional direct object:");
    530             for (String[] action : mActions) {
    531                 if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) {
    532                     stdout("- %1$6s %2$-13s: %3$s",
    533                             action[ACTION_VERB_INDEX],
    534                             action[ACTION_OBJECT_INDEX],
    535                             action[ACTION_DESC_INDEX]);
    536                 }
    537             }
    538         }
    539 
    540         // Only print details if a verb/object is requested
    541         if (verb != null) {
    542             for (String[] action : mActions) {
    543                 if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) {
    544                     if (directObject == null || directObject.equals(action[ACTION_OBJECT_INDEX])) {
    545                         stdout("\nAction \"%1$s %2$s\":",
    546                                 action[ACTION_VERB_INDEX],
    547                                 action[ACTION_OBJECT_INDEX]);
    548                         stdout("  %1$s", action[ACTION_DESC_INDEX]);
    549                         stdout("Options:");
    550                         listOptions(action[ACTION_VERB_INDEX], action[ACTION_OBJECT_INDEX]);
    551                     }
    552                 }
    553             }
    554         }
    555 
    556         exit();
    557     }
    558 
    559     /**
    560      * Internal helper to print all the option flags for a given action name.
    561      */
    562     protected void listOptions(String verb, String directObject) {
    563         int numOptions = 0;
    564         int longArgLen = 8;
    565 
    566         for (Entry<String, Arg> entry : mArguments.entrySet()) {
    567             Arg arg = entry.getValue();
    568             if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) {
    569                 int n = arg.getLongArg().length();
    570                 if (n > longArgLen) {
    571                     longArgLen = n;
    572                 }
    573             }
    574         }
    575 
    576         for (Entry<String, Arg> entry : mArguments.entrySet()) {
    577             Arg arg = entry.getValue();
    578             if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) {
    579 
    580                 String value = "";                                              //$NON-NLS-1$
    581                 String required = "";                                           //$NON-NLS-1$
    582                 if (arg.isMandatory()) {
    583                     required = " [required]";
    584 
    585                 } else {
    586                     if (arg.getDefaultValue() instanceof String[]) {
    587                         for (String v : (String[]) arg.getDefaultValue()) {
    588                             if (value.length() > 0) {
    589                                 value += ", ";
    590                             }
    591                             value += v;
    592                         }
    593                     } else if (arg.getDefaultValue() != null) {
    594                         Object v = arg.getDefaultValue();
    595                         if (arg.getMode() != Mode.BOOLEAN || v.equals(Boolean.TRUE)) {
    596                             value = v.toString();
    597                         }
    598                     }
    599                     if (value.length() > 0) {
    600                         value = " [Default: " + value + "]";
    601                     }
    602                 }
    603 
    604                 // Java doesn't support * for printf variable width, so we'll insert the long arg
    605                 // width "manually" in the printf format string.
    606                 String longArgWidth = Integer.toString(longArgLen + 2);
    607 
    608                 // Print a line in the form " -1_letter_arg --long_arg description"
    609                 // where either the 1-letter arg or the long arg are optional.
    610                 String output = String.format(
    611                         "  %1$-2s %2$-" + longArgWidth + "s: %3$s%4$s%5$s", //$NON-NLS-1$ //$NON-NLS-2$
    612                         arg.getShortArg().length() > 0 ?
    613                                 "-" + arg.getShortArg() :                              //$NON-NLS-1$
    614                                 "",                                                    //$NON-NLS-1$
    615                         arg.getLongArg().length() > 0 ?
    616                                 "--" + arg.getLongArg() :                              //$NON-NLS-1$
    617                                 "",                                                    //$NON-NLS-1$
    618                         arg.getDescription(),
    619                         value,
    620                         required);
    621                 stdout(output);
    622                 numOptions++;
    623             }
    624         }
    625 
    626         if (numOptions == 0) {
    627             stdout("  No options");
    628         }
    629     }
    630 
    631     //----
    632 
    633     private static enum Accept {
    634         CONTINUE,
    635         ACCEPT_AND_STOP,
    636         REJECT_AND_STOP,
    637     }
    638 
    639     /**
    640      * The mode of an argument specifies the type of variable it represents,
    641      * whether an extra parameter is required after the flag and how to parse it.
    642      */
    643     public static enum Mode {
    644         /** Argument value is a Boolean. Default value is a Boolean. */
    645         BOOLEAN {
    646             @Override
    647             public boolean needsExtra() {
    648                 return false;
    649             }
    650             @Override
    651             public Object process(Arg arg, String extra) {
    652                 // Toggle the current value
    653                 arg.setCurrentValue(! ((Boolean) arg.getCurrentValue()).booleanValue());
    654                 return Accept.ACCEPT_AND_STOP;
    655             }
    656         },
    657 
    658         /** Argument value is an Integer. Default value is an Integer. */
    659         INTEGER {
    660             @Override
    661             public boolean needsExtra() {
    662                 return true;
    663             }
    664             @Override
    665             public Object process(Arg arg, String extra) {
    666                 try {
    667                     arg.setCurrentValue(Integer.parseInt(extra));
    668                     return null;
    669                 } catch (NumberFormatException e) {
    670                     return String.format("Failed to parse '%1$s' as an integer: %2$s", extra,
    671                             e.getMessage());
    672                 }
    673             }
    674         },
    675 
    676         /** Argument value is a String. Default value is a String[]. */
    677         ENUM {
    678             @Override
    679             public boolean needsExtra() {
    680                 return true;
    681             }
    682             @Override
    683             public Object process(Arg arg, String extra) {
    684                 StringBuilder desc = new StringBuilder();
    685                 String[] values = (String[]) arg.getDefaultValue();
    686                 for (String value : values) {
    687                     if (value.equals(extra)) {
    688                         arg.setCurrentValue(extra);
    689                         return Accept.ACCEPT_AND_STOP;
    690                     }
    691 
    692                     if (desc.length() != 0) {
    693                         desc.append(", ");
    694                     }
    695                     desc.append(value);
    696                 }
    697 
    698                 return String.format("'%1$s' is not one of %2$s", extra, desc.toString());
    699             }
    700         },
    701 
    702         /** Argument value is a String. Default value is a null. */
    703         STRING {
    704             @Override
    705             public boolean needsExtra() {
    706                 return true;
    707             }
    708             @Override
    709             public Object process(Arg arg, String extra) {
    710                 arg.setCurrentValue(extra);
    711                 return Accept.ACCEPT_AND_STOP;
    712             }
    713         },
    714 
    715         /** Argument value is a {@link List}&lt;String&gt;. Default value is an empty list. */
    716         STRING_ARRAY {
    717             @Override
    718             public boolean needsExtra() {
    719                 return true;
    720             }
    721             @Override
    722             public Object process(Arg arg, String extra) {
    723                 // For simplification, a string array doesn't accept something that
    724                 // starts with a dash unless a pure -- was seen before.
    725                 if (extra != null) {
    726                     Object v = arg.getCurrentValue();
    727                     if (v == null) {
    728                         ArrayList<String> a = new ArrayList<String>();
    729                         arg.setCurrentValue(a);
    730                         v = a;
    731                     }
    732                     if (v instanceof List<?>) {
    733                         @SuppressWarnings("unchecked") List<String> a = (List<String>) v;
    734 
    735                         if (extra.equals("--") ||
    736                                 !extra.startsWith("-") ||
    737                                 (extra.startsWith("-") && a.contains("--"))) {
    738                             a.add(extra);
    739                             return Accept.CONTINUE;
    740                         } else if (a.isEmpty()) {
    741                             return "No values provided";
    742                         }
    743                     }
    744                 }
    745                 return Accept.REJECT_AND_STOP;
    746             }
    747         };
    748 
    749         /**
    750          * Returns true if this mode requires an extra parameter.
    751          */
    752         public abstract boolean needsExtra();
    753 
    754         /**
    755          * Processes the flag for this argument.
    756          *
    757          * @param arg The argument being processed.
    758          * @param extra The extra parameter. Null if {@link #needsExtra()} returned false.
    759          * @return {@link Accept#CONTINUE} if this argument can use multiple values and
    760          *   wishes to receive more.
    761          *   Or {@link Accept#ACCEPT_AND_STOP} if this was the last value accepted by the argument.
    762          *   Or {@link Accept#REJECT_AND_STOP} if this was value was reject and the argument
    763          *   stops accepting new values with no error.
    764          *   Or a string in case of error.
    765          *   Never returns null.
    766          */
    767         public abstract Object process(Arg arg, String extra);
    768     }
    769 
    770     /**
    771      * An argument accepted by the command-line, also called "a flag".
    772      * Arguments must have a short version (one letter), a long version name and a description.
    773      * They can have a default value, or it can be null.
    774      * Depending on the {@link Mode}, the default value can be a Boolean, an Integer, a String
    775      * or a String array (in which case the first item is the current by default.)
    776      */
    777     static class Arg {
    778         /** Verb for that argument. Never null. */
    779         private final String mVerb;
    780         /** Direct Object for that argument. Never null, but can be empty string. */
    781         private final String mDirectObject;
    782         /** The 1-letter short name of the argument, e.g. -v. */
    783         private final String mShortName;
    784         /** The long name of the argument, e.g. --verbose. */
    785         private final String mLongName;
    786         /** A description. Never null. */
    787         private final String mDescription;
    788         /** A default value. Can be null. */
    789         private final Object mDefaultValue;
    790         /** The argument mode (type + process method). Never null. */
    791         private final Mode mMode;
    792         /** True if this argument is mandatory for this verb/directobject. */
    793         private final boolean mMandatory;
    794         /** Current value. Initially set to the default value. */
    795         private Object mCurrentValue;
    796         /** True if the argument has been used on the command line. */
    797         private boolean mInCommandLine;
    798 
    799         /**
    800          * Creates a new argument flag description.
    801          *
    802          * @param mode The {@link Mode} for the argument.
    803          * @param mandatory True if this argument is mandatory for this action.
    804          * @param directObject The action name. Can be #NO_VERB_OBJECT or #INTERNAL_FLAG.
    805          * @param shortName The one-letter short argument name. Can be empty but not null.
    806          * @param longName The long argument name. Can be empty but not null.
    807          * @param description The description. Cannot be null.
    808          * @param defaultValue The default value (or values), which depends on the selected {@link Mode}.
    809          */
    810         public Arg(Mode mode,
    811                    boolean mandatory,
    812                    String verb,
    813                    String directObject,
    814                    String shortName,
    815                    String longName,
    816                    String description,
    817                    Object defaultValue) {
    818             mMode = mode;
    819             mMandatory = mandatory;
    820             mVerb = verb;
    821             mDirectObject = directObject;
    822             mShortName = shortName;
    823             mLongName = longName;
    824             mDescription = description;
    825             mDefaultValue = defaultValue;
    826             mInCommandLine = false;
    827             if (defaultValue instanceof String[]) {
    828                 mCurrentValue = ((String[])defaultValue)[0];
    829             } else {
    830                 mCurrentValue = mDefaultValue;
    831             }
    832         }
    833 
    834         /** Return true if this argument is mandatory for this verb/directobject. */
    835         public boolean isMandatory() {
    836             return mMandatory;
    837         }
    838 
    839         /** Returns the 1-letter short name of the argument, e.g. -v. */
    840         public String getShortArg() {
    841             return mShortName;
    842         }
    843 
    844         /** Returns the long name of the argument, e.g. --verbose. */
    845         public String getLongArg() {
    846             return mLongName;
    847         }
    848 
    849         /** Returns the description. Never null. */
    850         public String getDescription() {
    851             return mDescription;
    852         }
    853 
    854         /** Returns the verb for that argument. Never null. */
    855         public String getVerb() {
    856             return mVerb;
    857         }
    858 
    859         /** Returns the direct Object for that argument. Never null, but can be empty string. */
    860         public String getDirectObject() {
    861             return mDirectObject;
    862         }
    863 
    864         /** Returns the default value. Can be null. */
    865         public Object getDefaultValue() {
    866             return mDefaultValue;
    867         }
    868 
    869         /** Returns the current value. Initially set to the default value. Can be null. */
    870         public Object getCurrentValue() {
    871             return mCurrentValue;
    872         }
    873 
    874         /** Sets the current value. Can be null. */
    875         public void setCurrentValue(Object currentValue) {
    876             mCurrentValue = currentValue;
    877         }
    878 
    879         /** Returns the argument mode (type + process method). Never null. */
    880         public Mode getMode() {
    881             return mMode;
    882         }
    883 
    884         /** Returns true if the argument has been used on the command line. */
    885         public boolean isInCommandLine() {
    886             return mInCommandLine;
    887         }
    888 
    889         /** Sets if the argument has been used on the command line. */
    890         public void setInCommandLine(boolean inCommandLine) {
    891             mInCommandLine = inCommandLine;
    892         }
    893     }
    894 
    895     /**
    896      * Internal helper to define a new argument for a give action.
    897      *
    898      * @param mode The {@link Mode} for the argument.
    899      * @param mandatory The argument is required (never if {@link Mode#BOOLEAN})
    900      * @param verb The verb name. Can be #INTERNAL_VERB.
    901      * @param directObject The action name. Can be #NO_VERB_OBJECT or #INTERNAL_FLAG.
    902      * @param shortName The one-letter short argument name. Can be empty but not null.
    903      * @param longName The long argument name. Can be empty but not null.
    904      * @param description The description. Cannot be null.
    905      * @param defaultValue The default value (or values), which depends on the selected {@link Mode}.
    906      */
    907     protected void define(Mode mode,
    908             boolean mandatory,
    909             String verb,
    910             String directObject,
    911             String shortName, String longName,
    912             String description, Object defaultValue) {
    913         assert(!(mandatory && mode == Mode.BOOLEAN)); // a boolean mode cannot be mandatory
    914 
    915         // We should always have at least a short or long name, ideally both but never none.
    916         assert shortName != null;
    917         assert longName != null;
    918         assert shortName.length() > 0 || longName.length()  > 0;
    919 
    920         if (directObject == null) {
    921             directObject = NO_VERB_OBJECT;
    922         }
    923 
    924         String key = verb + '/' + directObject + '/' + longName;
    925         mArguments.put(key, new Arg(mode, mandatory,
    926                 verb, directObject, shortName, longName, description, defaultValue));
    927     }
    928 
    929     /**
    930      * Exits in case of error.
    931      * This is protected so that it can be overridden in unit tests.
    932      */
    933     protected void exit() {
    934         System.exit(1);
    935     }
    936 
    937     /**
    938      * Prints a line to stdout.
    939      * This is protected so that it can be overridden in unit tests.
    940      *
    941      * @param format The string to be formatted. Cannot be null.
    942      * @param args Format arguments.
    943      */
    944     protected void stdout(String format, Object...args) {
    945         String output = String.format(format, args);
    946         output = LineUtil.reflowLine(output);
    947         mLog.printf("%s\n", output);    //$NON-NLS-1$
    948     }
    949 
    950     /**
    951      * Prints a line to stderr.
    952      * This is protected so that it can be overridden in unit tests.
    953      *
    954      * @param format The string to be formatted. Cannot be null.
    955      * @param args Format arguments.
    956      */
    957     protected void stderr(String format, Object...args) {
    958         mLog.error(null, format, args);
    959     }
    960 }
    961