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