• 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 package com.google.devtools.common.options;
15 
16 import com.google.common.base.Joiner;
17 import com.google.common.base.Preconditions;
18 import com.google.common.base.Splitter;
19 import com.google.common.base.Strings;
20 import com.google.common.collect.ImmutableList;
21 import com.google.common.escape.Escaper;
22 import java.text.BreakIterator;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
29 import javax.annotation.Nullable;
30 
31 /** A renderer for usage messages for any combination of options classes. */
32 class OptionsUsage {
33 
34   private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n');
35   private static final Joiner COMMA_JOINER = Joiner.on(",");
36 
37   /**
38    * Given an options class, render the usage string into the usage, which is passed in as an
39    * argument. This will not include information about expansions for options using expansion
40    * functions (it would be unsafe to report this as we cannot know what options from other {@link
41    * OptionsBase} subclasses they depend on until a complete parser is constructed).
42    */
getUsage(Class<? extends OptionsBase> optionsClass, StringBuilder usage)43   static void getUsage(Class<? extends OptionsBase> optionsClass, StringBuilder usage) {
44     OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
45     List<OptionDefinition> optionDefinitions =
46         new ArrayList<>(OptionsData.getAllOptionDefinitionsForClass(optionsClass));
47     optionDefinitions.sort(OptionDefinition.BY_OPTION_NAME);
48     for (OptionDefinition optionDefinition : optionDefinitions) {
49       getUsage(optionDefinition, usage, OptionsParser.HelpVerbosity.LONG, data, false);
50     }
51   }
52 
53   /**
54    * Paragraph-fill the specified input text, indenting lines to 'indent' and
55    * wrapping lines at 'width'.  Returns the formatted result.
56    */
paragraphFill(String in, int indent, int width)57   static String paragraphFill(String in, int indent, int width) {
58     String indentString = Strings.repeat(" ", indent);
59     StringBuilder out = new StringBuilder();
60     String sep = "";
61     for (String paragraph : NEWLINE_SPLITTER.split(in)) {
62       // TODO(ccalvarin) break iterators expect hyphenated words to be line-breakable, which looks
63       // funny for --flag
64       BreakIterator boundary = BreakIterator.getLineInstance(); // (factory)
65       boundary.setText(paragraph);
66       out.append(sep).append(indentString);
67       int cursor = indent;
68       for (int start = boundary.first(), end = boundary.next();
69            end != BreakIterator.DONE;
70            start = end, end = boundary.next()) {
71         String word =
72             paragraph.substring(start, end); // (may include trailing space)
73         if (word.length() + cursor > width) {
74           out.append('\n').append(indentString);
75           cursor = indent;
76         }
77         out.append(word);
78         cursor += word.length();
79       }
80       sep = "\n";
81     }
82     return out.toString();
83   }
84 
85   /**
86    * Returns the expansion for an option, if any, regardless of if the expansion is from a function
87    * or is statically declared in the annotation.
88    */
getExpansionIfKnown( OptionDefinition optionDefinition, OptionsData optionsData)89   private static @Nullable ImmutableList<String> getExpansionIfKnown(
90       OptionDefinition optionDefinition, OptionsData optionsData) {
91     Preconditions.checkNotNull(optionDefinition);
92     return optionsData.getEvaluatedExpansion(optionDefinition);
93   }
94 
95   // Placeholder tag "UNKNOWN" is ignored.
shouldEffectTagBeListed(OptionEffectTag effectTag)96   private static boolean shouldEffectTagBeListed(OptionEffectTag effectTag) {
97     return !effectTag.equals(OptionEffectTag.UNKNOWN);
98   }
99 
100   // Tags that only apply to undocumented options are excluded.
shouldMetadataTagBeListed(OptionMetadataTag metadataTag)101   private static boolean shouldMetadataTagBeListed(OptionMetadataTag metadataTag) {
102     return !metadataTag.equals(OptionMetadataTag.HIDDEN)
103         && !metadataTag.equals(OptionMetadataTag.INTERNAL);
104   }
105 
106   /** Appends the usage message for a single option-field message to 'usage'. */
getUsage( OptionDefinition optionDefinition, StringBuilder usage, OptionsParser.HelpVerbosity helpVerbosity, OptionsData optionsData, boolean includeTags)107   static void getUsage(
108       OptionDefinition optionDefinition,
109       StringBuilder usage,
110       OptionsParser.HelpVerbosity helpVerbosity,
111       OptionsData optionsData,
112       boolean includeTags) {
113     String flagName = getFlagName(optionDefinition);
114     String typeDescription = getTypeDescription(optionDefinition);
115     usage.append("  --").append(flagName);
116     if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) {
117       usage.append('\n');
118       return;
119     }
120 
121     // Add the option's type and default information. Stop there for "medium" verbosity.
122     if (optionDefinition.getAbbreviation() != '\0') {
123       usage.append(" [-").append(optionDefinition.getAbbreviation()).append(']');
124     }
125     if (!typeDescription.equals("")) {
126       usage.append(" (").append(typeDescription).append("; ");
127       if (optionDefinition.allowsMultiple()) {
128         usage.append("may be used multiple times");
129       } else {
130         // Don't call the annotation directly (we must allow overrides to certain defaults)
131         String defaultValueString = optionDefinition.getUnparsedDefaultValue();
132         if (optionDefinition.isSpecialNullDefault()) {
133           usage.append("default: see description");
134         } else {
135           usage.append("default: \"").append(defaultValueString).append("\"");
136         }
137       }
138       usage.append(")");
139     }
140     usage.append("\n");
141     if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) {
142       return;
143     }
144 
145     // For verbosity "long," add the full description and expansion, along with the tag
146     // information if requested.
147     if (!optionDefinition.getHelpText().isEmpty()) {
148       usage.append(paragraphFill(optionDefinition.getHelpText(), /*indent=*/ 4, /*width=*/ 80));
149       usage.append('\n');
150     }
151     ImmutableList<String> expansion = getExpansionIfKnown(optionDefinition, optionsData);
152     if (expansion == null) {
153       usage.append(paragraphFill("Expands to unknown options.", /*indent=*/ 6, /*width=*/ 80));
154       usage.append('\n');
155     } else if (!expansion.isEmpty()) {
156       StringBuilder expandsMsg = new StringBuilder("Expands to: ");
157       for (String exp : expansion) {
158         expandsMsg.append(exp).append(" ");
159       }
160       usage.append(paragraphFill(expandsMsg.toString(), /*indent=*/ 6, /*width=*/ 80));
161       usage.append('\n');
162     }
163     if (optionDefinition.hasImplicitRequirements()) {
164       StringBuilder requiredMsg = new StringBuilder("Using this option will also add: ");
165       for (String req : optionDefinition.getImplicitRequirements()) {
166         requiredMsg.append(req).append(" ");
167       }
168       usage.append(paragraphFill(requiredMsg.toString(), 6, 80)); // (indent, width)
169       usage.append('\n');
170     }
171     if (!includeTags) {
172       return;
173     }
174 
175     // If we are expected to include the tags, add them for high verbosity.
176     Stream<OptionEffectTag> effectTagStream =
177         Arrays.stream(optionDefinition.getOptionEffectTags())
178             .filter(OptionsUsage::shouldEffectTagBeListed);
179     Stream<OptionMetadataTag> metadataTagStream =
180         Arrays.stream(optionDefinition.getOptionMetadataTags())
181             .filter(OptionsUsage::shouldMetadataTagBeListed);
182     String tagList =
183         Stream.concat(effectTagStream, metadataTagStream)
184             .map(tag -> tag.toString().toLowerCase())
185             .collect(Collectors.joining(", "));
186     if (!tagList.isEmpty()) {
187       usage.append(paragraphFill("Tags: " + tagList, 6, 80)); // (indent, width)
188       usage.append("\n");
189     }
190   }
191 
192   /** Append the usage message for a single option-field message to 'usage'. */
getUsageHtml( OptionDefinition optionDefinition, StringBuilder usage, Escaper escaper, OptionsData optionsData, boolean includeTags)193   static void getUsageHtml(
194       OptionDefinition optionDefinition,
195       StringBuilder usage,
196       Escaper escaper,
197       OptionsData optionsData,
198       boolean includeTags) {
199     String plainFlagName = optionDefinition.getOptionName();
200     String flagName = getFlagName(optionDefinition);
201     String valueDescription = optionDefinition.getValueTypeHelpText();
202     String typeDescription = getTypeDescription(optionDefinition);
203     usage.append("<dt><code><a name=\"flag--").append(plainFlagName).append("\"></a>--");
204     usage.append(flagName);
205     if (optionDefinition.usesBooleanValueSyntax() || optionDefinition.isVoidField()) {
206       // Nothing for boolean, tristate, boolean_or_enum, or void options.
207     } else if (!valueDescription.isEmpty()) {
208       usage.append("=").append(escaper.escape(valueDescription));
209     } else if (!typeDescription.isEmpty()) {
210       // Generic fallback, which isn't very good.
211       usage.append("=&lt;").append(escaper.escape(typeDescription)).append("&gt");
212     }
213     usage.append("</code>");
214     if (optionDefinition.getAbbreviation() != '\0') {
215       usage.append(" [<code>-").append(optionDefinition.getAbbreviation()).append("</code>]");
216     }
217     if (optionDefinition.allowsMultiple()) {
218       // Allow-multiple options can't have a default value.
219       usage.append(" multiple uses are accumulated");
220     } else {
221       // Don't call the annotation directly (we must allow overrides to certain defaults).
222       String defaultValueString = optionDefinition.getUnparsedDefaultValue();
223       if (optionDefinition.isVoidField()) {
224         // Void options don't have a default.
225       } else if (optionDefinition.isSpecialNullDefault()) {
226         usage.append(" default: see description");
227       } else {
228         usage.append(" default: \"").append(escaper.escape(defaultValueString)).append("\"");
229       }
230     }
231     usage.append("</dt>\n");
232     usage.append("<dd>\n");
233     if (!optionDefinition.getHelpText().isEmpty()) {
234       usage.append(
235           paragraphFill(
236               escaper.escape(optionDefinition.getHelpText()), /*indent=*/ 0, /*width=*/ 80));
237       usage.append('\n');
238     }
239 
240     if (!optionsData.getEvaluatedExpansion(optionDefinition).isEmpty()) {
241       // If this is an expansion option, list the expansion if known, or at least specify that we
242       // don't know.
243       usage.append("<br/>\n");
244       ImmutableList<String> expansion = getExpansionIfKnown(optionDefinition, optionsData);
245       StringBuilder expandsMsg;
246       if (expansion == null) {
247         expandsMsg = new StringBuilder("Expands to unknown options.<br/>\n");
248       } else {
249         Preconditions.checkArgument(!expansion.isEmpty());
250         expandsMsg = new StringBuilder("Expands to:<br/>\n");
251         for (String exp : expansion) {
252           // TODO(ulfjack): We should link to the expanded flags, but unfortunately we don't
253           // currently guarantee that all flags are only printed once. A flag in an OptionBase that
254           // is included by 2 different commands, but not inherited through a parent command, will
255           // be printed multiple times.
256           expandsMsg
257               .append("&nbsp;&nbsp;<code>")
258               .append(escaper.escape(exp))
259               .append("</code><br/>\n");
260         }
261       }
262       usage.append(expandsMsg.toString());
263     }
264 
265     // Add effect tags, if not UNKNOWN, and metadata tags, if not empty.
266     if (includeTags) {
267       Stream<OptionEffectTag> effectTagStream =
268           Arrays.stream(optionDefinition.getOptionEffectTags())
269               .filter(OptionsUsage::shouldEffectTagBeListed);
270       Stream<OptionMetadataTag> metadataTagStream =
271           Arrays.stream(optionDefinition.getOptionMetadataTags())
272               .filter(OptionsUsage::shouldMetadataTagBeListed);
273       String tagList =
274           Stream.concat(
275                   effectTagStream.map(
276                       tag ->
277                           String.format(
278                               "<a href=\"#effect_tag_%s\"><code>%s</code></a>",
279                               tag, tag.name().toLowerCase())),
280                   metadataTagStream.map(
281                       tag ->
282                           String.format(
283                               "<a href=\"#metadata_tag_%s\"><code>%s</code></a>",
284                               tag, tag.name().toLowerCase())))
285               .collect(Collectors.joining(", "));
286       if (!tagList.isEmpty()) {
287         usage.append("<br>Tags: \n").append(tagList);
288       }
289     }
290 
291     usage.append("</dd>\n");
292   }
293 
294   /**
295    * Returns the available completion for the given option field. The completions are the exact
296    * command line option (with the prepending '--') that one should pass. It is suitable for
297    * completion script to use. If the option expect an argument, the kind of argument is given
298    * after the equals. If the kind is a enum, the various enum values are given inside an accolade
299    * in a comma separated list. For other special kind, the type is given as a name (e.g.,
300    * <code>label</code>, <code>float</ode>, <code>path</code>...). Example outputs of this
301    * function are for, respectively, a tristate flag <code>tristate_flag</code>, a enum
302    * flag <code>enum_flag</code> which can take <code>value1</code>, <code>value2</code> and
303    * <code>value3</code>, a path fragment flag <code>path_flag</code>, a string flag
304    * <code>string_flag</code> and a void flag <code>void_flag</code>:
305    * <pre>
306    *   --tristate_flag={auto,yes,no}
307    *   --notristate_flag
308    *   --enum_flag={value1,value2,value3}
309    *   --path_flag=path
310    *   --string_flag=
311    *   --void_flag
312    * </pre>
313    *
314    * @param optionDefinition The field to return completion for
315    * @param builder the string builder to store the completion values
316    */
getCompletion(OptionDefinition optionDefinition, StringBuilder builder)317   static void getCompletion(OptionDefinition optionDefinition, StringBuilder builder) {
318     // Return the list of possible completions for this option
319     String flagName = optionDefinition.getOptionName();
320     Class<?> fieldType = optionDefinition.getType();
321     builder.append("--").append(flagName);
322     if (fieldType.equals(boolean.class)) {
323       builder.append("\n");
324       builder.append("--no").append(flagName).append("\n");
325     } else if (fieldType.equals(TriState.class)) {
326       builder.append("={auto,yes,no}\n");
327       builder.append("--no").append(flagName).append("\n");
328     } else if (fieldType.isEnum()) {
329       builder
330           .append("={")
331           .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase(Locale.ENGLISH))
332           .append("}\n");
333     } else if (fieldType.getSimpleName().equals("Label")) {
334       // String comparison so we don't introduce a dependency to com.google.devtools.build.lib.
335       builder.append("=label\n");
336     } else if (fieldType.getSimpleName().equals("PathFragment")) {
337       builder.append("=path\n");
338     } else if (Void.class.isAssignableFrom(fieldType)) {
339       builder.append("\n");
340     } else {
341       // TODO(bazel-team): add more types. Maybe even move the completion type
342       // to the @Option annotation?
343       builder.append("=\n");
344     }
345   }
346 
getTypeDescription(OptionDefinition optionsDefinition)347   private static String getTypeDescription(OptionDefinition optionsDefinition) {
348     return optionsDefinition.getConverter().getTypeDescription();
349   }
350 
getFlagName(OptionDefinition optionDefinition)351   static String getFlagName(OptionDefinition optionDefinition) {
352     String name = optionDefinition.getOptionName();
353     return optionDefinition.usesBooleanValueSyntax() ? "[no]" + name : name;
354   }
355 }
356