Home | History | Annotate | Download | only in options
      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    */
     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    */
     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    */
     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.
     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.
    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'. */
    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'. */
    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    */
    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 
    347   private static String getTypeDescription(OptionDefinition optionsDefinition) {
    348     return optionsDefinition.getConverter().getTypeDescription();
    349   }
    350 
    351   static String getFlagName(OptionDefinition optionDefinition) {
    352     String name = optionDefinition.getOptionName();
    353     return optionDefinition.usesBooleanValueSyntax() ? "[no]" + name : name;
    354   }
    355 }
    356