diff options
author | Ulf Adams <ulfjack@google.com> | 2016-06-22 09:24:28 +0000 |
---|---|---|
committer | Philipp Wollermann <philwo@google.com> | 2016-06-22 10:49:24 +0000 |
commit | 352211d7662490ae87115f761aa4cc1e08142529 (patch) | |
tree | 5903f7334ee45db808cfb7eb370e4697ab10c0ec /src/main/java/com/google | |
parent | bb08e15859096be7a8c04fa54c21af6483febac6 (diff) |
The help command can now output html for the command-line reference page.
An upcoming change will pipe this to an actual page.
--
MOS_MIGRATED_REVID=125545220
Diffstat (limited to 'src/main/java/com/google')
4 files changed, 212 insertions, 16 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java index f7bcd1d224..722baea810 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandUtils.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; @@ -35,7 +34,7 @@ public class BlazeCommandUtils { /** * Options classes used as startup options in Blaze core. */ - private static final List<Class<? extends OptionsBase>> DEFAULT_STARTUP_OPTIONS = + private static final ImmutableList<Class<? extends OptionsBase>> DEFAULT_STARTUP_OPTIONS = ImmutableList.<Class<? extends OptionsBase>>of( BlazeServerStartupOptions.class, HostJvmStartupOptions.class); @@ -43,7 +42,7 @@ public class BlazeCommandUtils { /** * The set of option-classes that are common to all Blaze commands. */ - private static final Collection<Class<? extends OptionsBase>> COMMON_COMMAND_OPTIONS = + private static final ImmutableList<Class<? extends OptionsBase>> COMMON_COMMAND_OPTIONS = ImmutableList.of(CommonCommandOptions.class, BlazeCommandEventHandler.Options.class); @@ -60,6 +59,10 @@ public class BlazeCommandUtils { return ImmutableList.copyOf(options); } + public static ImmutableList<Class<? extends OptionsBase>> getCommonOptions() { + return COMMON_COMMAND_OPTIONS; + } + /** * Returns the set of all options (including those inherited directly and * transitively) for this AbstractCommand's @Command annotation. diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java index 8e11e08cde..ba199923a1 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/HelpCommand.java @@ -16,6 +16,10 @@ package com.google.devtools.build.lib.runtime.commands; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Iterables; +import com.google.common.escape.Escaper; +import com.google.common.html.HtmlEscapers; import com.google.devtools.build.docgen.BlazeRuleHelpPrinter; import com.google.devtools.build.lib.analysis.BlazeVersionInfo; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; @@ -29,6 +33,7 @@ import com.google.devtools.build.lib.runtime.BlazeRuntime; import com.google.devtools.build.lib.runtime.Command; import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.util.ExitCode; +import com.google.devtools.build.lib.util.StringUtil; import com.google.devtools.build.lib.util.io.OutErr; import com.google.devtools.common.options.Converters; import com.google.devtools.common.options.Option; @@ -38,7 +43,9 @@ import com.google.devtools.common.options.OptionsProvider; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -56,6 +63,12 @@ import java.util.Set; public final class HelpCommand implements BlazeCommand { private static final Joiner SPACE_JOINER = Joiner.on(" "); + /** + * Only to be used to escape the internal hard-coded help texts when outputting HTML from help, + * which don't pose a security risk. + */ + private static final Escaper HTML_ESCAPER = HtmlEscapers.htmlEscaper(); + public static class Options extends OptionsBase { @Option(name = "help_verbosity", @@ -85,7 +98,7 @@ public final class HelpCommand implements BlazeCommand { * Returns a map that maps option categories to descriptive help strings for categories that * are not part of the Bazel core. */ - private ImmutableMap<String, String> getOptionCategories(BlazeRuntime runtime) { + private static ImmutableMap<String, String> getOptionCategories(BlazeRuntime runtime) { ImmutableMap.Builder<String, String> optionCategoriesBuilder = ImmutableMap.builder(); String name = runtime.getProductName(); optionCategoriesBuilder @@ -141,7 +154,7 @@ public final class HelpCommand implements BlazeCommand { Options helpOptions = options.getOptions(Options.class); if (options.getResidue().isEmpty()) { emitBlazeVersionInfo(outErr, runtime.getProductName()); - emitGenericHelp(runtime, outErr); + emitGenericHelp(outErr, runtime); return ExitCode.SUCCESS; } if (options.getResidue().size() != 1) { @@ -151,7 +164,8 @@ public final class HelpCommand implements BlazeCommand { String helpSubject = options.getResidue().get(0); if (helpSubject.equals("startup_options")) { emitBlazeVersionInfo(outErr, runtime.getProductName()); - emitStartupOptions(outErr, helpOptions.helpVerbosity, runtime, getOptionCategories(runtime)); + emitStartupOptions( + outErr, helpOptions.helpVerbosity, runtime, getOptionCategories(runtime)); return ExitCode.SUCCESS; } else if (helpSubject.equals("target-syntax")) { emitBlazeVersionInfo(outErr, runtime.getProductName()); @@ -163,6 +177,9 @@ public final class HelpCommand implements BlazeCommand { } else if (helpSubject.equals("completion")) { emitCompletionHelp(runtime, outErr); return ExitCode.SUCCESS; + } else if (helpSubject.equals("everything-as-html")) { + new HtmlEmitter(runtime).emit(outErr); + return ExitCode.SUCCESS; } BlazeCommand command = runtime.getCommandMap().get(helpSubject); @@ -196,7 +213,6 @@ public final class HelpCommand implements BlazeCommand { outErr.printOut(String.format("%80s\n", line)); } - @SuppressWarnings("unchecked") // varargs generic array creation private void emitStartupOptions(OutErr outErr, OptionsParser.HelpVerbosity helpVerbosity, BlazeRuntime runtime, ImmutableMap<String, String> optionCategories) { outErr.printOut( @@ -213,10 +229,9 @@ public final class HelpCommand implements BlazeCommand { // First startup_options Iterable<BlazeModule> blazeModules = runtime.getBlazeModules(); ConfiguredRuleClassProvider ruleClassProvider = runtime.getRuleClassProvider(); - Map<String, BlazeCommand> commandsByName = runtime.getCommandMap(); - Set<String> commands = commandsByName.keySet(); + Map<String, BlazeCommand> commandsByName = getSortedCommands(runtime); - outErr.printOutLn("BAZEL_COMMAND_LIST=\"" + SPACE_JOINER.join(commands) + "\""); + outErr.printOutLn("BAZEL_COMMAND_LIST=\"" + SPACE_JOINER.join(commandsByName.keySet()) + "\""); outErr.printOutLn("BAZEL_INFO_KEYS=\""); for (String name : InfoCommand.getHardwiredInfoItemNames(runtime.getProductName())) { @@ -230,9 +245,9 @@ public final class HelpCommand implements BlazeCommand { outErr.printOut(OptionsParser.newOptionsParser(options).getOptionsCompletion()); outErr.printOutLn("\""); - for (String name : commands) { - BlazeCommand command = commandsByName.get(name); - String varName = name.toUpperCase().replace('-', '_'); + for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) { + BlazeCommand command = e.getValue(); + String varName = e.getKey().toUpperCase(Locale.US).replace('-', '_'); Command annotation = command.getClass().getAnnotation(Command.class); if (!annotation.completion().isEmpty()) { outErr.printOutLn("BAZEL_COMMAND_" + varName + "_ARGUMENT=\"" @@ -245,6 +260,10 @@ public final class HelpCommand implements BlazeCommand { } } + private static Map<String, BlazeCommand> getSortedCommands(BlazeRuntime runtime) { + return ImmutableSortedMap.copyOf(runtime.getCommandMap()); + } + private void emitTargetSyntaxHelp(OutErr outErr, ImmutableMap<String, String> optionCategories, String productName) { outErr.printOut(BlazeCommandUtils.expandHelpTopic("target-syntax", @@ -264,10 +283,9 @@ public final class HelpCommand implements BlazeCommand { } } - private void emitGenericHelp(BlazeRuntime runtime, OutErr outErr) { + private void emitGenericHelp(OutErr outErr, BlazeRuntime runtime) { outErr.printOut(String.format("Usage: %s <command> <options> ...\n\n", runtime.getProductName())); - outErr.printOut("Available commands:\n"); Map<String, BlazeCommand> commandsByName = runtime.getCommandMap(); @@ -298,4 +316,87 @@ public final class HelpCommand implements BlazeCommand { outErr.printOut(String.format(" %s help info-keys\n", runtime.getProductName())); outErr.printOut(" Displays a list of keys used by the info command.\n"); } + + private static final class HtmlEmitter { + private final BlazeRuntime runtime; + private final ImmutableMap<String, String> optionCategories; + + private HtmlEmitter(BlazeRuntime runtime) { + this.runtime = runtime; + this.optionCategories = getOptionCategories(runtime); + } + + private void emit(OutErr outErr) { + Map<String, BlazeCommand> commandsByName = getSortedCommands(runtime); + StringBuilder result = new StringBuilder(); + result.append("<h2>Commands</h2>\n"); + result.append("<table>\n"); + for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) { + BlazeCommand command = e.getValue(); + Command annotation = command.getClass().getAnnotation(Command.class); + if (annotation.hidden()) { + continue; + } + String shortDescription = annotation.shortDescription(). + replace("%{product}", runtime.getProductName()); + + result.append("<tr>\n"); + result.append( + String.format( + " <td><a href=\"#%s\"><code>%s</code></a></td>\n", e.getKey(), e.getKey())); + result.append(" <td>").append(HTML_ESCAPER.escape(shortDescription)).append("</td>\n"); + result.append("</tr>\n"); + } + result.append("</table>\n"); + result.append("\n"); + + result.append("<h2>Startup Options</h2>\n"); + appendOptionsHtml(result, BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules())); + result.append("\n"); + + result.append("<h2><a name=\"common_options\">Options Common to all Commands</a></h2>\n"); + appendOptionsHtml(result, BlazeCommandUtils.getCommonOptions()); + result.append("\n"); + + for (Map.Entry<String, BlazeCommand> e : commandsByName.entrySet()) { + result.append( + String.format( + "<h2><a name=\"%s\">%s Options</a></h2>\n", e.getKey(), capitalize(e.getKey()))); + BlazeCommand command = e.getValue(); + Command annotation = command.getClass().getAnnotation(Command.class); + if (annotation.hidden()) { + continue; + } + List<String> inheritedCmdNames = new ArrayList<>(); + for (Class<? extends BlazeCommand> base : annotation.inherits()) { + String name = base.getAnnotation(Command.class).name(); + inheritedCmdNames.add(String.format("<a href=\"#%s\">%s</a>", name, name)); + } + if (!inheritedCmdNames.isEmpty()) { + result.append("<p>Inherits all options from "); + result.append(StringUtil.joinEnglishList(inheritedCmdNames, "and")); + result.append(".</p>\n\n"); + } + Set<Class<? extends OptionsBase>> options = new HashSet<>(); + Collections.addAll(options, annotation.options()); + for (BlazeModule blazeModule : runtime.getBlazeModules()) { + Iterables.addAll(options, blazeModule.getCommandOptions(annotation)); + } + appendOptionsHtml(result, options); + result.append("\n"); + } + outErr.printOut(result.toString()); + } + + private void appendOptionsHtml( + StringBuilder result, Iterable<Class<? extends OptionsBase>> optionsClasses) { + OptionsParser parser = OptionsParser.newOptionsParser(optionsClasses); + result.append(parser.describeOptionsHtml(optionCategories, HTML_ESCAPER) + .replace("%{product}", runtime.getProductName())); + } + + private static String capitalize(String s) { + return s.substring(0, 1).toUpperCase(Locale.US) + s.substring(1); + } + } } diff --git a/src/main/java/com/google/devtools/common/options/OptionsParser.java b/src/main/java/com/google/devtools/common/options/OptionsParser.java index 83a7b08b3a..301f2d49be 100644 --- a/src/main/java/com/google/devtools/common/options/OptionsParser.java +++ b/src/main/java/com/google/devtools/common/options/OptionsParser.java @@ -21,6 +21,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.escape.Escaper; import java.lang.reflect.Field; import java.util.ArrayList; @@ -436,7 +437,6 @@ public class OptionsParser implements OptionsProvider { HelpVerbosity helpVerbosity) { StringBuilder desc = new StringBuilder(); if (!impl.getOptionsClasses().isEmpty()) { - List<Field> allFields = Lists.newArrayList(); for (Class<? extends OptionsBase> optionsClass : impl.getOptionsClasses()) { allFields.addAll(impl.getAnnotatedFieldsFor(optionsClass)); @@ -466,6 +466,52 @@ public class OptionsParser implements OptionsProvider { } /** + * Returns a description of all the options this parser can digest. + * In addition to {@link Option} annotations, this method also + * interprets {@link OptionsUsage} annotations which give an intuitive short + * description for the options. + * + * @param categoryDescriptions a mapping from category names to category + * descriptions. Options of the same category (see {@link + * Option#category}) will be grouped together, preceded by the description + * of the category. + */ + public String describeOptionsHtml(Map<String, String> categoryDescriptions, Escaper escaper) { + StringBuilder desc = new StringBuilder(); + if (!impl.getOptionsClasses().isEmpty()) { + List<Field> allFields = Lists.newArrayList(); + for (Class<? extends OptionsBase> optionsClass : impl.getOptionsClasses()) { + allFields.addAll(impl.getAnnotatedFieldsFor(optionsClass)); + } + Collections.sort(allFields, OptionsUsage.BY_CATEGORY); + String prevCategory = null; + + for (Field optionField : allFields) { + String category = optionField.getAnnotation(Option.class).category(); + DocumentationLevel level = documentationLevel(category); + if (!category.equals(prevCategory) && level == DocumentationLevel.DOCUMENTED) { + String description = categoryDescriptions.get(category); + if (description == null) { + description = "Options category '" + category + "'"; + } + if (prevCategory != null) { + desc.append("</dl>\n\n"); + } + desc.append(escaper.escape(description)).append(":\n"); + desc.append("<dl>"); + prevCategory = category; + } + + if (level == DocumentationLevel.DOCUMENTED) { + OptionsUsage.getUsageHtml(optionField, desc, escaper); + } + } + desc.append("</dl>\n"); + } + return desc.toString(); + } + + /** * Returns a string listing the possible flag completion for this command along with the command * completion if any. See {@link OptionsUsage#getCompletion(Field, StringBuilder)} for more * details on the format for the flag completion. diff --git a/src/main/java/com/google/devtools/common/options/OptionsUsage.java b/src/main/java/com/google/devtools/common/options/OptionsUsage.java index e79a062ce4..b64506dd1e 100644 --- a/src/main/java/com/google/devtools/common/options/OptionsUsage.java +++ b/src/main/java/com/google/devtools/common/options/OptionsUsage.java @@ -19,6 +19,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Lists; +import com.google.common.escape.Escaper; import java.lang.reflect.Field; import java.text.BreakIterator; @@ -127,6 +128,51 @@ class OptionsUsage { } /** + * Append the usage message for a single option-field message to 'usage'. + */ + static void getUsageHtml(Field optionField, StringBuilder usage, Escaper escaper) { + String flagName = getFlagName(optionField); + String typeDescription = getTypeDescription(optionField); + Option annotation = optionField.getAnnotation(Option.class); + usage.append("<dt><code>--").append(flagName).append("</code>"); + if (annotation.abbrev() != '\0') { + usage.append(" [<code>-").append(annotation.abbrev()).append("</code>]"); + } + if (!typeDescription.isEmpty()) { + usage.append(" (").append(escaper.escape(typeDescription)).append("; "); + if (annotation.allowMultiple()) { + // Allow-multiple options can't have a default value. + usage.append("may be used multiple times"); + } else { + // Don't call the annotation directly (we must allow overrides to certain defaults). + String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); + if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) { + usage.append("default: see description"); + } else { + usage.append("default: \"").append(escaper.escape(defaultValueString)).append("\""); + } + } + usage.append(")"); + } + usage.append("</dt>\n"); + usage.append("<dd>\n"); + if (!annotation.help().isEmpty()) { + usage.append(paragraphFill(escaper.escape(annotation.help()), 0, 80)); // (indent, width) + usage.append('\n'); + } + if (annotation.expansion().length > 0) { + usage.append("<br/>\n"); + StringBuilder expandsMsg = new StringBuilder("Expands to:"); + for (String exp : annotation.expansion()) { + expandsMsg.append(" ").append(exp); + } + usage.append(paragraphFill(escaper.escape(expandsMsg.toString()), 0, 80)); // (indent, width) + usage.append('\n'); + } + 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 |