All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.devtools.common.options.OptionsParserImpl Maven / Gradle / Ivy

// 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 static com.google.devtools.common.options.OptionPriority.PriorityCategory.INVOCATION_POLICY;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toCollection;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterators;
import com.google.devtools.common.options.OptionPriority.PriorityCategory;
import com.google.devtools.common.options.OptionValueDescription.ExpansionBundle;
import com.google.devtools.common.options.OptionsParser.OptionDescription;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;

/**
 * The implementation of the options parser. This is intentionally package private for full
 * flexibility. Use {@link OptionsParser} or {@link Options} if you're a consumer.
 */
class OptionsParserImpl {

  /** Helper class to create a new instance of {@link OptionsParserImpl}. */
  static final class Builder {
    private OptionsData optionsData;
    private ArgsPreProcessor argsPreProcessor = args -> args;
    @Nullable private String skippedPrefix;
    private boolean ignoreInternalOptions = true;

    /** Set the {@link OptionsData} to be used in this instance. */
    public Builder optionsData(OptionsData optionsData) {
      this.optionsData = optionsData;
      return this;
    }

    /** Sets the {@link ArgsPreProcessor} to use during processing. */
    public Builder argsPreProcessor(ArgsPreProcessor preProcessor) {
      this.argsPreProcessor = preProcessor;
      return this;
    }

    /** Any flags with this prefix will be skipped during processing. */
    public Builder skippedPrefix(@Nullable String skippedPrefix) {
      this.skippedPrefix = skippedPrefix;
      return this;
    }

    /** Sets whether the parser should ignore internal-only options. */
    public Builder ignoreInternalOptions(boolean ignoreInternalOptions) {
      this.ignoreInternalOptions = ignoreInternalOptions;
      return this;
    }

    /** Returns a newly-initialized {@link OptionsParserImpl}. */
    public OptionsParserImpl build() {
      return new OptionsParserImpl(
          this.optionsData, this.argsPreProcessor, this.skippedPrefix, this.ignoreInternalOptions);
    }
  }

  /** Returns a new {@link Builder} with correct defaults applied. */
  public static Builder builder() {
    return new Builder();
  }

  private final OptionsData optionsData;

  /**
   * We store the results of option parsing in here - since there can only be one value per option
   * field, this is where the different instances of an option have been combined and the final
   * value is tracked. It'll look like
   *
   * 
   *   OptionDefinition("--host") -> "www.google.com"
   *   OptionDefinition("--port") -> 80
   * 
* * This map is modified by repeated calls to {@link #parse(OptionPriority.PriorityCategory, * Function,List)}. */ private final Map optionValues = new HashMap<>(); /** * Since parse() expects multiple calls to it with the same {@link PriorityCategory} to be treated * as though the args in the later call have higher priority over the earlier calls, we need to * track the high water mark of option priority at each category. Each call to parse will start at * this level. */ private final Map nextPriorityPerPriorityCategory = Stream.of(PriorityCategory.values()) .collect(Collectors.toMap(p -> p, OptionPriority::lowestOptionPriorityAtCategory)); /** * Explicit option tracking, tracking each option as it was provided, after they have been parsed. * *

The value is unconverted, still the string as it was read from the input, or partially * altered in cases where the flag was set by non {@code --flag=value} forms; e.g. {@code --nofoo} * becomes {@code --foo=0}. */ private final List parsedOptions = new ArrayList<>(); private final List warnings = new ArrayList<>(); private final ArgsPreProcessor argsPreProcessor; @Nullable private final String skippedPrefix; private final boolean ignoreInternalOptions; OptionsParserImpl( OptionsData optionsData, ArgsPreProcessor argsPreProcessor, @Nullable String skippedPrefix, boolean ignoreInternalOptions) { this.optionsData = optionsData; this.argsPreProcessor = argsPreProcessor; this.skippedPrefix = skippedPrefix; this.ignoreInternalOptions = ignoreInternalOptions; } /** Returns the {@link OptionsData} used in this instance. */ OptionsData getOptionsData() { return optionsData; } /** Returns a {@link Builder} that is configured the same as this parser. */ Builder toBuilder() { return builder() .optionsData(optionsData) .argsPreProcessor(argsPreProcessor) .skippedPrefix(skippedPrefix); } /** Implements {@link OptionsParser#asCompleteListOfParsedOptions()}. */ List asCompleteListOfParsedOptions() { return parsedOptions.stream() // It is vital that this sort is stable so that options on the same priority are not // reordered. .sorted(comparing(ParsedOptionDescription::getPriority)) .collect(toCollection(ArrayList::new)); } /** Implements {@link OptionsParser#asListOfExplicitOptions()}. */ List asListOfExplicitOptions() { return parsedOptions.stream() .filter(ParsedOptionDescription::isExplicit) // It is vital that this sort is stable so that options on the same priority are not // reordered. .sorted(comparing(ParsedOptionDescription::getPriority)) .collect(toCollection(ArrayList::new)); } /** Implements {@link OptionsParser#canonicalize}. */ List asCanonicalizedList() { return asCanonicalizedListOfParsedOptions() .stream() .map(ParsedOptionDescription::getDeprecatedCanonicalForm) .collect(ImmutableList.toImmutableList()); } /** Implements {@link OptionsParser#canonicalize}. */ List asCanonicalizedListOfParsedOptions() { return optionValues.keySet().stream() .map(optionDefinition -> optionValues.get(optionDefinition).getCanonicalInstances()) .flatMap(Collection::stream) // Return the effective (canonical) options in the order they were applied. .sorted(comparing(ParsedOptionDescription::getPriority)) .collect(ImmutableList.toImmutableList()); } /** Implements {@link OptionsParser#asListOfOptionValues()}. */ List asListOfEffectiveOptions() { List result = new ArrayList<>(); for (Map.Entry mapEntry : optionsData.getAllOptionDefinitions()) { OptionDefinition optionDefinition = mapEntry.getValue(); OptionValueDescription optionValue = optionValues.get(optionDefinition); if (optionValue == null) { result.add(OptionValueDescription.getDefaultOptionValue(optionDefinition)); } else { result.add(optionValue); } } return result; } private void maybeAddDeprecationWarning( OptionDefinition optionDefinition, PriorityCategory priority) { // Don't add a warning for deprecated flag set by the invocation policy. if (priority.equals(INVOCATION_POLICY)) { return; } // Continue to support the old behavior for @Deprecated options. String warning = optionDefinition.getDeprecationWarning(); if (!warning.isEmpty() || (optionDefinition.getField().isAnnotationPresent(Deprecated.class))) { addDeprecationWarning(optionDefinition.getOptionName(), warning); } } private void addDeprecationWarning(String optionName, String warning) { warnings.add( String.format( "Option '%s' is deprecated%s", optionName, (warning.isEmpty() ? "" : ": " + warning))); } OptionValueDescription clearValue(OptionDefinition optionDefinition) throws OptionsParsingException { return optionValues.remove(optionDefinition); } OptionValueDescription getOptionValueDescription(String name) { OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name); if (optionDefinition == null) { throw new IllegalArgumentException("No such option '" + name + "'"); } return optionValues.get(optionDefinition); } OptionDescription getOptionDescription(String name) throws OptionsParsingException { OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name); if (optionDefinition == null) { return null; } return new OptionDescription(optionDefinition, optionsData); } /** * Implementation of {@link OptionsParser#getExpansionValueDescriptions(OptionDefinition, * OptionInstanceOrigin)} */ ImmutableList getExpansionValueDescriptions( OptionDefinition expansionFlagDef, OptionInstanceOrigin originOfExpansionFlag) throws OptionsParsingException { ImmutableList.Builder builder = ImmutableList.builder(); // Values needed to correctly track the origin of the expanded options. OptionPriority nextOptionPriority = OptionPriority.getChildPriority(originOfExpansionFlag.getPriority()); String source; ParsedOptionDescription implicitDependent = null; ParsedOptionDescription expandedFrom = null; ImmutableList options; ParsedOptionDescription expansionFlagParsedDummy = ParsedOptionDescription.newDummyInstance(expansionFlagDef, originOfExpansionFlag); if (expansionFlagDef.hasImplicitRequirements()) { options = ImmutableList.copyOf(expansionFlagDef.getImplicitRequirements()); source = String.format( "implicitly required by %s (source: %s)", expansionFlagDef, originOfExpansionFlag.getSource()); implicitDependent = expansionFlagParsedDummy; } else if (expansionFlagDef.isExpansionOption()) { options = optionsData.getEvaluatedExpansion(expansionFlagDef); source = String.format( "expanded by %s (source: %s)", expansionFlagDef, originOfExpansionFlag.getSource()); expandedFrom = expansionFlagParsedDummy; } else { return ImmutableList.of(); } Iterator optionsIterator = options.iterator(); while (optionsIterator.hasNext()) { String unparsedFlagExpression = optionsIterator.next(); ParsedOptionDescription parsedOption = identifyOptionAndPossibleArgument( unparsedFlagExpression, optionsIterator, nextOptionPriority, o -> source, implicitDependent, expandedFrom); builder.add(parsedOption); nextOptionPriority = OptionPriority.nextOptionPriority(nextOptionPriority); } return builder.build(); } boolean containsExplicitOption(String name) { OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name); if (optionDefinition == null) { throw new IllegalArgumentException("No such option '" + name + "'"); } return optionValues.get(optionDefinition) != null; } /** * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be * called recursively. The option's definition dictates how it reacts to multiple settings. By * default, the arg seen last at the highest priority takes precedence, overriding the early * values. Options that accumulate multiple values will track them in priority and appearance * order. */ ResidueAndPriority parse( PriorityCategory priorityCat, Function sourceFunction, List args) throws OptionsParsingException { ResidueAndPriority residueAndPriority = parse(nextPriorityPerPriorityCategory.get(priorityCat), sourceFunction, null, null, args); nextPriorityPerPriorityCategory.put(priorityCat, residueAndPriority.nextPriority); return residueAndPriority; } /** * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be * called recursively. Calls may contain intersecting sets of options; in that case, the arg seen * last takes precedence. * *

The method treats options that have neither an implicitDependent nor an expandedFrom value * as explicitly set. */ private ResidueAndPriority parse( OptionPriority priority, Function sourceFunction, ParsedOptionDescription implicitDependent, ParsedOptionDescription expandedFrom, List args) throws OptionsParsingException { List unparsedArgs = new ArrayList<>(); List unparsedPostDoubleDashArgs = new ArrayList<>(); Iterator argsIterator = argsPreProcessor.preProcess(args).iterator(); while (argsIterator.hasNext()) { String arg = argsIterator.next(); if (!arg.startsWith("-")) { unparsedArgs.add(arg); continue; // not an option arg } if (skippedPrefix != null && arg.startsWith(skippedPrefix)) { unparsedArgs.add(arg); continue; } if (arg.equals("--")) { // "--" means all remaining args aren't options Iterators.addAll(unparsedPostDoubleDashArgs, argsIterator); break; } ParsedOptionDescription parsedOption = identifyOptionAndPossibleArgument( arg, argsIterator, priority, sourceFunction, implicitDependent, expandedFrom); handleNewParsedOption(parsedOption); priority = OptionPriority.nextOptionPriority(priority); } // Go through the final values and make sure they are valid values for their option. Unlike any // checks that happened above, this also checks that flags that were not set have a valid // default value. getValue() will throw if the value is invalid. for (OptionValueDescription valueDescription : asListOfEffectiveOptions()) { valueDescription.getValue(); } return new ResidueAndPriority(unparsedArgs, unparsedPostDoubleDashArgs, priority); } /** A class that stores residue and priority information. */ static final class ResidueAndPriority { final List postDoubleDashResidue; final List preDoubleDashResidue; final OptionPriority nextPriority; ResidueAndPriority( List preDashResidue, List postDashResidue, OptionPriority nextPriority) { this.preDoubleDashResidue = preDashResidue; this.postDoubleDashResidue = postDashResidue; this.nextPriority = nextPriority; } public List getResidue() { List toReturn = new ArrayList<>(preDoubleDashResidue.size() + postDoubleDashResidue.size()); toReturn.addAll(preDoubleDashResidue); toReturn.addAll(postDoubleDashResidue); return toReturn; } } /** Implements {@link OptionsParser#parseArgsAsExpansionOfOption} */ ResidueAndPriority parseArgsAsExpansionOfOption( ParsedOptionDescription optionToExpand, Function sourceFunction, List args) throws OptionsParsingException { return parse( OptionPriority.getChildPriority(optionToExpand.getPriority()), sourceFunction, null, optionToExpand, args); } /** * Implementation of {@link OptionsParser#addOptionValueAtSpecificPriority(OptionInstanceOrigin, * OptionDefinition, String)} */ void addOptionValueAtSpecificPriority( OptionInstanceOrigin origin, OptionDefinition option, String unconvertedValue) throws OptionsParsingException { Preconditions.checkNotNull(option); Preconditions.checkNotNull( unconvertedValue, "Cannot set %s to a null value. Pass \"\" if an empty value is required.", option); Preconditions.checkNotNull( origin, "Cannot assign value \'%s\' to %s without a clear origin for this value.", unconvertedValue, option); PriorityCategory priorityCategory = origin.getPriority().getPriorityCategory(); boolean isNotDefault = priorityCategory != OptionPriority.PriorityCategory.DEFAULT; Preconditions.checkArgument( isNotDefault, "Attempt to assign value \'%s\' to %s at priority %s failed. Cannot set options at " + "default priority - by definition, that means the option is unset.", unconvertedValue, option, priorityCategory); handleNewParsedOption( ParsedOptionDescription.newParsedOptionDescription( option, String.format("--%s=%s", option.getOptionName(), unconvertedValue), unconvertedValue, origin)); } /** Takes care of tracking the parsed option's value in relation to other options. */ private void handleNewParsedOption(ParsedOptionDescription parsedOption) throws OptionsParsingException { OptionDefinition optionDefinition = parsedOption.getOptionDefinition(); // All options can be deprecated; check and warn before doing any option-type specific work. maybeAddDeprecationWarning(optionDefinition, parsedOption.getPriority().getPriorityCategory()); // Track the value, before any remaining option-type specific work that is done outside of // the OptionValueDescription. OptionValueDescription entry = optionValues.computeIfAbsent( optionDefinition, def -> OptionValueDescription.createOptionValueDescription(def, optionsData)); ExpansionBundle expansionBundle = entry.addOptionInstance(parsedOption, warnings); @Nullable String unconvertedValue = parsedOption.getUnconvertedValue(); // There are 3 types of flags that expand to other flag values. Expansion flags are the // accepted way to do this, but implicit requirements also do this. We rely on the // OptionProcessor compile-time check's guarantee that no option sets // both expansion behaviors. (In Bazel, --config is another such flag, but that expansion // is not controlled within the options parser, so we ignore it here) // As much as possible, we want the behaviors of these different types of flags to be // identical, as this minimizes the number of edge cases, but we do not yet track these values // in the same way. if (parsedOption.getImplicitDependent() == null) { // Log explicit options and expanded options in the order they are parsed (can be sorted // later). This information is needed to correctly canonicalize flags. parsedOptions.add(parsedOption); } if (expansionBundle != null) { ResidueAndPriority residueAndPriority = parse( OptionPriority.getChildPriority(parsedOption.getPriority()), o -> expansionBundle.sourceOfExpansionArgs, optionDefinition.hasImplicitRequirements() ? parsedOption : null, optionDefinition.isExpansionOption() ? parsedOption : null, expansionBundle.expansionArgs); if (!residueAndPriority.getResidue().isEmpty()) { // Throw an assertion here, because this indicates an error in the definition of this // option's expansion or requirements, not with the input as provided by the user. throw new AssertionError( "Unparsed options remain after processing " + unconvertedValue + ": " + Joiner.on(' ').join(residueAndPriority.getResidue())); } } } private ParsedOptionDescription identifyOptionAndPossibleArgument( String arg, Iterator nextArgs, OptionPriority priority, Function sourceFunction, ParsedOptionDescription implicitDependent, ParsedOptionDescription expandedFrom) throws OptionsParsingException { // Store the way this option was parsed on the command line. StringBuilder commandLineForm = new StringBuilder(); commandLineForm.append(arg); String unconvertedValue = null; OptionDefinition optionDefinition; boolean booleanValue = true; if (arg.length() == 2) { // -l (may be nullary or unary) optionDefinition = optionsData.getFieldForAbbrev(arg.charAt(1)); booleanValue = true; } else if (arg.length() == 3 && arg.charAt(2) == '-') { // -l- (boolean) optionDefinition = optionsData.getFieldForAbbrev(arg.charAt(1)); booleanValue = false; } else if (arg.startsWith("--")) { // --long_option int equalsAt = arg.indexOf('='); int nameStartsAt = arg.startsWith("--") ? 2 : 1; String name = equalsAt == -1 ? arg.substring(nameStartsAt) : arg.substring(nameStartsAt, equalsAt); if (name.trim().isEmpty()) { throw new OptionsParsingException("Invalid options syntax: " + arg, arg); } unconvertedValue = equalsAt == -1 ? null : arg.substring(equalsAt + 1); optionDefinition = optionsData.getOptionDefinitionFromName(name); // Look for a "no"-prefixed option name: "no". if (optionDefinition == null && name.startsWith("no")) { name = name.substring(2); optionDefinition = optionsData.getOptionDefinitionFromName(name); booleanValue = false; if (optionDefinition != null) { // TODO(bazel-team): Add tests for these cases. if (!optionDefinition.usesBooleanValueSyntax()) { throw new OptionsParsingException( "Illegal use of 'no' prefix on non-boolean option: " + arg, arg); } if (unconvertedValue != null) { throw new OptionsParsingException( "Unexpected value after boolean option: " + arg, arg); } // "no" signifies a boolean option w/ false value unconvertedValue = "0"; } } } else { throw new OptionsParsingException("Invalid options syntax: " + arg, arg); } if (optionDefinition == null || shouldIgnoreOption(optionDefinition)) { // Do not recognize internal options, which are treated as if they did not exist. throw new OptionsParsingException("Unrecognized option: " + arg, arg); } if (unconvertedValue == null) { // Special-case boolean to supply value based on presence of "no" prefix. if (optionDefinition.usesBooleanValueSyntax()) { unconvertedValue = booleanValue ? "1" : "0"; } else if (optionDefinition.getType().equals(Void.class)) { // This is expected, Void type options have no args. } else if (nextArgs.hasNext()) { // "--flag value" form unconvertedValue = nextArgs.next(); commandLineForm.append(" ").append(unconvertedValue); } else { throw new OptionsParsingException("Expected value after " + arg); } } return ParsedOptionDescription.newParsedOptionDescription( optionDefinition, commandLineForm.toString(), unconvertedValue, new OptionInstanceOrigin( priority, sourceFunction.apply(optionDefinition), implicitDependent, expandedFrom)); } private boolean shouldIgnoreOption(OptionDefinition optionDefinition) { return ignoreInternalOptions && ImmutableList.copyOf(optionDefinition.getOptionMetadataTags()) .contains(OptionMetadataTag.INTERNAL); } /** * Gets the result of parsing the options. */ O getParsedOptions(Class optionsClass) { // Create the instance: O optionsInstance; try { Constructor constructor = optionsData.getConstructor(optionsClass); if (constructor == null) { return null; } optionsInstance = constructor.newInstance(); } catch (ReflectiveOperationException e) { throw new IllegalStateException("Error while instantiating options class", e); } // Set the fields for (OptionDefinition optionDefinition : OptionsData.getAllOptionDefinitionsForClass(optionsClass)) { Object value; OptionValueDescription optionValue = optionValues.get(optionDefinition); if (optionValue == null) { value = optionDefinition.getDefaultValue(); } else { value = optionValue.getValue(); } try { optionDefinition.getField().set(optionsInstance, value); } catch (IllegalArgumentException e) { throw new IllegalStateException( String.format("Unable to set %s to value '%s'.", optionDefinition, value), e); } catch (IllegalAccessException e) { throw new IllegalStateException( "Could not set the field due to access issues. This is impossible, as the " + "OptionProcessor checks that all options are non-final public fields.", e); } } return optionsInstance; } List getWarnings() { return ImmutableList.copyOf(warnings); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy