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("=<").append(escaper.escape(typeDescription)).append(">"); 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(" <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