// Copyright 2014 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.common.options; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.escape.Escaper; import java.text.BreakIterator; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.Nullable; /** A renderer for usage messages for any combination of options classes. */ class OptionsUsage { private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n'); private static final Joiner COMMA_JOINER = Joiner.on(","); /** * Given an options class, render the usage string into the usage, which is passed in as an * argument. This will not include information about expansions for options using expansion * functions (it would be unsafe to report this as we cannot know what options from other {@link * OptionsBase} subclasses they depend on until a complete parser is constructed). */ static void getUsage(Class optionsClass, StringBuilder usage) { OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass); List optionDefinitions = new ArrayList<>(OptionsData.getAllOptionDefinitionsForClass(optionsClass)); optionDefinitions.sort(OptionDefinition.BY_OPTION_NAME); for (OptionDefinition optionDefinition : optionDefinitions) { getUsage(optionDefinition, usage, OptionsParser.HelpVerbosity.LONG, data, false); } } /** * Paragraph-fill the specified input text, indenting lines to 'indent' and * wrapping lines at 'width'. Returns the formatted result. */ static String paragraphFill(String in, int indent, int width) { String indentString = Strings.repeat(" ", indent); StringBuilder out = new StringBuilder(); String sep = ""; for (String paragraph : NEWLINE_SPLITTER.split(in)) { // TODO(ccalvarin) break iterators expect hyphenated words to be line-breakable, which looks // funny for --flag BreakIterator boundary = BreakIterator.getLineInstance(); // (factory) boundary.setText(paragraph); out.append(sep).append(indentString); int cursor = indent; for (int start = boundary.first(), end = boundary.next(); end != BreakIterator.DONE; start = end, end = boundary.next()) { String word = paragraph.substring(start, end); // (may include trailing space) if (word.length() + cursor > width) { out.append('\n').append(indentString); cursor = indent; } out.append(word); cursor += word.length(); } sep = "\n"; } return out.toString(); } /** * Returns the expansion for an option, if any, regardless of if the expansion is from a function * or is statically declared in the annotation. */ private static @Nullable ImmutableList getExpansionIfKnown( OptionDefinition optionDefinition, OptionsData optionsData) { Preconditions.checkNotNull(optionDefinition); return optionsData.getEvaluatedExpansion(optionDefinition); } // Placeholder tag "UNKNOWN" is ignored. private static boolean shouldEffectTagBeListed(OptionEffectTag effectTag) { return !effectTag.equals(OptionEffectTag.UNKNOWN); } // Tags that only apply to undocumented options are excluded. private static boolean shouldMetadataTagBeListed(OptionMetadataTag metadataTag) { return !metadataTag.equals(OptionMetadataTag.HIDDEN) && !metadataTag.equals(OptionMetadataTag.INTERNAL); } /** Appends the usage message for a single option-field message to 'usage'. */ static void getUsage( OptionDefinition optionDefinition, StringBuilder usage, OptionsParser.HelpVerbosity helpVerbosity, OptionsData optionsData, boolean includeTags) { String flagName = getFlagName(optionDefinition); String typeDescription = getTypeDescription(optionDefinition); usage.append(" --").append(flagName); if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) { usage.append('\n'); return; } // Add the option's type and default information. Stop there for "medium" verbosity. if (optionDefinition.getAbbreviation() != '\0') { usage.append(" [-").append(optionDefinition.getAbbreviation()).append(']'); } if (!typeDescription.equals("")) { usage.append(" (").append(typeDescription).append("; "); if (optionDefinition.allowsMultiple()) { usage.append("may be used multiple times"); } else { // Don't call the annotation directly (we must allow overrides to certain defaults) String defaultValueString = optionDefinition.getUnparsedDefaultValue(); if (optionDefinition.isSpecialNullDefault()) { usage.append("default: see description"); } else { usage.append("default: \"").append(defaultValueString).append("\""); } } usage.append(")"); } usage.append("\n"); if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) { return; } // For verbosity "long," add the full description and expansion, along with the tag // information if requested. if (!optionDefinition.getHelpText().isEmpty()) { usage.append(paragraphFill(optionDefinition.getHelpText(), /*indent=*/ 4, /*width=*/ 80)); usage.append('\n'); } ImmutableList expansion = getExpansionIfKnown(optionDefinition, optionsData); if (expansion == null) { usage.append(paragraphFill("Expands to unknown options.", /*indent=*/ 6, /*width=*/ 80)); usage.append('\n'); } else if (!expansion.isEmpty()) { StringBuilder expandsMsg = new StringBuilder("Expands to: "); for (String exp : expansion) { expandsMsg.append(exp).append(" "); } usage.append(paragraphFill(expandsMsg.toString(), /*indent=*/ 6, /*width=*/ 80)); usage.append('\n'); } if (optionDefinition.hasImplicitRequirements()) { StringBuilder requiredMsg = new StringBuilder("Using this option will also add: "); for (String req : optionDefinition.getImplicitRequirements()) { requiredMsg.append(req).append(" "); } usage.append(paragraphFill(requiredMsg.toString(), 6, 80)); // (indent, width) usage.append('\n'); } if (!includeTags) { return; } // If we are expected to include the tags, add them for high verbosity. Stream effectTagStream = Arrays.stream(optionDefinition.getOptionEffectTags()) .filter(OptionsUsage::shouldEffectTagBeListed); Stream metadataTagStream = Arrays.stream(optionDefinition.getOptionMetadataTags()) .filter(OptionsUsage::shouldMetadataTagBeListed); String tagList = Stream.concat(effectTagStream, metadataTagStream) .map(tag -> tag.toString().toLowerCase()) .collect(Collectors.joining(", ")); if (!tagList.isEmpty()) { usage.append(paragraphFill("Tags: " + tagList, 6, 80)); // (indent, width) usage.append("\n"); } } /** Append the usage message for a single option-field message to 'usage'. */ static void getUsageHtml( OptionDefinition optionDefinition, StringBuilder usage, Escaper escaper, OptionsData optionsData, boolean includeTags) { String plainFlagName = optionDefinition.getOptionName(); String flagName = getFlagName(optionDefinition); String valueDescription = optionDefinition.getValueTypeHelpText(); String typeDescription = getTypeDescription(optionDefinition); usage.append("
--"); usage.append(flagName); if (optionDefinition.usesBooleanValueSyntax() || optionDefinition.isVoidField()) { // Nothing for boolean, tristate, boolean_or_enum, or void options. } else if (!valueDescription.isEmpty()) { usage.append("=").append(escaper.escape(valueDescription)); } else if (!typeDescription.isEmpty()) { // Generic fallback, which isn't very good. usage.append("=<").append(escaper.escape(typeDescription)).append(">"); } usage.append(""); if (optionDefinition.getAbbreviation() != '\0') { usage.append(" [-").append(optionDefinition.getAbbreviation()).append("]"); } if (optionDefinition.allowsMultiple()) { // Allow-multiple options can't have a default value. usage.append(" multiple uses are accumulated"); } else { // Don't call the annotation directly (we must allow overrides to certain defaults). String defaultValueString = optionDefinition.getUnparsedDefaultValue(); if (optionDefinition.isVoidField()) { // Void options don't have a default. } else if (optionDefinition.isSpecialNullDefault()) { usage.append(" default: see description"); } else { usage.append(" default: \"").append(escaper.escape(defaultValueString)).append("\""); } } usage.append("
\n"); usage.append("
\n"); if (!optionDefinition.getHelpText().isEmpty()) { usage.append( paragraphFill( escaper.escape(optionDefinition.getHelpText()), /*indent=*/ 0, /*width=*/ 80)); usage.append('\n'); } if (!optionsData.getEvaluatedExpansion(optionDefinition).isEmpty()) { // If this is an expansion option, list the expansion if known, or at least specify that we // don't know. usage.append("
\n"); ImmutableList expansion = getExpansionIfKnown(optionDefinition, optionsData); StringBuilder expandsMsg; if (expansion == null) { expandsMsg = new StringBuilder("Expands to unknown options.
\n"); } else { Preconditions.checkArgument(!expansion.isEmpty()); expandsMsg = new StringBuilder("Expands to:
\n"); for (String exp : expansion) { // TODO(ulfjack): We should link to the expanded flags, but unfortunately we don't // currently guarantee that all flags are only printed once. A flag in an OptionBase that // is included by 2 different commands, but not inherited through a parent command, will // be printed multiple times. expandsMsg .append("  ") .append(escaper.escape(exp)) .append("
\n"); } } usage.append(expandsMsg.toString()); } // Add effect tags, if not UNKNOWN, and metadata tags, if not empty. if (includeTags) { Stream effectTagStream = Arrays.stream(optionDefinition.getOptionEffectTags()) .filter(OptionsUsage::shouldEffectTagBeListed); Stream metadataTagStream = Arrays.stream(optionDefinition.getOptionMetadataTags()) .filter(OptionsUsage::shouldMetadataTagBeListed); String tagList = Stream.concat( effectTagStream.map( tag -> String.format( "%s", tag, tag.name().toLowerCase())), metadataTagStream.map( tag -> String.format( "%s", tag, tag.name().toLowerCase()))) .collect(Collectors.joining(", ")); if (!tagList.isEmpty()) { usage.append("
Tags: \n").append(tagList); } } usage.append("
\n"); } /** * Returns the available completion for the given option field. The completions are the exact * command line option (with the prepending '--') that one should pass. It is suitable for * completion script to use. If the option expect an argument, the kind of argument is given * after the equals. If the kind is a enum, the various enum values are given inside an accolade * in a comma separated list. For other special kind, the type is given as a name (e.g., * label, float, path...). Example outputs of this * function are for, respectively, a tristate flag tristate_flag, a enum * flag enum_flag which can take value1, value2 and * value3, a path fragment flag path_flag, a string flag * string_flag and a void flag void_flag: *
   *   --tristate_flag={auto,yes,no}
   *   --notristate_flag
   *   --enum_flag={value1,value2,value3}
   *   --path_flag=path
   *   --string_flag=
   *   --void_flag
   * 
* * @param optionDefinition The field to return completion for * @param builder the string builder to store the completion values */ static void getCompletion(OptionDefinition optionDefinition, StringBuilder builder) { // Return the list of possible completions for this option String flagName = optionDefinition.getOptionName(); Class fieldType = optionDefinition.getType(); builder.append("--").append(flagName); if (fieldType.equals(boolean.class)) { builder.append("\n"); builder.append("--no").append(flagName).append("\n"); } else if (fieldType.equals(TriState.class)) { builder.append("={auto,yes,no}\n"); builder.append("--no").append(flagName).append("\n"); } else if (fieldType.isEnum()) { builder .append("={") .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase(Locale.ENGLISH)) .append("}\n"); } else if (fieldType.getSimpleName().equals("Label")) { // String comparison so we don't introduce a dependency to com.google.devtools.build.lib. builder.append("=label\n"); } else if (fieldType.getSimpleName().equals("PathFragment")) { builder.append("=path\n"); } else if (Void.class.isAssignableFrom(fieldType)) { builder.append("\n"); } else { // TODO(bazel-team): add more types. Maybe even move the completion type // to the @Option annotation? builder.append("=\n"); } } private static String getTypeDescription(OptionDefinition optionsDefinition) { return optionsDefinition.getConverter().getTypeDescription(); } static String getFlagName(OptionDefinition optionDefinition) { String name = optionDefinition.getOptionName(); return optionDefinition.usesBooleanValueSyntax() ? "[no]" + name : name; } }