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