// 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;

import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.lib.cmdline.TargetParsingException;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.packages.Target;
import com.google.devtools.build.lib.runtime.proto.InvocationPolicyOuterClass.InvocationPolicy;
import com.google.devtools.build.lib.server.FailureDetails;
import com.google.devtools.build.lib.server.FailureDetails.Command.Code;
import com.google.devtools.build.lib.server.FailureDetails.FailureDetail;
import com.google.devtools.build.lib.server.FailureDetails.TargetPatterns;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import com.google.devtools.build.lib.skyframe.TargetPatternPhaseValue;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.vfs.FileSystemUtils;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.InvocationPolicyEnforcer;
import com.google.devtools.common.options.OptionDefinition;
import com.google.devtools.common.options.OptionPriority.PriorityCategory;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.OptionsParsingResult;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;

/**
 * Handles parsing the blaze command arguments.
 *
 * <p>This class manages rc options, configs, default options, and invocation policy.
 */
public final class BlazeOptionHandler {
  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

  // Keep in sync with options added in OptionProcessor::AddRcfileArgsAndOptions()
  private static final ImmutableSet<String> INTERNAL_COMMAND_OPTIONS =
      ImmutableSet.of(
          "rc_source",
          "default_override",
          "isatty",
          "terminal_columns",
          "ignore_client_env",
          "client_env",
          "client_cwd");

  // All options set on this pseudo command are inherited by all commands, with unrecognized options
  // resulting in an error.
  private static final String ALWAYS_PSEUDO_COMMAND = "always";

  // All options set on this pseudo command are inherited by all commands, with unrecognized options
  // being ignored as long as they are recognized by at least one (other) command.
  private static final String COMMON_PSEUDO_COMMAND = "common";

  // Marks an event to indicate a parsing error.
  static final String BAD_OPTION_TAG = "invalidOption";
  // Separates the invalid tag from the full error message for easier parsing.
  static final String ERROR_SEPARATOR = " :: ";

  private final BlazeRuntime runtime;
  private final OptionsParser optionsParser;
  private final BlazeWorkspace workspace;
  private final BlazeCommand command;
  private final Command commandAnnotation;
  private final InvocationPolicy invocationPolicy;
  private final List<String> rcfileNotes = new ArrayList<>();
  private final ImmutableList<Class<? extends OptionsBase>> allOptionsClasses;

  BlazeOptionHandler(
      BlazeRuntime runtime,
      BlazeWorkspace workspace,
      BlazeCommand command,
      Command commandAnnotation,
      OptionsParser optionsParser,
      InvocationPolicy invocationPolicy) {
    this.runtime = runtime;
    this.workspace = workspace;
    this.command = command;
    this.commandAnnotation = commandAnnotation;
    this.optionsParser = optionsParser;
    this.invocationPolicy = invocationPolicy;
    this.allOptionsClasses =
        runtime.getCommandMap().values().stream()
            .map(BlazeCommand::getClass)
            .flatMap(
                cmd ->
                    BlazeCommandUtils.getOptions(
                        cmd, runtime.getBlazeModules(), runtime.getRuleClassProvider())
                        .stream())
            .distinct()
            .collect(toImmutableList());
  }

  /**
   * Return options as {@link OptionsParsingResult} so the options can't be easily modified after
   * we've applied the invocation policy.
   */
  OptionsParsingResult getOptionsResult() {
    return optionsParser;
  }

  public List<String> getRcfileNotes() {
    return rcfileNotes;
  }

  /**
   * Only some commands work if cwd != workspaceSuffix in Blaze. In that case, also check if Blaze
   * was called from the output directory and fail if it was.
   */
  private DetailedExitCode checkCwdInWorkspace(EventHandler eventHandler) {
    if (!commandAnnotation.mustRunInWorkspace()) {
      return DetailedExitCode.success();
    }

    if (!workspace.getDirectories().inWorkspace()) {
      String message =
          "The '"
              + commandAnnotation.name()
              + "' command is only supported from within a workspace"
              + " (below a directory having a WORKSPACE file).\n"
              + "See documentation at"
              + " https://bazel.build/concepts/build-ref#workspace";
      eventHandler.handle(Event.error(message));
      return createDetailedExitCode(message, Code.NOT_IN_WORKSPACE);
    }

    Path workspacePath = workspace.getWorkspace();
    // TODO(kchodorow): Remove this once spaces are supported.
    if (workspacePath.getPathString().contains(" ")) {
      String message =
          runtime.getProductName()
              + " does not currently work properly from paths "
              + "containing spaces ("
              + workspacePath
              + ").";
      eventHandler.handle(Event.error(message));
      return createDetailedExitCode(message, Code.SPACES_IN_WORKSPACE_PATH);
    }

    if (workspacePath.getParentDirectory() != null) {
      Path doNotBuild =
          workspacePath.getParentDirectory().getRelative(BlazeWorkspace.DO_NOT_BUILD_FILE_NAME);

      if (doNotBuild.exists()) {
        if (!commandAnnotation.canRunInOutputDirectory()) {
          String message = getNotInRealWorkspaceError(doNotBuild);
          eventHandler.handle(Event.error(message));
          return createDetailedExitCode(message, Code.IN_OUTPUT_DIRECTORY);
        } else {
          eventHandler.handle(
              Event.warn(
                  runtime.getProductName() + " is run from output directory. This is unsound."));
        }
      }
    }
    return DetailedExitCode.success();
  }

  /**
   * Parses the unconditional options from .rc files for the current command.
   *
   * <p>This is not as trivial as simply taking the list of options for the specified command
   * because commands can inherit arguments from each other, and we have to respect that (e.g. if an
   * option is specified for 'build', it needs to take effect for the 'test' command, too). More
   * specific commands should have priority over the broader commands (say a "build" option that
   * conflicts with a "common" option should override the common one regardless of order.)
   *
   * <p>For each command, the options are parsed in rc order. This uses the primary rc file first,
   * and follows import statements. This is the order in which they were passed by the client.
   */
  @VisibleForTesting
  void parseRcOptions(
      EventHandler eventHandler, ListMultimap<String, RcChunkOfArgs> commandToRcArgs)
      throws OptionsParsingException {
    for (String commandToParse : getCommandNamesToParse(commandAnnotation)) {
      // Get all args defined for this command (or "common"), grouped by rc chunk.
      for (RcChunkOfArgs rcArgs : commandToRcArgs.get(commandToParse)) {
        if (!rcArgs.getArgs().isEmpty()) {
          String inherited = commandToParse.equals(commandAnnotation.name()) ? "" : "Inherited ";
          String source =
              rcArgs.getRcFile().equals("client")
                  ? "Options provided by the client"
                  : String.format(
                      "Reading rc options for '%s' from %s",
                      commandAnnotation.name(), rcArgs.getRcFile());
          rcfileNotes.add(
              String.format(
                  "%s:\n  %s'%s' options: %s",
                  source, inherited, commandToParse, Joiner.on(' ').join(rcArgs.getArgs())));
        }
        if (commandToParse.equals(COMMON_PSEUDO_COMMAND)) {
          // Pass in options data for all commands supported by the runtime so that options that
          // apply to some but not the current command can be ignored.
          //
          // Important note: The consistency checks performed by
          // OptionsParser#getFallbackOptionsData ensure that there aren't any two options across
          // all commands that have the same name but parse differently (e.g. because one accepts
          // a value and the other doesn't). This means that the options available on a command
          // limit the options available on other commands even without command inheritance. This
          // restriction is necessary to ensure that the options specified on the "common"
          // pseudo command can be parsed unambiguously.
          ImmutableList<String> ignoredArgs =
              optionsParser.parseWithSourceFunction(
                  PriorityCategory.RC_FILE,
                  o -> rcArgs.getRcFile(),
                  rcArgs.getArgs(),
                  OptionsParser.getFallbackOptionsData(allOptionsClasses));
          if (!ignoredArgs.isEmpty()) {
            // Append richer information to the note.
            int index = rcfileNotes.size() - 1;
            String note = rcfileNotes.get(index);
            note +=
                String.format(
                    "\n  Ignored as unsupported by '%s': %s",
                    commandAnnotation.name(), Joiner.on(' ').join(ignoredArgs));
            rcfileNotes.set(index, note);
          }
        } else {
          optionsParser.parse(PriorityCategory.RC_FILE, rcArgs.getRcFile(), rcArgs.getArgs());
        }
      }
    }
  }

  private void parseArgsAndConfigs(List<String> args, ExtendedEventHandler eventHandler)
      throws OptionsParsingException, InterruptedException, AbruptExitException {
    Path workspaceDirectory = workspace.getWorkspace();
    // TODO(ulfjack): The working directory is passed by the client as part of CommonCommandOptions,
    // and we can't know it until after we've parsed the options, so use the workspace for now.
    Path workingDirectory = workspace.getWorkspace();

    Function<OptionDefinition, String> commandOptionSourceFunction =
        option -> {
          if (INTERNAL_COMMAND_OPTIONS.contains(option.getOptionName())) {
            return "options generated by " + runtime.getProductName() + " launcher";
          } else {
            return "command line options";
          }
        };

    // Explicit command-line options:
    List<String> cmdLineAfterCommand = args.subList(1, args.size());

    // Before parsing any rcfiles we need to first parse --rc_source so the parser can reference the
    // proper rcfiles. The --default_override options should be parsed with the --rc_source since
    // {@link #parseRcOptions} depends on the list populated by the {@link
    // ClientOptions#OptionOverrideConverter}.
    ImmutableList.Builder<String> defaultOverridesAndRcSources = new ImmutableList.Builder<>();
    ImmutableList.Builder<String> remainingCmdLine = new ImmutableList.Builder<>();
    partitionCommandLineArgs(cmdLineAfterCommand, defaultOverridesAndRcSources, remainingCmdLine);

    // Parses options needed to parse rcfiles properly.
    optionsParser.parseWithSourceFunction(
        PriorityCategory.COMMAND_LINE,
        commandOptionSourceFunction,
        defaultOverridesAndRcSources.build(),
        /* fallbackData= */ null);

    // Command-specific options from .blazerc passed in via --default_override and --rc_source.
    ClientOptions rcFileOptions = optionsParser.getOptions(ClientOptions.class);
    ListMultimap<String, RcChunkOfArgs> commandToRcArgs =
        structureRcOptionsAndConfigs(
            eventHandler,
            rcFileOptions.rcSource,
            rcFileOptions.optionsOverrides,
            runtime.getCommandMap().keySet());
    parseRcOptions(eventHandler, commandToRcArgs);

    // Parses the remaining command-line options.
    optionsParser.parseWithSourceFunction(
        PriorityCategory.COMMAND_LINE,
        commandOptionSourceFunction,
        remainingCmdLine.build(),
        /* fallbackData= */ null);

    if (commandAnnotation.builds()) {
      // splits project files from targets in the traditional sense
      ProjectFileSupport.handleProjectFiles(
          eventHandler,
          runtime.getProjectFileProvider(),
          workspaceDirectory.asFragment(),
          workingDirectory,
          optionsParser,
          commandAnnotation.name());
    }

    expandConfigOptions(eventHandler, commandToRcArgs);
  }

  /**
   * {@link ExtendedEventHandler} override that passes through "normal" events but not events that
   * would go to the build event proto.
   *
   * <p>Starlark flags are conceptually options but still need target pattern evaluation. If we pass
   * {@link #post}able events from that evaluation, that would produce "target loaded" and "target
   * configured" events in the build event proto output that consumers can confuse with actual
   * targets requested by the build.
   *
   * <p>This is important because downstream services (like a continuous integration tool or build
   * results dashboard) read these messages to reconcile which requested targets were built. If they
   * determine Blaze tried to build {@code //foo //bar} then see a "target configured" message for
   * some other target {@code //my_starlark_flag}, they might show misleading messages like "Built 3
   * of 2 requested targets.".
   *
   * <p>Hence this class. By dropping those events, we restrict all info and error reporting logic
   * to the options parsing pipeline.
   */
  private static class NonPostingEventHandler implements ExtendedEventHandler {
    private final ExtendedEventHandler delegate;

    NonPostingEventHandler(ExtendedEventHandler delegate) {
      this.delegate = delegate;
    }

    @Override
    public void handle(Event e) {
      delegate.handle(e);
    }

    @Override
    public void post(ExtendedEventHandler.Postable e) {}
  }

  /**
   * Lets {@link StarlarkOptionsParser} convert flag names to {@link Target}s through {@link
   * TargetPatternPhaseValue}.
   *
   * <p>This is used for top-level flag parsing, outside any {@link SkyFunction}.
   */
  public static class SkyframeExecutorTargetLoader
      implements StarlarkOptionsParser.BuildSettingLoader {
    private final SkyframeExecutor skyframeExecutor;
    private final PathFragment relativeWorkingDirectory;
    private final ExtendedEventHandler reporter;

    public SkyframeExecutorTargetLoader(CommandEnvironment env) {
      this.skyframeExecutor = env.getSkyframeExecutor();
      this.relativeWorkingDirectory = env.getRelativeWorkingDirectory();
      this.reporter = new NonPostingEventHandler(env.getReporter());
    }

    @VisibleForTesting
    public SkyframeExecutorTargetLoader(
        SkyframeExecutor skyframeExecutor,
        PathFragment relativeWorkingDirectory,
        ExtendedEventHandler reporter) {
      this.skyframeExecutor = skyframeExecutor;
      this.relativeWorkingDirectory = relativeWorkingDirectory;
      this.reporter = new NonPostingEventHandler(reporter);
    }

    @Override
    public Target loadBuildSetting(String targetLabel)
        throws InterruptedException, TargetParsingException {
      TargetPatternPhaseValue tpv =
          skyframeExecutor.loadTargetPatternsWithoutFilters(
              reporter,
              Collections.singletonList(targetLabel),
              relativeWorkingDirectory,
              SkyframeExecutor.DEFAULT_THREAD_COUNT,
              /* keepGoing= */ false);
      ImmutableSet<Target> result = tpv.getTargets(reporter, skyframeExecutor.getPackageManager());
      if (result.size() != 1) {
        throw new TargetParsingException(
            "user-defined flags must reference exactly one target",
            TargetPatterns.Code.TARGET_FORMAT_INVALID);
      }
      return Iterables.getOnlyElement(result);
    }
  }

  /**
   * TODO(bazel-team): When we move CoreOptions options to be defined in starlark, make sure they're
   * not passed in here during {@link #getOptionsResult}.
   */
  DetailedExitCode parseStarlarkOptions(CommandEnvironment env) {
    // For now, restrict starlark options to commands that already build to ensure that loading
    // will work. We may want to open this up to other commands in the future. The "info"
    // and "clean" commands have builds=true set in their annotation but don't actually do any
    // building (b/120041419).
    if (!commandAnnotation.builds()
        || commandAnnotation.name().equals("info")
        || commandAnnotation.name().equals("clean")) {
      return DetailedExitCode.success();
    }
    try {
      Preconditions.checkState(
          StarlarkOptionsParser.newStarlarkOptionsParser(
                  new SkyframeExecutorTargetLoader(env), optionsParser)
              .parse());
    } catch (OptionsParsingException e) {
      String logMessage = "Error parsing Starlark options";
      logger.atInfo().withCause(e).log("%s", logMessage);
      return processOptionsParsingException(
          env.getReporter(), e, logMessage, Code.STARLARK_OPTIONS_PARSE_FAILURE);
    }
    return DetailedExitCode.success();
  }

  /**
   * Parses the options, taking care not to generate any output to outErr, return, or throw an
   * exception.
   *
   * @return {@code DetailedExitCode.success()} if everything went well, or some other value if not
   */
  DetailedExitCode parseOptions(List<String> args, ExtendedEventHandler eventHandler) {
    // The initialization code here was carefully written to parse the options early before we call
    // into the BlazeModule APIs, which means we must not generate any output to outErr, return, or
    // throw an exception. All the events happening here are instead stored in a temporary event
    // handler, and later replayed.
    DetailedExitCode earlyExitCode = checkCwdInWorkspace(eventHandler);
    if (!earlyExitCode.isSuccess()) {
      return earlyExitCode;
    }

    try {
      parseArgsAndConfigs(args, eventHandler);
      // Allow the command to edit the options.
      command.editOptions(optionsParser);
      // Migration of --watchfs to a command option.
      // TODO(ulfjack): Get rid of the startup option and drop this code.
      if (runtime.getStartupOptionsProvider().getOptions(BlazeServerStartupOptions.class).watchFS) {
        eventHandler.handle(
            Event.error(
                "--watchfs as startup option is deprecated, replace it with the equivalent command "
                    + "option. For example, instead of 'bazel --watchfs build //foo', run "
                    + "'bazel build --watchfs //foo'."));
        try {
          optionsParser.parse("--watchfs");
        } catch (OptionsParsingException e) {
          // This should never happen.
          throw new IllegalStateException(e);
        }
      }
      // Merge the invocation policy that is user-supplied, from the command line, and any
      // invocation policy that was added by a module. The module one goes 'first,' so the user
      // one has priority.
      InvocationPolicy combinedPolicy =
          InvocationPolicy.newBuilder()
              .mergeFrom(runtime.getModuleInvocationPolicy())
              .mergeFrom(invocationPolicy)
              .build();
      InvocationPolicyEnforcer optionsPolicyEnforcer =
          new InvocationPolicyEnforcer(
              combinedPolicy, Level.INFO, optionsParser.getConversionContext());
      // Enforce the invocation policy. It is intentional that this is the last step in preparing
      // the options. The invocation policy is used in security-critical contexts, and may be used
      // as a last resort to override flags. That means that the policy can override flags set in
      // BlazeCommand.editOptions, so the code needs to be safe regardless of the actual flag
      // values. At the time of this writing, editOptions was only used as a convenience feature or
      // to improve the user experience, but not required for safety or correctness.
      optionsPolicyEnforcer.enforce(optionsParser, commandAnnotation.name());
      // Print warnings for odd options usage
      for (String warning : optionsParser.getWarnings()) {
        eventHandler.handle(Event.warn(warning));
      }
      CommonCommandOptions commonOptions = optionsParser.getOptions(CommonCommandOptions.class);
      for (String warning : commonOptions.deprecationWarnings) {
        eventHandler.handle(Event.warn(warning));
      }
    } catch (OptionsParsingException e) {
      String logMessage = "Error parsing options";
      logger.atInfo().withCause(e).log("%s", logMessage);
      return processOptionsParsingException(
          eventHandler, e, logMessage, Code.OPTIONS_PARSE_FAILURE);
    } catch (InterruptedException e) {
      return DetailedExitCode.of(
          FailureDetail.newBuilder()
              .setInterrupted(
                  FailureDetails.Interrupted.newBuilder()
                      .setCode(FailureDetails.Interrupted.Code.INTERRUPTED))
              .build());
    } catch (AbruptExitException e) {
      return e.getDetailedExitCode();
    }
    return DetailedExitCode.success();
  }

  /**
   * Expand the values of --config according to the definitions provided in the rc files and the
   * applicable command.
   */
  @VisibleForTesting
  void expandConfigOptions(
      EventHandler eventHandler, ListMultimap<String, RcChunkOfArgs> commandToRcArgs)
      throws OptionsParsingException {
    ConfigExpander.expandConfigOptions(
        eventHandler,
        commandToRcArgs,
        commandAnnotation.name(),
        getCommandNamesToParse(commandAnnotation),
        rcfileNotes::add,
        optionsParser,
        OptionsParser.getFallbackOptionsData(allOptionsClasses));
  }

  private static List<String> getCommandNamesToParse(Command commandAnnotation) {
    List<String> result = new ArrayList<>();
    result.add(ALWAYS_PSEUDO_COMMAND);
    result.add(COMMON_PSEUDO_COMMAND);
    getCommandNamesToParseHelper(commandAnnotation, result);
    return result;
  }

  private static void getCommandNamesToParseHelper(
      Command commandAnnotation, List<String> accumulator) {
    for (Class<? extends BlazeCommand> base : commandAnnotation.inherits()) {
      getCommandNamesToParseHelper(base.getAnnotation(Command.class), accumulator);
    }
    accumulator.add(commandAnnotation.name());
  }

  private static DetailedExitCode processOptionsParsingException(
      ExtendedEventHandler eventHandler,
      OptionsParsingException e,
      String logMessage,
      Code failureCode) {
    Event error;
    // Differentiates errors stemming from an invalid argument and errors from different parts of
    // the codebase.
    if (e.getInvalidArgument() != null) {
      error =
          Event.error(e.getInvalidArgument() + ERROR_SEPARATOR + e.getMessage())
              .withTag(BAD_OPTION_TAG);
    } else {
      error = Event.error(e.getMessage());
    }
    eventHandler.handle(error);
    return createDetailedExitCode(logMessage + ": " + e.getMessage(), failureCode);
  }

  private String getNotInRealWorkspaceError(Path doNotBuildFile) {
    String message =
        String.format(
            "%1$s should not be called from a %1$s output directory. ", runtime.getProductName());
    try {
      String realWorkspace = new String(FileSystemUtils.readContentAsLatin1(doNotBuildFile));
      message += String.format("The pertinent workspace directory is: '%s'", realWorkspace);
    } catch (IOException e) {
      // We are exiting anyway.
    }

    return message;
  }

  /**
   * The rc options are passed via {@link ClientOptions#optionsOverrides} and {@link
   * ClientOptions#rcSource}, which is basically a line-by-line transfer of the rc files read by the
   * client. This is not a particularly useful format for expanding the options, so this method
   * structures the list so that it is easier to find the arguments that apply to a command, or to
   * find the definitions of a config value.
   */
  @VisibleForTesting
  static ListMultimap<String, RcChunkOfArgs> structureRcOptionsAndConfigs(
      EventHandler eventHandler,
      List<String> rcFiles,
      List<ClientOptions.OptionOverride> rawOverrides,
      Set<String> validCommands)
      throws OptionsParsingException {
    ListMultimap<String, RcChunkOfArgs> commandToRcArgs = ArrayListMultimap.create();

    String lastRcFile = null;
    ListMultimap<String, String> commandToArgMapForLastRc = null;
    for (ClientOptions.OptionOverride override : rawOverrides) {
      if (override.blazeRc < 0 || override.blazeRc >= rcFiles.size()) {
        eventHandler.handle(
            Event.warn("inconsistency in generated command line args. Ignoring bogus argument\n"));
        continue;
      }
      String rcFile = rcFiles.get(override.blazeRc);
      // The canonicalize-flags command only inherits bazelrc "build" commands. Not "test", not
      // "build:foo". Restrict --flag_alias accordingly to prevent building with flags that
      // canonicalize-flags can't recognize.
      if ((override.option.startsWith("--" + Converters.BLAZE_ALIASING_FLAG + "=")
              || override.option.equals("--" + Converters.BLAZE_ALIASING_FLAG))
          // In production, "build" is always a valid command, but not necessarily in tests.
          // Particularly C0Command, which some tests use for low-level options parsing logic. We
          // don't want to interfere with those.
          && validCommands.contains("build")
          && !override.command.equals("build")) {
        throw new OptionsParsingException(
            String.format(
                "%s: \"%s %s\" disallowed. --%s only supports the \"build\" command.",
                rcFile, override.command, override.option, Converters.BLAZE_ALIASING_FLAG));
      }
      String command = override.command;
      int index = command.indexOf(':');
      if (index > 0) {
        command = command.substring(0, index);
      }
      if (!validCommands.contains(command)
          && !command.equals(ALWAYS_PSEUDO_COMMAND)
          && !command.equals(COMMON_PSEUDO_COMMAND)) {
        eventHandler.handle(
            Event.warn(
                "while reading option defaults file '"
                    + rcFile
                    + "':\n"
                    + "  invalid command name '"
                    + override.command
                    + "'."));
        continue;
      }

      // We've moved on to another rc file "chunk," store the accumulated args from the last one.
      if (!rcFile.equals(lastRcFile)) {
        if (lastRcFile != null) {
          // Go through the various commands identified in this rc file (or chunk of file) and
          // store them grouped first by command, then by rc chunk.
          for (String commandKey : commandToArgMapForLastRc.keySet()) {
            commandToRcArgs.put(
                commandKey,
                new RcChunkOfArgs(lastRcFile, commandToArgMapForLastRc.get(commandKey)));
          }
        }
        lastRcFile = rcFile;
        commandToArgMapForLastRc = ArrayListMultimap.create();
      }

      commandToArgMapForLastRc.put(override.command, override.option);
    }
    if (lastRcFile != null) {
      // Once again, for this last rc file chunk, store them grouped by command.
      for (String commandKey : commandToArgMapForLastRc.keySet()) {
        commandToRcArgs.put(
            commandKey, new RcChunkOfArgs(lastRcFile, commandToArgMapForLastRc.get(commandKey)));
      }
    }

    return commandToRcArgs;
  }

  private static DetailedExitCode createDetailedExitCode(String message, Code detailedCode) {
    return DetailedExitCode.of(
        FailureDetail.newBuilder()
            .setMessage(message)
            .setCommand(FailureDetails.Command.newBuilder().setCode(detailedCode))
            .build());
  }

  private static void partitionCommandLineArgs(
      List<String> cmdLine,
      ImmutableList.Builder<String> defaultOverridesAndRcSources,
      ImmutableList.Builder<String> remainingCmdLine) {

    Iterator<String> cmdLineIterator = cmdLine.iterator();

    while (cmdLineIterator.hasNext()) {
      String option = cmdLineIterator.next();
      if (option.startsWith("--rc_source=") || option.startsWith("--default_override=")) {
        defaultOverridesAndRcSources.add(option);
      } else if (option.equals("--rc_source") || option.equals("--default_override")) {
        Optional<String> possibleArgument =
            cmdLineIterator.hasNext() ? Optional.of(cmdLineIterator.next()) : Optional.empty();
        defaultOverridesAndRcSources.add(option);
        if (possibleArgument.isPresent()) {
          defaultOverridesAndRcSources.add(possibleArgument.get());
        }
      } else {
        remainingCmdLine.add(option);
      }
    }
  }
}
