// 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.build.lib.runtime.commands; import com.google.common.base.CaseFormat; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; 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; import com.google.devtools.build.lib.analysis.NoBuildEvent; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.packages.RuleClass; import com.google.devtools.build.lib.runtime.BlazeCommand; import com.google.devtools.build.lib.runtime.BlazeCommandResult; import com.google.devtools.build.lib.runtime.BlazeCommandUtils; import com.google.devtools.build.lib.runtime.BlazeModule; 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.runtime.commands.proto.BazelFlagsProto; 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; import com.google.devtools.common.options.OptionDefinition; import com.google.devtools.common.options.OptionDocumentationCategory; import com.google.devtools.common.options.OptionEffectTag; import com.google.devtools.common.options.OptionFilterDescriptions; import com.google.devtools.common.options.OptionMetadataTag; import com.google.devtools.common.options.OptionsBase; import com.google.devtools.common.options.OptionsParser; import com.google.devtools.common.options.OptionsParser.HelpVerbosity; import com.google.devtools.common.options.OptionsProvider; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; /** The 'blaze help' command, which prints all available commands as well as specific help pages. */ @Command( name = "help", options = {HelpCommand.Options.class}, allowResidue = true, mustRunInWorkspace = false, shortDescription = "Prints help for commands, or the index.", completion = "command|{startup_options,target-syntax,info-keys}", help = "resource:help.txt" ) 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", defaultValue = "medium", converter = Converters.HelpVerbosityConverter.class, documentationCategory = OptionDocumentationCategory.LOGGING, effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT}, help = "Select the verbosity of the help command." ) public OptionsParser.HelpVerbosity helpVerbosity; @Option( name = "long", abbrev = 'l', defaultValue = "null", expansion = {"--help_verbosity=long"}, documentationCategory = OptionDocumentationCategory.LOGGING, effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT}, help = "Show full description of each option, instead of just its name." ) public Void showLongFormOptions; @Option( name = "short", defaultValue = "null", expansion = {"--help_verbosity=short"}, documentationCategory = OptionDocumentationCategory.LOGGING, effectTags = {OptionEffectTag.AFFECTS_OUTPUTS, OptionEffectTag.TERMINAL_OUTPUT}, help = "Show only the names of the options, not their types or meanings." ) public Void showShortFormOptions; } @Override public void editOptions(OptionsParser optionsParser) {} @Override public BlazeCommandResult exec(CommandEnvironment env, OptionsProvider options) { env.getEventBus().post(new NoBuildEvent()); BlazeRuntime runtime = env.getRuntime(); OutErr outErr = env.getReporter().getOutErr(); Options helpOptions = options.getOptions(Options.class); if (options.getResidue().isEmpty()) { emitBlazeVersionInfo(outErr, runtime.getProductName()); emitGenericHelp(outErr, runtime); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); } if (options.getResidue().size() != 1) { env.getReporter().handle(Event.error("You must specify exactly one command")); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); } String helpSubject = options.getResidue().get(0); String productName = runtime.getProductName(); // Go through the custom subjects before going through Bazel commands. switch (helpSubject) { case "startup_options": emitBlazeVersionInfo(outErr, runtime.getProductName()); emitStartupOptions(outErr, helpOptions.helpVerbosity, runtime); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); case "target-syntax": emitBlazeVersionInfo(outErr, runtime.getProductName()); emitTargetSyntaxHelp(outErr, productName); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); case "info-keys": emitInfoKeysHelp(env, outErr); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); case "completion": emitCompletionHelp(runtime, outErr); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); case "flags-as-proto": emitFlagsAsProtoHelp(runtime, outErr); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); case "everything-as-html": new HtmlEmitter(runtime).emit(outErr); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); default: // fall out } BlazeCommand command = runtime.getCommandMap().get(helpSubject); if (command == null) { ConfiguredRuleClassProvider provider = runtime.getRuleClassProvider(); RuleClass ruleClass = provider.getRuleClassMap().get(helpSubject); if (ruleClass != null && ruleClass.isDocumented()) { // There is a rule with a corresponding name outErr.printOut( BlazeRuleHelpPrinter.getRuleDoc(helpSubject, runtime.getProductName(), provider)); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); } else { env.getReporter().handle(Event.error( null, "'" + helpSubject + "' is neither a command nor a build rule")); return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR); } } emitBlazeVersionInfo(outErr, productName); outErr.printOut( BlazeCommandUtils.getUsage( command.getClass(), helpOptions.helpVerbosity, runtime.getBlazeModules(), runtime.getRuleClassProvider(), productName)); return BlazeCommandResult.exitCode(ExitCode.SUCCESS); } private void emitBlazeVersionInfo(OutErr outErr, String productName) { String releaseInfo = BlazeVersionInfo.instance().getReleaseName(); String line = String.format("[%s %s]", productName, releaseInfo); outErr.printOut(String.format("%80s\n", line)); } private void emitStartupOptions( OutErr outErr, HelpVerbosity helpVerbosity, BlazeRuntime runtime) { outErr.printOut( BlazeCommandUtils.expandHelpTopic( "startup_options", "resource:startup_options.txt", getClass(), BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules()), helpVerbosity, runtime.getProductName())); } private void emitCompletionHelp(BlazeRuntime runtime, OutErr outErr) { Map commandsByName = getSortedCommands(runtime); outErr.printOutLn("BAZEL_COMMAND_LIST=\"" + SPACE_JOINER.join(commandsByName.keySet()) + "\""); outErr.printOutLn("BAZEL_INFO_KEYS=\""); for (String name : InfoCommand.getHardwiredInfoItemNames(runtime.getProductName())) { outErr.printOutLn(name); } outErr.printOutLn("\""); Consumer startupOptionVisitor = parser -> { outErr.printOutLn("BAZEL_STARTUP_OPTIONS=\""); outErr.printOut(parser.getOptionsCompletion()); outErr.printOutLn("\""); }; CommandOptionVisitor commandOptionVisitor = (commandName, commandAnnotation, parser) -> { String varName = CaseFormat.LOWER_HYPHEN.to(CaseFormat.UPPER_UNDERSCORE, commandName); if (!Strings.isNullOrEmpty(commandAnnotation.completion())) { outErr.printOutLn( "BAZEL_COMMAND_" + varName + "_ARGUMENT=\"" + commandAnnotation.completion() + "\""); } outErr.printOutLn("BAZEL_COMMAND_" + varName + "_FLAGS=\""); outErr.printOut(parser.getOptionsCompletion()); outErr.printOutLn("\""); }; visitAllOptions(runtime, startupOptionVisitor, commandOptionVisitor); } private void emitFlagsAsProtoHelp(BlazeRuntime runtime, OutErr outErr) { Map flags = new HashMap<>(); Predicate allOptions = option -> true; BiConsumer visitor = (commandName, option) -> { if (ImmutableSet.copyOf(option.getOptionMetadataTags()) .contains(OptionMetadataTag.INTERNAL)) { return; } BazelFlagsProto.FlagInfo.Builder info = flags.computeIfAbsent(option.getOptionName(), key -> createFlagInfo(option)); info.addCommands(commandName); }; Consumer startupOptionVisitor = parser -> { parser.visitOptions(allOptions, option -> visitor.accept("startup", option)); }; CommandOptionVisitor commandOptionVisitor = (commandName, commandAnnotation, parser) -> { parser.visitOptions(allOptions, option -> visitor.accept(commandName, option)); }; visitAllOptions(runtime, startupOptionVisitor, commandOptionVisitor); BazelFlagsProto.FlagCollection.Builder collectionBuilder = BazelFlagsProto.FlagCollection.newBuilder(); for (BazelFlagsProto.FlagInfo.Builder info : flags.values()) { collectionBuilder.addFlagInfos(info); } outErr.printOut(Base64.getEncoder().encodeToString(collectionBuilder.build().toByteArray())); } private BazelFlagsProto.FlagInfo.Builder createFlagInfo(OptionDefinition option) { BazelFlagsProto.FlagInfo.Builder flagBuilder = BazelFlagsProto.FlagInfo.newBuilder(); flagBuilder.setName(option.getOptionName()); flagBuilder.setHasNegativeFlag(option.hasNegativeOption()); flagBuilder.setDocumentation(option.getHelpText()); return flagBuilder; } private void visitAllOptions( BlazeRuntime runtime, Consumer startupOptionVisitor, CommandOptionVisitor commandOptionVisitor) { // First startup_options Iterable blazeModules = runtime.getBlazeModules(); ConfiguredRuleClassProvider ruleClassProvider = runtime.getRuleClassProvider(); Map commandsByName = getSortedCommands(runtime); Iterable> options = BlazeCommandUtils.getStartupOptions(blazeModules); startupOptionVisitor.accept(OptionsParser.newOptionsParser(options)); for (Map.Entry e : commandsByName.entrySet()) { BlazeCommand command = e.getValue(); Command annotation = command.getClass().getAnnotation(Command.class); options = BlazeCommandUtils.getOptions(command.getClass(), blazeModules, ruleClassProvider); commandOptionVisitor.visit(e.getKey(), annotation, OptionsParser.newOptionsParser(options)); } } private static Map getSortedCommands(BlazeRuntime runtime) { return ImmutableSortedMap.copyOf(runtime.getCommandMap()); } private void emitTargetSyntaxHelp(OutErr outErr, String productName) { outErr.printOut( BlazeCommandUtils.expandHelpTopic( "target-syntax", "resource:target-syntax.txt", getClass(), ImmutableList.>of(), OptionsParser.HelpVerbosity.MEDIUM, productName)); } private void emitInfoKeysHelp(CommandEnvironment env, OutErr outErr) { for (InfoItem item : InfoCommand.getInfoItemMap(env, OptionsParser.newOptionsParser( ImmutableList.>of())).values()) { outErr.printOut(String.format("%-23s %s\n", item.getName(), item.getDescription())); } } private void emitGenericHelp(OutErr outErr, BlazeRuntime runtime) { outErr.printOut(String.format("Usage: %s ...\n\n", runtime.getProductName())); outErr.printOut("Available commands:\n"); Map commandsByName = runtime.getCommandMap(); List namesInOrder = new ArrayList<>(commandsByName.keySet()); Collections.sort(namesInOrder); for (String name : namesInOrder) { BlazeCommand command = commandsByName.get(name); Command annotation = command.getClass().getAnnotation(Command.class); if (annotation.hidden()) { continue; } String shortDescription = annotation.shortDescription(). replace("%{product}", runtime.getProductName()); outErr.printOut(String.format(" %-19s %s\n", name, shortDescription)); } outErr.printOut("\n"); outErr.printOut("Getting more help:\n"); outErr.printOut(String.format(" %s help \n", runtime.getProductName())); outErr.printOut(" Prints help and options for .\n"); outErr.printOut(String.format(" %s help startup_options\n", runtime.getProductName())); outErr.printOut(String.format(" Options for the JVM hosting %s.\n", runtime.getProductName())); outErr.printOut(String.format(" %s help target-syntax\n", runtime.getProductName())); outErr.printOut(" Explains the syntax for specifying targets.\n"); 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 HtmlEmitter(BlazeRuntime runtime) { this.runtime = runtime; } private void emit(OutErr outErr) { Map commandsByName = getSortedCommands(runtime); StringBuilder result = new StringBuilder(); result.append("

Commands

\n"); result.append("\n"); for (Map.Entry 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("\n"); result.append( String.format( " \n", e.getKey(), e.getKey())); result.append(" \n"); result.append("\n"); } result.append("
%s").append(HTML_ESCAPER.escape(shortDescription)).append("
\n"); result.append("\n"); result.append("

Startup Options

\n"); appendOptionsHtml(result, BlazeCommandUtils.getStartupOptions(runtime.getBlazeModules())); result.append("\n"); result.append("

Options Common to all Commands

\n"); appendOptionsHtml(result, BlazeCommandUtils.getCommonOptions(runtime.getBlazeModules())); result.append("\n"); for (Map.Entry e : commandsByName.entrySet()) { result.append( String.format( "

%s Options

\n", e.getKey(), capitalize(e.getKey()))); BlazeCommand command = e.getValue(); Command annotation = command.getClass().getAnnotation(Command.class); if (annotation.hidden()) { continue; } List inheritedCmdNames = new ArrayList<>(); for (Class base : annotation.inherits()) { String name = base.getAnnotation(Command.class).name(); inheritedCmdNames.add(String.format("%s", name, name)); } if (!inheritedCmdNames.isEmpty()) { result.append("

Inherits all options from "); result.append(StringUtil.joinEnglishList(inheritedCmdNames, "and")); result.append(".

\n\n"); } Set> 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"); // For now, we print all the configuration options in a list after all the non-configuration // options. Note that usesConfigurationOptions is only true for the build command right now. if (annotation.usesConfigurationOptions()) { options.clear(); Collections.addAll(options, annotation.options()); if (annotation.usesConfigurationOptions()) { options.addAll(runtime.getRuleClassProvider().getConfigurationOptions()); } appendOptionsHtml(result, options); result.append("\n"); } } // Describe the tags once, any mentions above should link to these descriptions. String productName = runtime.getProductName(); ImmutableMap effectTagDescriptions = OptionFilterDescriptions.getOptionEffectTagDescription(productName); result.append("

Option Effect Tags

\n"); result.append("\n"); for (OptionEffectTag tag : OptionEffectTag.values()) { String tagDescription = effectTagDescriptions.get(tag); result.append("\n"); result.append( String.format( "\n", tag, tag.name().toLowerCase())); result.append(String.format("\n", HTML_ESCAPER.escape(tagDescription))); result.append("\n"); } result.append("
%s%s
\n"); ImmutableMap metadataTagDescriptions = OptionFilterDescriptions.getOptionMetadataTagDescription(productName); result.append("

Option Metadata Tags

\n"); result.append("\n"); for (OptionMetadataTag tag : OptionMetadataTag.values()) { // skip the tags that are reserved for undocumented flags. if (!tag.equals(OptionMetadataTag.HIDDEN) && !tag.equals(OptionMetadataTag.INTERNAL)) { String tagDescription = metadataTagDescriptions.get(tag); result.append("\n"); result.append( String.format( "\n", tag, tag.name().toLowerCase())); result.append(String.format("\n", HTML_ESCAPER.escape(tagDescription))); result.append("\n"); } } result.append("
%s%s
\n"); outErr.printOut(result.toString()); } private void appendOptionsHtml( StringBuilder result, Iterable> optionsClasses) { OptionsParser parser = OptionsParser.newOptionsParser(optionsClasses); String productName = runtime.getProductName(); result.append( parser .describeOptionsHtml(HTML_ESCAPER, productName) .replace("%{product}", productName)); } private static String capitalize(String s) { return s.substring(0, 1).toUpperCase(Locale.US) + s.substring(1); } } /** A visitor for Blaze commands and their respective command line options. */ @FunctionalInterface interface CommandOptionVisitor { /** * Visits a Blaze command by providing access to its name, its meta-data and its command line * options (via an {@link OptionsParser} instance). * * @param commandName name of the command, e.g. "help". * @param commandAnnotation {@link Command} that contains addition information about the * command. * @param parser an {@link OptionsParser} instance that provides access to all options supported * by the command. */ void visit(String commandName, Command commandAnnotation, OptionsParser parser); } }