356 lines
15 KiB
Java
356 lines
15 KiB
Java
|
// 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<? extends OptionsBase> optionsClass, StringBuilder usage) {
|
||
|
OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
|
||
|
List<OptionDefinition> 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<String> 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<String> 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<OptionEffectTag> effectTagStream =
|
||
|
Arrays.stream(optionDefinition.getOptionEffectTags())
|
||
|
.filter(OptionsUsage::shouldEffectTagBeListed);
|
||
|
Stream<OptionMetadataTag> 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("<dt><code><a name=\"flag--").append(plainFlagName).append("\"></a>--");
|
||
|
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("</code>");
|
||
|
if (optionDefinition.getAbbreviation() != '\0') {
|
||
|
usage.append(" [<code>-").append(optionDefinition.getAbbreviation()).append("</code>]");
|
||
|
}
|
||
|
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("</dt>\n");
|
||
|
usage.append("<dd>\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("<br/>\n");
|
||
|
ImmutableList<String> expansion = getExpansionIfKnown(optionDefinition, optionsData);
|
||
|
StringBuilder expandsMsg;
|
||
|
if (expansion == null) {
|
||
|
expandsMsg = new StringBuilder("Expands to unknown options.<br/>\n");
|
||
|
} else {
|
||
|
Preconditions.checkArgument(!expansion.isEmpty());
|
||
|
expandsMsg = new StringBuilder("Expands to:<br/>\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(" <code>")
|
||
|
.append(escaper.escape(exp))
|
||
|
.append("</code><br/>\n");
|
||
|
}
|
||
|
}
|
||
|
usage.append(expandsMsg.toString());
|
||
|
}
|
||
|
|
||
|
// Add effect tags, if not UNKNOWN, and metadata tags, if not empty.
|
||
|
if (includeTags) {
|
||
|
Stream<OptionEffectTag> effectTagStream =
|
||
|
Arrays.stream(optionDefinition.getOptionEffectTags())
|
||
|
.filter(OptionsUsage::shouldEffectTagBeListed);
|
||
|
Stream<OptionMetadataTag> metadataTagStream =
|
||
|
Arrays.stream(optionDefinition.getOptionMetadataTags())
|
||
|
.filter(OptionsUsage::shouldMetadataTagBeListed);
|
||
|
String tagList =
|
||
|
Stream.concat(
|
||
|
effectTagStream.map(
|
||
|
tag ->
|
||
|
String.format(
|
||
|
"<a href=\"#effect_tag_%s\"><code>%s</code></a>",
|
||
|
tag, tag.name().toLowerCase())),
|
||
|
metadataTagStream.map(
|
||
|
tag ->
|
||
|
String.format(
|
||
|
"<a href=\"#metadata_tag_%s\"><code>%s</code></a>",
|
||
|
tag, tag.name().toLowerCase())))
|
||
|
.collect(Collectors.joining(", "));
|
||
|
if (!tagList.isEmpty()) {
|
||
|
usage.append("<br>Tags: \n").append(tagList);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
usage.append("</dd>\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.,
|
||
|
* <code>label</code>, <code>float</ode>, <code>path</code>...). Example outputs of this
|
||
|
* function are for, respectively, a tristate flag <code>tristate_flag</code>, a enum
|
||
|
* flag <code>enum_flag</code> which can take <code>value1</code>, <code>value2</code> and
|
||
|
* <code>value3</code>, a path fragment flag <code>path_flag</code>, a string flag
|
||
|
* <code>string_flag</code> and a void flag <code>void_flag</code>:
|
||
|
* <pre>
|
||
|
* --tristate_flag={auto,yes,no}
|
||
|
* --notristate_flag
|
||
|
* --enum_flag={value1,value2,value3}
|
||
|
* --path_flag=path
|
||
|
* --string_flag=
|
||
|
* --void_flag
|
||
|
* </pre>
|
||
|
*
|
||
|
* @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;
|
||
|
}
|
||
|
}
|