blob: 80e56cbcca3f2607a366ca2926baed20e760a71e [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
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
15package com.google.devtools.common.options;
16
17import com.google.common.base.Function;
18import com.google.common.base.Functions;
19import com.google.common.base.Joiner;
20import com.google.common.base.Preconditions;
21import com.google.common.collect.ImmutableList;
22import com.google.common.collect.Lists;
23import com.google.common.collect.Maps;
24
25import java.lang.reflect.Field;
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.Collection;
29import java.util.Collections;
Damien Martin-Guillerezfbed1062015-09-03 14:32:52 +000030import java.util.Comparator;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010031import java.util.List;
32import java.util.Map;
33
34/**
35 * A parser for options. Typical use case in a main method:
36 *
37 * <pre>
38 * OptionsParser parser = OptionsParser.newOptionsParser(FooOptions.class, BarOptions.class);
39 * parser.parseAndExitUponError(args);
40 * FooOptions foo = parser.getOptions(FooOptions.class);
41 * BarOptions bar = parser.getOptions(BarOptions.class);
42 * List&lt;String&gt; otherArguments = parser.getResidue();
43 * </pre>
44 *
45 * <p>FooOptions and BarOptions would be options specification classes, derived
46 * from OptionsBase, that contain fields annotated with @Option(...).
47 *
48 * <p>Alternatively, rather than calling
49 * {@link #parseAndExitUponError(OptionPriority, String, String[])},
50 * client code may call {@link #parse(OptionPriority,String,List)}, and handle
51 * parser exceptions usage messages themselves.
52 *
53 * <p>This options parsing implementation has (at least) one design flaw. It
54 * allows both '--foo=baz' and '--foo baz' for all options except void, boolean
55 * and tristate options. For these, the 'baz' in '--foo baz' is not treated as
56 * a parameter to the option, making it is impossible to switch options between
57 * void/boolean/tristate and everything else without breaking backwards
58 * compatibility.
59 *
60 * @see Options a simpler class which you can use if you only have one options
61 * specification class
62 */
63public class OptionsParser implements OptionsProvider {
64
65 /**
66 * A cache for the parsed options data. Both keys and values are immutable, so
67 * this is always safe. Only access this field through the {@link
68 * #getOptionsData} method for thread-safety! The cache is very unlikely to
69 * grow to a significant amount of memory, because there's only a fixed set of
70 * options classes on the classpath.
71 */
72 private static final Map<ImmutableList<Class<? extends OptionsBase>>, OptionsData> optionsData =
73 Maps.newHashMap();
74
75 private static synchronized OptionsData getOptionsData(
76 ImmutableList<Class<? extends OptionsBase>> optionsClasses) {
77 OptionsData result = optionsData.get(optionsClasses);
78 if (result == null) {
79 result = OptionsData.of(optionsClasses);
80 optionsData.put(optionsClasses, result);
81 }
82 return result;
83 }
84
85 /**
86 * Returns all the annotated fields for the given class, including inherited
87 * ones.
88 */
89 static Collection<Field> getAllAnnotatedFields(Class<? extends OptionsBase> optionsClass) {
90 OptionsData data = getOptionsData(ImmutableList.<Class<? extends OptionsBase>>of(optionsClass));
91 return data.getFieldsForClass(optionsClass);
92 }
93
94 /**
95 * @see #newOptionsParser(Iterable)
96 */
97 public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1) {
98 return newOptionsParser(ImmutableList.<Class<? extends OptionsBase>>of(class1));
99 }
100
101 /**
102 * @see #newOptionsParser(Iterable)
103 */
104 public static OptionsParser newOptionsParser(Class<? extends OptionsBase> class1,
105 Class<? extends OptionsBase> class2) {
106 return newOptionsParser(ImmutableList.of(class1, class2));
107 }
108
109 /**
110 * Create a new {@link OptionsParser}.
111 */
112 public static OptionsParser newOptionsParser(
113 Iterable<Class<? extends OptionsBase>> optionsClasses) {
114 return new OptionsParser(getOptionsData(ImmutableList.copyOf(optionsClasses)));
115 }
116
117 /**
118 * Canonicalizes a list of options using the given option classes. The
119 * contract is that if the returned set of options is passed to an options
120 * parser with the same options classes, then that will have the same effect
121 * as using the original args (which are passed in here), except for cosmetic
122 * differences.
123 */
124 public static List<String> canonicalize(
125 Collection<Class<? extends OptionsBase>> optionsClasses, List<String> args)
126 throws OptionsParsingException {
127 OptionsParser parser = new OptionsParser(optionsClasses);
128 parser.setAllowResidue(false);
129 parser.parse(args);
130 return parser.impl.asCanonicalizedList();
131 }
132
133 private final OptionsParserImpl impl;
134 private final List<String> residue = new ArrayList<String>();
135 private boolean allowResidue = true;
136
137 OptionsParser(Collection<Class<? extends OptionsBase>> optionsClasses) {
138 this(OptionsData.of(optionsClasses));
139 }
140
141 OptionsParser(OptionsData optionsData) {
142 impl = new OptionsParserImpl(optionsData);
143 }
144
145 /**
146 * Indicates whether or not the parser will allow a non-empty residue; that
147 * is, iff this value is true then a call to one of the {@code parse}
148 * methods will throw {@link OptionsParsingException} unless
149 * {@link #getResidue()} is empty after parsing.
150 */
151 public void setAllowResidue(boolean allowResidue) {
152 this.allowResidue = allowResidue;
153 }
154
155 /**
156 * Indicates whether or not the parser will allow long options with a
157 * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example.
158 */
159 public void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) {
160 this.impl.setAllowSingleDashLongOptions(allowSingleDashLongOptions);
161 }
162
163 public void parseAndExitUponError(String[] args) {
164 parseAndExitUponError(OptionPriority.COMMAND_LINE, "unknown", args);
165 }
166
167 /**
168 * A convenience function for use in main methods. Parses the command line
169 * parameters, and exits upon error. Also, prints out the usage message
170 * if "--help" appears anywhere within {@code args}.
171 */
172 public void parseAndExitUponError(OptionPriority priority, String source, String[] args) {
173 try {
174 parse(priority, source, Arrays.asList(args));
175 } catch (OptionsParsingException e) {
176 System.err.println("Error parsing command line: " + e.getMessage());
177 System.err.println("Try --help.");
178 System.exit(2);
179 }
180 for (String arg : args) {
181 if (arg.equals("--help")) {
182 System.out.println(describeOptions(Collections.<String, String>emptyMap(),
183 HelpVerbosity.LONG));
184 System.exit(0);
185 }
186 }
187 }
188
189 /**
190 * The name and value of an option with additional metadata describing its
191 * priority, source, whether it was set via an implicit dependency, and if so,
192 * by which other option.
193 */
194 public static class OptionValueDescription {
195 private final String name;
196 private final Object value;
197 private final OptionPriority priority;
198 private final String source;
199 private final String implicitDependant;
200 private final String expandedFrom;
201
202 public OptionValueDescription(String name, Object value,
203 OptionPriority priority, String source, String implicitDependant, String expandedFrom) {
204 this.name = name;
205 this.value = value;
206 this.priority = priority;
207 this.source = source;
208 this.implicitDependant = implicitDependant;
209 this.expandedFrom = expandedFrom;
210 }
211
212 public String getName() {
213 return name;
214 }
215
216 public Object getValue() {
217 return value;
218 }
219
220 public OptionPriority getPriority() {
221 return priority;
222 }
223
224 public String getSource() {
225 return source;
226 }
227
228 public String getImplicitDependant() {
229 return implicitDependant;
230 }
231
232 public boolean isImplicitDependency() {
233 return implicitDependant != null;
234 }
235
236 public String getExpansionParent() {
237 return expandedFrom;
238 }
239
240 public boolean isExpansion() {
241 return expandedFrom != null;
242 }
243
244 @Override
245 public String toString() {
246 StringBuilder result = new StringBuilder();
247 result.append("option '").append(name).append("' ");
248 result.append("set to '").append(value).append("' ");
249 result.append("with priority ").append(priority);
250 if (source != null) {
251 result.append(" and source '").append(source).append("'");
252 }
253 if (implicitDependant != null) {
254 result.append(" implicitly by ");
255 }
256 return result.toString();
257 }
258 }
259
260 /**
261 * The name and unparsed value of an option with additional metadata describing its
262 * priority, source, whether it was set via an implicit dependency, and if so,
263 * by which other option.
264 *
265 * <p>Note that the unparsed value and the source parameters can both be null.
266 */
267 public static class UnparsedOptionValueDescription {
268 private final String name;
269 private final Field field;
270 private final String unparsedValue;
271 private final OptionPriority priority;
272 private final String source;
273 private final boolean explicit;
274
275 public UnparsedOptionValueDescription(String name, Field field, String unparsedValue,
276 OptionPriority priority, String source, boolean explicit) {
277 this.name = name;
278 this.field = field;
279 this.unparsedValue = unparsedValue;
280 this.priority = priority;
281 this.source = source;
282 this.explicit = explicit;
283 }
284
285 public String getName() {
286 return name;
287 }
288
289 Field getField() {
290 return field;
291 }
292
293 public boolean isBooleanOption() {
294 return field.getType().equals(boolean.class);
295 }
296
297 private DocumentationLevel documentationLevel() {
298 Option option = field.getAnnotation(Option.class);
299 return OptionsParser.documentationLevel(option.category());
300 }
301
302 public boolean isDocumented() {
303 return documentationLevel() == DocumentationLevel.DOCUMENTED;
304 }
305
306 public boolean isHidden() {
307 return documentationLevel() == DocumentationLevel.HIDDEN;
308 }
309
310 boolean isExpansion() {
311 Option option = field.getAnnotation(Option.class);
312 return option.expansion().length > 0;
313 }
314
315 boolean isImplicitRequirement() {
316 Option option = field.getAnnotation(Option.class);
317 return option.implicitRequirements().length > 0;
318 }
319
320 boolean allowMultiple() {
321 Option option = field.getAnnotation(Option.class);
322 return option.allowMultiple();
323 }
324
325 public String getUnparsedValue() {
326 return unparsedValue;
327 }
328
329 OptionPriority getPriority() {
330 return priority;
331 }
332
333 public String getSource() {
334 return source;
335 }
336
337 public boolean isExplicit() {
338 return explicit;
339 }
340
341 @Override
342 public String toString() {
343 StringBuilder result = new StringBuilder();
344 result.append("option '").append(name).append("' ");
345 result.append("set to '").append(unparsedValue).append("' ");
346 result.append("with priority ").append(priority);
347 if (source != null) {
348 result.append(" and source '").append(source).append("'");
349 }
350 return result.toString();
351 }
352 }
353
354 /**
355 * The verbosity with which option help messages are displayed: short (just
356 * the name), medium (name, type, default, abbreviation), and long (full
357 * description).
358 */
359 public enum HelpVerbosity { LONG, MEDIUM, SHORT }
360
361 /**
362 * The level of documentation. Only documented options are output as part of
363 * the help.
364 *
365 * <p>We use 'hidden' so that options that form the protocol between the
366 * client and the server are not logged.
367 */
368 enum DocumentationLevel {
369 DOCUMENTED, UNDOCUMENTED, HIDDEN
370 }
371
372 /**
373 * Returns a description of all the options this parser can digest.
374 * In addition to {@link Option} annotations, this method also
375 * interprets {@link OptionsUsage} annotations which give an intuitive short
376 * description for the options.
377 *
378 * @param categoryDescriptions a mapping from category names to category
379 * descriptions. Options of the same category (see {@link
380 * Option#category}) will be grouped together, preceded by the description
381 * of the category.
382 * @param helpVerbosity if {@code long}, the options will be described
383 * verbosely, including their types, defaults and descriptions. If {@code
384 * medium}, the descriptions are omitted, and if {@code short}, the options
385 * are just enumerated.
386 */
387 public String describeOptions(Map<String, String> categoryDescriptions,
388 HelpVerbosity helpVerbosity) {
389 StringBuilder desc = new StringBuilder();
390 if (!impl.getOptionsClasses().isEmpty()) {
391
392 List<Field> allFields = Lists.newArrayList();
393 for (Class<? extends OptionsBase> optionsClass : impl.getOptionsClasses()) {
394 allFields.addAll(impl.getAnnotatedFieldsFor(optionsClass));
395 }
396 Collections.sort(allFields, OptionsUsage.BY_CATEGORY);
397 String prevCategory = null;
398
399 for (Field optionField : allFields) {
400 String category = optionField.getAnnotation(Option.class).category();
401 if (!category.equals(prevCategory)) {
402 prevCategory = category;
403 String description = categoryDescriptions.get(category);
404 if (description == null) {
405 description = "Options category '" + category + "'";
406 }
407 if (documentationLevel(category) == DocumentationLevel.DOCUMENTED) {
408 desc.append("\n").append(description).append(":\n");
409 }
410 }
411
412 if (documentationLevel(prevCategory) == DocumentationLevel.DOCUMENTED) {
413 OptionsUsage.getUsage(optionField, desc, helpVerbosity);
414 }
415 }
416 }
417 return desc.toString().trim();
418 }
419
420 /**
Damien Martin-Guillerez29728d42015-04-09 20:48:04 +0000421 * Returns a string listing the possible flag completion for this command along with the command
422 * completion if any. See {@link OptionsUsage#getCompletion(Field, StringBuilder)} for more
423 * details on the format for the flag completion.
424 */
425 public String getOptionsCompletion() {
426 StringBuilder desc = new StringBuilder();
427
428 // List all options
429 List<Field> allFields = Lists.newArrayList();
430 for (Class<? extends OptionsBase> optionsClass : impl.getOptionsClasses()) {
431 allFields.addAll(impl.getAnnotatedFieldsFor(optionsClass));
432 }
Damien Martin-Guillerezfbed1062015-09-03 14:32:52 +0000433 // Sort field for deterministic ordering
434 Collections.sort(allFields, new Comparator<Field>() {
435 @Override
436 public int compare(Field f1, Field f2) {
437 String name1 = f1.getAnnotation(Option.class).name();
438 String name2 = f2.getAnnotation(Option.class).name();
439 return name1.compareTo(name2);
440 }
441 });
Damien Martin-Guillerez29728d42015-04-09 20:48:04 +0000442 for (Field optionField : allFields) {
443 String category = optionField.getAnnotation(Option.class).category();
444 if (documentationLevel(category) == DocumentationLevel.DOCUMENTED) {
445 OptionsUsage.getCompletion(optionField, desc);
446 }
447 }
448
449 return desc.toString();
450 }
451
452 /**
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100453 * Returns a description of the option value set by the last previous call to
454 * {@link #parse(OptionPriority, String, List)} that successfully set the given
455 * option. If the option is of type {@link List}, the description will
456 * correspond to any one of the calls, but not necessarily the last.
457 */
458 public OptionValueDescription getOptionValueDescription(String name) {
459 return impl.getOptionValueDescription(name);
460 }
461
462 static DocumentationLevel documentationLevel(String category) {
463 if ("undocumented".equals(category)) {
464 return DocumentationLevel.UNDOCUMENTED;
465 } else if ("hidden".equals(category)) {
466 return DocumentationLevel.HIDDEN;
467 } else {
468 return DocumentationLevel.DOCUMENTED;
469 }
470 }
471
472 /**
473 * A convenience method, equivalent to
474 * {@code parse(OptionPriority.COMMAND_LINE, null, Arrays.asList(args))}.
475 */
476 public void parse(String... args) throws OptionsParsingException {
477 parse(OptionPriority.COMMAND_LINE, (String) null, Arrays.asList(args));
478 }
479
480 /**
481 * A convenience method, equivalent to
482 * {@code parse(OptionPriority.COMMAND_LINE, null, args)}.
483 */
484 public void parse(List<String> args) throws OptionsParsingException {
485 parse(OptionPriority.COMMAND_LINE, (String) null, args);
486 }
487
488 /**
489 * Parses {@code args}, using the classes registered with this parser.
490 * {@link #getOptions(Class)} and {@link #getResidue()} return the results.
491 * May be called multiple times; later options override existing ones if they
492 * have equal or higher priority. The source of options is a free-form string
493 * that can be used for debugging. Strings that cannot be parsed as options
494 * accumulates as residue, if this parser allows it.
495 *
496 * @see OptionPriority
497 */
498 public void parse(OptionPriority priority, String source,
499 List<String> args) throws OptionsParsingException {
500 parseWithSourceFunction(priority, Functions.constant(source), args);
501 }
502
503 /**
504 * Parses {@code args}, using the classes registered with this parser.
505 * {@link #getOptions(Class)} and {@link #getResidue()} return the results. May be called
506 * multiple times; later options override existing ones if they have equal or higher priority.
507 * The source of options is given as a function that maps option names to the source of the
508 * option. Strings that cannot be parsed as options accumulates as* residue, if this parser
509 * allows it.
510 */
511 public void parseWithSourceFunction(OptionPriority priority,
512 Function<? super String, String> sourceFunction, List<String> args)
513 throws OptionsParsingException {
514 Preconditions.checkNotNull(priority);
515 Preconditions.checkArgument(priority != OptionPriority.DEFAULT);
516 residue.addAll(impl.parse(priority, sourceFunction, args));
517 if (!allowResidue && !residue.isEmpty()) {
518 String errorMsg = "Unrecognized arguments: " + Joiner.on(' ').join(residue);
519 throw new OptionsParsingException(errorMsg);
520 }
521 }
522
523 @Override
524 public List<String> getResidue() {
525 return ImmutableList.copyOf(residue);
526 }
527
528 /**
529 * Returns a list of warnings about problems encountered by previous parse calls.
530 */
531 public List<String> getWarnings() {
532 return impl.getWarnings();
533 }
534
535 @Override
536 public <O extends OptionsBase> O getOptions(Class<O> optionsClass) {
537 return impl.getParsedOptions(optionsClass);
538 }
539
540 @Override
541 public boolean containsExplicitOption(String name) {
542 return impl.containsExplicitOption(name);
543 }
544
545 @Override
546 public List<UnparsedOptionValueDescription> asListOfUnparsedOptions() {
547 return impl.asListOfUnparsedOptions();
548 }
549
550 @Override
551 public List<UnparsedOptionValueDescription> asListOfExplicitOptions() {
552 return impl.asListOfExplicitOptions();
553 }
554
555 @Override
556 public List<OptionValueDescription> asListOfEffectiveOptions() {
557 return impl.asListOfEffectiveOptions();
558 }
559}