1 // Copyright 2014 The Bazel Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //    http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.google.devtools.common.options;
16 
17 import com.google.common.base.Function;
18 import com.google.common.base.Functions;
19 import com.google.common.base.Joiner;
20 import com.google.common.base.Preconditions;
21 import com.google.common.base.Predicate;
22 import com.google.common.collect.ArrayListMultimap;
23 import com.google.common.collect.ImmutableList;
24 import com.google.common.collect.Iterables;
25 import com.google.common.collect.Iterators;
26 import com.google.common.collect.LinkedHashMultimap;
27 import com.google.common.collect.Lists;
28 import com.google.common.collect.Maps;
29 import com.google.common.collect.Multimap;
30 import com.google.devtools.common.options.OptionsParser.OptionDescription;
31 import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
32 import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription;
33 import java.lang.reflect.Constructor;
34 import java.lang.reflect.Field;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.Comparator;
39 import java.util.Iterator;
40 import java.util.LinkedHashMap;
41 import java.util.List;
42 import java.util.Map;
43 
44 /**
45  * The implementation of the options parser. This is intentionally package
46  * private for full flexibility. Use {@link OptionsParser} or {@link Options}
47  * if you're a consumer.
48  */
49 class OptionsParserImpl {
50 
51   private final OptionsData optionsData;
52 
53   /**
54    * We store the results of parsing the arguments in here. It'll look like
55    *
56    * <pre>
57    *   Field("--host") -> "www.google.com"
58    *   Field("--port") -> 80
59    * </pre>
60    *
61    * This map is modified by repeated calls to {@link #parse(OptionPriority,Function,List)}.
62    */
63   private final Map<Field, OptionValueDescription> parsedValues = Maps.newHashMap();
64 
65   /**
66    * We store the pre-parsed, explicit options for each priority in here.
67    * We use partially preparsed options, which can be different from the original
68    * representation, e.g. "--nofoo" becomes "--foo=0".
69    */
70   private final List<UnparsedOptionValueDescription> unparsedValues = Lists.newArrayList();
71 
72   /**
73    * Unparsed values for use with the canonicalize command are stored separately from
74    * unparsedValues so that invocation policy can modify the values for canonicalization (e.g.
75    * override user-specified values with default values) without corrupting the data used to
76    * represent the user's original invocation for {@link #asListOfExplicitOptions()} and
77    * {@link #asListOfUnparsedOptions()}. A LinkedHashMultimap is used so that canonicalization
78    * happens in the correct order and multiple values can be stored for flags that allow multiple
79    * values.
80    */
81   private final Multimap<Field, UnparsedOptionValueDescription> canonicalizeValues
82       = LinkedHashMultimap.create();
83 
84   private final List<String> warnings = Lists.newArrayList();
85 
86   private boolean allowSingleDashLongOptions = false;
87 
88   private ArgsPreProcessor argsPreProcessor =
89       new ArgsPreProcessor() {
90         @Override
91         public List<String> preProcess(List<String> args) throws OptionsParsingException {
92           return args;
93         }
94       };
95 
96   /**
97    * Create a new parser object
98    */
OptionsParserImpl(OptionsData optionsData)99   OptionsParserImpl(OptionsData optionsData) {
100     this.optionsData = optionsData;
101   }
102 
getOptionsData()103   OptionsData getOptionsData() {
104     return optionsData;
105   }
106 
107   /**
108    * Indicates whether or not the parser will allow long options with a
109    * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example.
110    */
setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions)111   void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) {
112     this.allowSingleDashLongOptions = allowSingleDashLongOptions;
113   }
114 
115   /** Sets the ArgsPreProcessor for manipulations of the options before parsing. */
setArgsPreProcessor(ArgsPreProcessor preProcessor)116   void setArgsPreProcessor(ArgsPreProcessor preProcessor) {
117     this.argsPreProcessor = Preconditions.checkNotNull(preProcessor);
118   }
119 
120   /**
121    * The implementation of {@link OptionsBase#asMap}.
122    */
optionsAsMap(OptionsBase optionsInstance)123   static Map<String, Object> optionsAsMap(OptionsBase optionsInstance) {
124     Map<String, Object> map = Maps.newHashMap();
125     for (Field field : OptionsParser.getAllAnnotatedFields(optionsInstance.getClass())) {
126       try {
127         String name = field.getAnnotation(Option.class).name();
128         Object value = field.get(optionsInstance);
129         map.put(name, value);
130       } catch (IllegalAccessException e) {
131         throw new IllegalStateException(e); // unreachable
132       }
133     }
134     return map;
135   }
136 
getAnnotatedFieldsFor(Class<? extends OptionsBase> clazz)137   List<Field> getAnnotatedFieldsFor(Class<? extends OptionsBase> clazz) {
138     return optionsData.getFieldsForClass(clazz);
139   }
140 
141   /**
142    * Implements {@link OptionsParser#asListOfUnparsedOptions()}.
143    */
asListOfUnparsedOptions()144   List<UnparsedOptionValueDescription> asListOfUnparsedOptions() {
145     List<UnparsedOptionValueDescription> result = Lists.newArrayList(unparsedValues);
146     // It is vital that this sort is stable so that options on the same priority are not reordered.
147     Collections.sort(result, new Comparator<UnparsedOptionValueDescription>() {
148       @Override
149       public int compare(UnparsedOptionValueDescription o1,
150           UnparsedOptionValueDescription o2) {
151         return o1.getPriority().compareTo(o2.getPriority());
152       }
153     });
154     return result;
155   }
156 
157   /**
158    * Implements {@link OptionsParser#asListOfExplicitOptions()}.
159    */
asListOfExplicitOptions()160   List<UnparsedOptionValueDescription> asListOfExplicitOptions() {
161     List<UnparsedOptionValueDescription> result = Lists.newArrayList(Iterables.filter(
162       unparsedValues,
163       new Predicate<UnparsedOptionValueDescription>() {
164         @Override
165         public boolean apply(UnparsedOptionValueDescription input) {
166           return input.isExplicit();
167         }
168     }));
169     // It is vital that this sort is stable so that options on the same priority are not reordered.
170     Collections.sort(result, new Comparator<UnparsedOptionValueDescription>() {
171       @Override
172       public int compare(UnparsedOptionValueDescription o1,
173           UnparsedOptionValueDescription o2) {
174         return o1.getPriority().compareTo(o2.getPriority());
175       }
176     });
177     return result;
178   }
179 
180   /**
181    * Implements {@link OptionsParser#canonicalize}.
182    */
asCanonicalizedList()183   List<String> asCanonicalizedList() {
184 
185     List<UnparsedOptionValueDescription> processed = Lists.newArrayList(
186         canonicalizeValues.values());
187     // Sort implicit requirement options to the end, keeping their existing order, and sort the
188     // other options alphabetically.
189     Collections.sort(processed, new Comparator<UnparsedOptionValueDescription>() {
190       @Override
191       public int compare(UnparsedOptionValueDescription o1, UnparsedOptionValueDescription o2) {
192         if (o1.isImplicitRequirement()) {
193           return o2.isImplicitRequirement() ? 0 : 1;
194         }
195         if (o2.isImplicitRequirement()) {
196           return -1;
197         }
198         return o1.getName().compareTo(o2.getName());
199       }
200     });
201 
202     List<String> result = Lists.newArrayList();
203     for (UnparsedOptionValueDescription value : processed) {
204 
205       // Ignore expansion options.
206       if (value.isExpansion()) {
207         continue;
208       }
209 
210       result.add("--" + value.getName() + "=" + value.getUnparsedValue());
211     }
212     return result;
213   }
214 
215   /**
216    * Implements {@link OptionsParser#asListOfEffectiveOptions()}.
217    */
asListOfEffectiveOptions()218   List<OptionValueDescription> asListOfEffectiveOptions() {
219     List<OptionValueDescription> result = Lists.newArrayList();
220     for (Map.Entry<String, Field> mapEntry : optionsData.getAllNamedFields()) {
221       String fieldName = mapEntry.getKey();
222       Field field = mapEntry.getValue();
223       OptionValueDescription entry = parsedValues.get(field);
224       if (entry == null) {
225         Object value = optionsData.getDefaultValue(field);
226         result.add(
227             new OptionValueDescription(
228                 fieldName, value, OptionPriority.DEFAULT, null, null, null, false));
229       } else {
230         result.add(entry);
231       }
232     }
233     return result;
234   }
235 
getOptionsClasses()236   Collection<Class<?  extends OptionsBase>> getOptionsClasses() {
237     return optionsData.getOptionsClasses();
238   }
239 
maybeAddDeprecationWarning(Field field)240   private void maybeAddDeprecationWarning(Field field) {
241     Option option = field.getAnnotation(Option.class);
242     // Continue to support the old behavior for @Deprecated options.
243     String warning = option.deprecationWarning();
244     if (!warning.isEmpty() || (field.getAnnotation(Deprecated.class) != null)) {
245       addDeprecationWarning(option.name(), warning);
246     }
247   }
248 
addDeprecationWarning(String optionName, String warning)249   private void addDeprecationWarning(String optionName, String warning) {
250     warnings.add("Option '" + optionName + "' is deprecated"
251         + (warning.isEmpty() ? "" : ": " + warning));
252   }
253 
254   // Warnings should not end with a '.' because the internal reporter adds one automatically.
setValue(Field field, String name, Object value, OptionPriority priority, String source, String implicitDependant, String expandedFrom)255   private void setValue(Field field, String name, Object value,
256       OptionPriority priority, String source, String implicitDependant, String expandedFrom) {
257     OptionValueDescription entry = parsedValues.get(field);
258     if (entry != null) {
259       // Override existing option if the new value has higher or equal priority.
260       if (priority.compareTo(entry.getPriority()) >= 0) {
261         // Output warnings:
262         if ((implicitDependant != null) && (entry.getImplicitDependant() != null)) {
263           if (!implicitDependant.equals(entry.getImplicitDependant())) {
264             warnings.add(
265                 "Option '"
266                     + name
267                     + "' is implicitly defined by both option '"
268                     + entry.getImplicitDependant()
269                     + "' and option '"
270                     + implicitDependant
271                     + "'");
272           }
273         } else if ((implicitDependant != null) && priority.equals(entry.getPriority())) {
274           warnings.add(
275               "Option '"
276                   + name
277                   + "' is implicitly defined by option '"
278                   + implicitDependant
279                   + "'; the implicitly set value overrides the previous one");
280         } else if (entry.getImplicitDependant() != null) {
281           warnings.add(
282               "A new value for option '"
283                   + name
284                   + "' overrides a previous implicit setting of that option by option '"
285                   + entry.getImplicitDependant()
286                   + "'");
287         } else if ((priority == entry.getPriority())
288             && ((entry.getExpansionParent() == null) && (expandedFrom != null))) {
289           // Create a warning if an expansion option overrides an explicit option:
290           warnings.add("The option '" + expandedFrom + "' was expanded and now overrides a "
291               + "previous explicitly specified option '" + name + "'");
292         }
293 
294         // Record the new value:
295         parsedValues.put(
296             field,
297             new OptionValueDescription(
298                 name, value, priority, source, implicitDependant, expandedFrom, false));
299       }
300     } else {
301       parsedValues.put(
302           field,
303           new OptionValueDescription(
304               name, value, priority, source, implicitDependant, expandedFrom, false));
305       maybeAddDeprecationWarning(field);
306     }
307   }
308 
addListValue(Field field, Object value, OptionPriority priority, String source, String implicitDependant, String expandedFrom)309   private void addListValue(Field field, Object value, OptionPriority priority, String source,
310       String implicitDependant, String expandedFrom) {
311     OptionValueDescription entry = parsedValues.get(field);
312     if (entry == null) {
313       entry =
314           new OptionValueDescription(
315               field.getName(),
316               ArrayListMultimap.create(),
317               priority,
318               source,
319               implicitDependant,
320               expandedFrom,
321               true);
322       parsedValues.put(field, entry);
323       maybeAddDeprecationWarning(field);
324     }
325     entry.addValue(priority, value);
326   }
327 
clearValue(String optionName, Map<String, OptionValueDescription> clearedValues)328   void clearValue(String optionName, Map<String, OptionValueDescription> clearedValues)
329       throws OptionsParsingException {
330     Field field = optionsData.getFieldFromName(optionName);
331     if (field == null) {
332       throw new IllegalArgumentException("No such option '" + optionName + "'");
333     }
334     Option option = field.getAnnotation(Option.class);
335     clearValue(field, option, clearedValues);
336   }
337 
clearValue( Field field, Option option, Map<String, OptionValueDescription> clearedValues)338   private void clearValue(
339       Field field, Option option, Map<String, OptionValueDescription> clearedValues)
340       throws OptionsParsingException {
341 
342     OptionValueDescription removed = parsedValues.remove(field);
343     if (removed != null) {
344       clearedValues.put(option.name(), removed);
345     }
346 
347     canonicalizeValues.removeAll(field);
348 
349     // Recurse to remove any implicit or expansion flags that this flag may have added when
350     // originally parsed.
351     String[] expansion = optionsData.getEvaluatedExpansion(field);
352     for (String[] args : new String[][] {option.implicitRequirements(), expansion}) {
353       Iterator<String> argsIterator = Iterators.forArray(args);
354       while (argsIterator.hasNext()) {
355         String arg = argsIterator.next();
356         ParseOptionResult parseOptionResult = parseOption(arg, argsIterator);
357         clearValue(parseOptionResult.field, parseOptionResult.option, clearedValues);
358       }
359     }
360   }
361 
getOptionValueDescription(String name)362   OptionValueDescription getOptionValueDescription(String name) {
363     Field field = optionsData.getFieldFromName(name);
364     if (field == null) {
365       throw new IllegalArgumentException("No such option '" + name + "'");
366     }
367     return parsedValues.get(field);
368   }
369 
getOptionDescription(String name)370   OptionDescription getOptionDescription(String name) {
371     Field field = optionsData.getFieldFromName(name);
372     if (field == null) {
373       return null;
374     }
375 
376     Option optionAnnotation = field.getAnnotation(Option.class);
377     return new OptionDescription(
378         name,
379         optionsData.getDefaultValue(field),
380         optionsData.getConverter(field),
381         optionAnnotation.allowMultiple());
382   }
383 
containsExplicitOption(String name)384   boolean containsExplicitOption(String name) {
385     Field field = optionsData.getFieldFromName(name);
386     if (field == null) {
387       throw new IllegalArgumentException("No such option '" + name + "'");
388     }
389     return parsedValues.get(field) != null;
390   }
391 
392   /**
393    * Parses the args, and returns what it doesn't parse. May be called multiple
394    * times, and may be called recursively. In each call, there may be no
395    * duplicates, but separate calls may contain intersecting sets of options; in
396    * that case, the arg seen last takes precedence.
397    */
parse(OptionPriority priority, Function<? super String, String> sourceFunction, List<String> args)398   List<String> parse(OptionPriority priority, Function<? super String, String> sourceFunction,
399       List<String> args) throws OptionsParsingException {
400     return parse(priority, sourceFunction, null, null, args);
401   }
402 
403   /**
404    * Parses the args, and returns what it doesn't parse. May be called multiple
405    * times, and may be called recursively. Calls may contain intersecting sets
406    * of options; in that case, the arg seen last takes precedence.
407    *
408    * <p>The method uses the invariant that if an option has neither an implicit
409    * dependent nor an expanded from value, then it must have been explicitly
410    * set.
411    */
parse( OptionPriority priority, Function<? super String, String> sourceFunction, String implicitDependent, String expandedFrom, List<String> args)412   private List<String> parse(
413       OptionPriority priority,
414       Function<? super String, String> sourceFunction,
415       String implicitDependent,
416       String expandedFrom,
417       List<String> args) throws OptionsParsingException {
418 
419     List<String> unparsedArgs = Lists.newArrayList();
420     LinkedHashMap<String,List<String>> implicitRequirements = Maps.newLinkedHashMap();
421 
422     Iterator<String> argsIterator = argsPreProcessor.preProcess(args).iterator();
423     while (argsIterator.hasNext()) {
424       String arg = argsIterator.next();
425 
426       if (!arg.startsWith("-")) {
427         unparsedArgs.add(arg);
428         continue;  // not an option arg
429       }
430 
431       if (arg.equals("--")) {  // "--" means all remaining args aren't options
432         Iterators.addAll(unparsedArgs, argsIterator);
433         break;
434       }
435 
436       ParseOptionResult optionParseResult = parseOption(arg, argsIterator);
437       Field field = optionParseResult.field;
438       Option option = optionParseResult.option;
439       String value = optionParseResult.value;
440 
441       final String originalName = option.name();
442 
443       if (option.wrapperOption()) {
444         if (value.startsWith("-")) {
445 
446           List<String> unparsed = parse(
447               priority,
448               Functions.constant("Unwrapped from wrapper option --" + originalName),
449               null, // implicitDependent
450               null, // expandedFrom
451               ImmutableList.of(value));
452 
453           if (!unparsed.isEmpty()) {
454             throw new OptionsParsingException("Unparsed options remain after unwrapping " +
455               arg + ": " + Joiner.on(' ').join(unparsed));
456           }
457 
458           // Don't process implicitRequirements or expansions for wrapper options. In particular,
459           // don't record this option in unparsedValues, so that only the wrapped option shows
460           // up in canonicalized options.
461           continue;
462 
463         } else {
464           throw new OptionsParsingException("Invalid --" + originalName + " value format. "
465               + "You may have meant --" + originalName + "=--" + value);
466         }
467       }
468 
469       if (implicitDependent == null) {
470         // Log explicit options and expanded options in the order they are parsed (can be sorted
471         // later). Also remember whether they were expanded or not. This information is needed to
472         // correctly canonicalize flags.
473         UnparsedOptionValueDescription unparsedOptionValueDescription =
474             new UnparsedOptionValueDescription(
475                 originalName,
476                 field,
477                 value,
478                 priority,
479                 sourceFunction.apply(originalName),
480                 expandedFrom == null);
481         unparsedValues.add(unparsedOptionValueDescription);
482         if (option.allowMultiple()) {
483           canonicalizeValues.put(field, unparsedOptionValueDescription);
484         } else {
485           canonicalizeValues.replaceValues(field, ImmutableList.of(unparsedOptionValueDescription));
486         }
487       }
488 
489       // Handle expansion options.
490       String[] expansion = optionsData.getEvaluatedExpansion(field);
491       if (expansion.length > 0) {
492         Function<Object, String> expansionSourceFunction =
493             Functions.constant(
494                 "expanded from option --"
495                     + originalName
496                     + " from "
497                     + sourceFunction.apply(originalName));
498         maybeAddDeprecationWarning(field);
499         List<String> unparsed = parse(priority, expansionSourceFunction, null, originalName,
500             ImmutableList.copyOf(expansion));
501         if (!unparsed.isEmpty()) {
502           // Throw an assertion, because this indicates an error in the code that specified the
503           // expansion for the current option.
504           throw new AssertionError("Unparsed options remain after parsing expansion of " +
505             arg + ": " + Joiner.on(' ').join(unparsed));
506         }
507       } else {
508         Converter<?> converter = optionsData.getConverter(field);
509         Object convertedValue;
510         try {
511           convertedValue = converter.convert(value);
512         } catch (OptionsParsingException e) {
513           // The converter doesn't know the option name, so we supply it here by
514           // re-throwing:
515           throw new OptionsParsingException("While parsing option " + arg
516                                             + ": " + e.getMessage(), e);
517         }
518 
519         // ...but allow duplicates of single-use options across separate calls to
520         // parse(); latest wins:
521         if (!option.allowMultiple()) {
522           setValue(field, originalName, convertedValue,
523               priority, sourceFunction.apply(originalName), implicitDependent, expandedFrom);
524         } else {
525           // But if it's a multiple-use option, then just accumulate the
526           // values, in the order in which they were seen.
527           // Note: The type of the list member is not known; Java introspection
528           // only makes it available in String form via the signature string
529           // for the field declaration.
530           addListValue(field, convertedValue, priority, sourceFunction.apply(originalName),
531               implicitDependent, expandedFrom);
532         }
533       }
534 
535       // Collect any implicit requirements.
536       if (option.implicitRequirements().length > 0) {
537         implicitRequirements.put(option.name(), Arrays.asList(option.implicitRequirements()));
538       }
539     }
540 
541     // Now parse any implicit requirements that were collected.
542     // TODO(bazel-team): this should happen when the option is encountered.
543     if (!implicitRequirements.isEmpty()) {
544       for (Map.Entry<String, List<String>> entry : implicitRequirements.entrySet()) {
545         Function<Object, String> requirementSourceFunction =
546             Functions.constant(
547                 "implicit requirement of option --"
548                     + entry.getKey()
549                     + " from "
550                     + sourceFunction.apply(entry.getKey()));
551 
552         List<String> unparsed = parse(priority, requirementSourceFunction, entry.getKey(), null,
553             entry.getValue());
554         if (!unparsed.isEmpty()) {
555           // Throw an assertion, because this indicates an error in the code that specified in the
556           // implicit requirements for the option(s).
557           throw new AssertionError("Unparsed options remain after parsing implicit options: "
558               + Joiner.on(' ').join(unparsed));
559         }
560       }
561     }
562 
563     return unparsedArgs;
564   }
565 
566   private static final class ParseOptionResult {
567     final Field field;
568     final Option option;
569     final String value;
570 
ParseOptionResult(Field field, Option option, String value)571     ParseOptionResult(Field field, Option option, String value) {
572       this.field = field;
573       this.option = option;
574       this.value = value;
575     }
576   }
577 
parseOption(String arg, Iterator<String> nextArgs)578   private ParseOptionResult parseOption(String arg, Iterator<String> nextArgs)
579       throws OptionsParsingException {
580 
581     String value = null;
582     Field field;
583     boolean booleanValue = true;
584 
585     if (arg.length() == 2) { // -l  (may be nullary or unary)
586       field = optionsData.getFieldForAbbrev(arg.charAt(1));
587       booleanValue = true;
588 
589     } else if (arg.length() == 3 && arg.charAt(2) == '-') { // -l-  (boolean)
590       field = optionsData.getFieldForAbbrev(arg.charAt(1));
591       booleanValue = false;
592 
593     } else if (allowSingleDashLongOptions // -long_option
594         || arg.startsWith("--")) { // or --long_option
595 
596       int equalsAt = arg.indexOf('=');
597       int nameStartsAt = arg.startsWith("--") ? 2 : 1;
598       String name =
599           equalsAt == -1 ? arg.substring(nameStartsAt) : arg.substring(nameStartsAt, equalsAt);
600       if (name.trim().isEmpty()) {
601         throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
602       }
603       value = equalsAt == -1 ? null : arg.substring(equalsAt + 1);
604       field = optionsData.getFieldFromName(name);
605 
606       // look for a "no"-prefixed option name: "no<optionname>";
607       // (Undocumented: we also allow --no_foo.  We're generous like that.)
608       if (field == null && name.startsWith("no")) {
609         name = name.substring(name.startsWith("no_") ? 3 : 2);
610         field = optionsData.getFieldFromName(name);
611         booleanValue = false;
612         if (field != null) {
613           // TODO(bazel-team): Add tests for these cases.
614           if (!OptionsData.isBooleanField(field)) {
615             throw new OptionsParsingException(
616                 "Illegal use of 'no' prefix on non-boolean option: " + arg, arg);
617           }
618           if (value != null) {
619             throw new OptionsParsingException(
620                 "Unexpected value after boolean option: " + arg, arg);
621           }
622           // "no<optionname>" signifies a boolean option w/ false value
623           value = "0";
624         }
625       }
626     } else {
627       throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
628     }
629 
630     Option option = field == null ? null : field.getAnnotation(Option.class);
631 
632     if (option == null
633         || OptionsParser.documentationLevel(option.category())
634             == OptionsParser.DocumentationLevel.INTERNAL) {
635       // This also covers internal options, which are treated as if they did not exist.
636       throw new OptionsParsingException("Unrecognized option: " + arg, arg);
637     }
638 
639     if (value == null) {
640       // Special-case boolean to supply value based on presence of "no" prefix.
641       if (OptionsData.isBooleanField(field)) {
642         value = booleanValue ? "1" : "0";
643       } else if (field.getType().equals(Void.class) && !option.wrapperOption()) {
644         // This is expected, Void type options have no args (unless they're wrapper options).
645       } else if (nextArgs.hasNext()) {
646         value = nextArgs.next();  // "--flag value" form
647       } else {
648         throw new OptionsParsingException("Expected value after " + arg);
649       }
650     }
651 
652     return new ParseOptionResult(field, option, value);
653   }
654 
655   /**
656    * Gets the result of parsing the options.
657    */
getParsedOptions(Class<O> optionsClass)658   <O extends OptionsBase> O getParsedOptions(Class<O> optionsClass) {
659     // Create the instance:
660     O optionsInstance;
661     try {
662       Constructor<O> constructor = optionsData.getConstructor(optionsClass);
663       if (constructor == null) {
664         return null;
665       }
666       optionsInstance = constructor.newInstance(new Object[0]);
667     } catch (Exception e) {
668       throw new IllegalStateException(e);
669     }
670 
671     // Set the fields
672     for (Field field : optionsData.getFieldsForClass(optionsClass)) {
673       Object value;
674       OptionValueDescription entry = parsedValues.get(field);
675       if (entry == null) {
676         value = optionsData.getDefaultValue(field);
677       } else {
678         value = entry.getValue();
679       }
680       try {
681         field.set(optionsInstance, value);
682       } catch (IllegalAccessException e) {
683         throw new IllegalStateException(e);
684       }
685     }
686     return optionsInstance;
687   }
688 
getWarnings()689   List<String> getWarnings() {
690     return ImmutableList.copyOf(warnings);
691   }
692 
getDefaultOptionString(Field optionField)693   static String getDefaultOptionString(Field optionField) {
694     Option annotation = optionField.getAnnotation(Option.class);
695     return annotation.defaultValue();
696   }
697 
isSpecialNullDefault(String defaultValueString, Field optionField)698   static boolean isSpecialNullDefault(String defaultValueString, Field optionField) {
699     return defaultValueString.equals("null") && !optionField.getType().isPrimitive();
700   }
701 }
702