Home | History | Annotate | Download | only in options
      1 // Copyright 2014 The Bazel Authors. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //    http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package com.google.devtools.common.options;
     16 
     17 import static java.util.Comparator.comparing;
     18 import static java.util.stream.Collectors.toCollection;
     19 
     20 import com.google.common.base.Joiner;
     21 import com.google.common.base.Preconditions;
     22 import com.google.common.collect.ImmutableList;
     23 import com.google.common.collect.Iterators;
     24 import com.google.devtools.common.options.OptionPriority.PriorityCategory;
     25 import com.google.devtools.common.options.OptionValueDescription.ExpansionBundle;
     26 import com.google.devtools.common.options.OptionsParser.OptionDescription;
     27 import java.lang.reflect.Constructor;
     28 import java.util.ArrayList;
     29 import java.util.Collection;
     30 import java.util.HashMap;
     31 import java.util.Iterator;
     32 import java.util.List;
     33 import java.util.Map;
     34 import java.util.function.Function;
     35 import java.util.stream.Collectors;
     36 import java.util.stream.Stream;
     37 import javax.annotation.Nullable;
     38 
     39 /**
     40  * The implementation of the options parser. This is intentionally package
     41  * private for full flexibility. Use {@link OptionsParser} or {@link Options}
     42  * if you're a consumer.
     43  */
     44 class OptionsParserImpl {
     45 
     46   private final OptionsData optionsData;
     47 
     48   /**
     49    * We store the results of option parsing in here - since there can only be one value per option
     50    * field, this is where the different instances of an option have been combined and the final
     51    * value is tracked. It'll look like
     52    *
     53    * <pre>
     54    *   OptionDefinition("--host") -> "www.google.com"
     55    *   OptionDefinition("--port") -> 80
     56    * </pre>
     57    *
     58    * This map is modified by repeated calls to {@link #parse(OptionPriority.PriorityCategory,
     59    * Function,List)}.
     60    */
     61   private final Map<OptionDefinition, OptionValueDescription> optionValues = new HashMap<>();
     62 
     63   /**
     64    * Explicit option tracking, tracking each option as it was provided, after they have been parsed.
     65    *
     66    * <p>The value is unconverted, still the string as it was read from the input, or partially
     67    * altered in cases where the flag was set by non {@code --flag=value} forms; e.g. {@code --nofoo}
     68    * becomes {@code --foo=0}.
     69    */
     70   private final List<ParsedOptionDescription> parsedOptions = new ArrayList<>();
     71 
     72   private final List<String> warnings = new ArrayList<>();
     73 
     74   /**
     75    * Since parse() expects multiple calls to it with the same {@link PriorityCategory} to be treated
     76    * as though the args in the later call have higher priority over the earlier calls, we need to
     77    * track the high water mark of option priority at each category. Each call to parse will start at
     78    * this level.
     79    */
     80   private final Map<PriorityCategory, OptionPriority> nextPriorityPerPriorityCategory =
     81       Stream.of(PriorityCategory.values())
     82           .collect(Collectors.toMap(p -> p, OptionPriority::lowestOptionPriorityAtCategory));
     83 
     84   private boolean allowSingleDashLongOptions = false;
     85 
     86   private ArgsPreProcessor argsPreProcessor = args -> args;
     87 
     88   /** Create a new parser object. Do not accept a null OptionsData object. */
     89   OptionsParserImpl(OptionsData optionsData) {
     90     Preconditions.checkNotNull(optionsData);
     91     this.optionsData = optionsData;
     92   }
     93 
     94   OptionsData getOptionsData() {
     95     return optionsData;
     96   }
     97 
     98   /**
     99    * Indicates whether or not the parser will allow long options with a
    100    * single-dash, instead of the usual double-dash, too, eg. -example instead of just --example.
    101    */
    102   void setAllowSingleDashLongOptions(boolean allowSingleDashLongOptions) {
    103     this.allowSingleDashLongOptions = allowSingleDashLongOptions;
    104   }
    105 
    106   /** Sets the ArgsPreProcessor for manipulations of the options before parsing. */
    107   void setArgsPreProcessor(ArgsPreProcessor preProcessor) {
    108     this.argsPreProcessor = Preconditions.checkNotNull(preProcessor);
    109   }
    110 
    111   /** Implements {@link OptionsParser#asCompleteListOfParsedOptions()}. */
    112   List<ParsedOptionDescription> asCompleteListOfParsedOptions() {
    113     return parsedOptions
    114         .stream()
    115         // It is vital that this sort is stable so that options on the same priority are not
    116         // reordered.
    117         .sorted(comparing(ParsedOptionDescription::getPriority))
    118         .collect(toCollection(ArrayList::new));
    119   }
    120 
    121   /** Implements {@link OptionsParser#asListOfExplicitOptions()}. */
    122   List<ParsedOptionDescription> asListOfExplicitOptions() {
    123     return parsedOptions
    124         .stream()
    125         .filter(ParsedOptionDescription::isExplicit)
    126         // It is vital that this sort is stable so that options on the same priority are not
    127         // reordered.
    128         .sorted(comparing(ParsedOptionDescription::getPriority))
    129         .collect(toCollection(ArrayList::new));
    130   }
    131 
    132   /** Implements {@link OptionsParser#canonicalize}. */
    133   List<String> asCanonicalizedList() {
    134     return asCanonicalizedListOfParsedOptions()
    135         .stream()
    136         .map(ParsedOptionDescription::getDeprecatedCanonicalForm)
    137         .collect(ImmutableList.toImmutableList());
    138   }
    139 
    140   /** Implements {@link OptionsParser#canonicalize}. */
    141   List<ParsedOptionDescription> asCanonicalizedListOfParsedOptions() {
    142     return optionValues
    143         .keySet()
    144         .stream()
    145         .sorted()
    146         .map(optionDefinition -> optionValues.get(optionDefinition).getCanonicalInstances())
    147         .flatMap(Collection::stream)
    148         .collect(ImmutableList.toImmutableList());
    149   }
    150 
    151   /** Implements {@link OptionsParser#asListOfOptionValues()}. */
    152   List<OptionValueDescription> asListOfEffectiveOptions() {
    153     List<OptionValueDescription> result = new ArrayList<>();
    154     for (Map.Entry<String, OptionDefinition> mapEntry : optionsData.getAllOptionDefinitions()) {
    155       OptionDefinition optionDefinition = mapEntry.getValue();
    156       OptionValueDescription optionValue = optionValues.get(optionDefinition);
    157       if (optionValue == null) {
    158         result.add(OptionValueDescription.getDefaultOptionValue(optionDefinition));
    159       } else {
    160         result.add(optionValue);
    161       }
    162     }
    163     return result;
    164   }
    165 
    166   private void maybeAddDeprecationWarning(OptionDefinition optionDefinition) {
    167     // Continue to support the old behavior for @Deprecated options.
    168     String warning = optionDefinition.getDeprecationWarning();
    169     if (!warning.isEmpty() || (optionDefinition.getField().isAnnotationPresent(Deprecated.class))) {
    170       addDeprecationWarning(optionDefinition.getOptionName(), warning);
    171     }
    172   }
    173 
    174   private void addDeprecationWarning(String optionName, String warning) {
    175     warnings.add(
    176         String.format(
    177             "Option '%s' is deprecated%s", optionName, (warning.isEmpty() ? "" : ": " + warning)));
    178   }
    179 
    180 
    181   OptionValueDescription clearValue(OptionDefinition optionDefinition)
    182       throws OptionsParsingException {
    183     return optionValues.remove(optionDefinition);
    184   }
    185 
    186   OptionValueDescription getOptionValueDescription(String name) {
    187     OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
    188     if (optionDefinition == null) {
    189       throw new IllegalArgumentException("No such option '" + name + "'");
    190     }
    191     return optionValues.get(optionDefinition);
    192   }
    193 
    194   OptionDescription getOptionDescription(String name) throws OptionsParsingException {
    195     OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
    196     if (optionDefinition == null) {
    197       return null;
    198     }
    199     return new OptionDescription(optionDefinition, optionsData);
    200   }
    201 
    202   /**
    203    * Implementation of {@link OptionsParser#getExpansionValueDescriptions(OptionDefinition,
    204    * OptionInstanceOrigin)}
    205    */
    206   ImmutableList<ParsedOptionDescription> getExpansionValueDescriptions(
    207       OptionDefinition expansionFlag, OptionInstanceOrigin originOfExpansionFlag)
    208       throws OptionsParsingException {
    209     ImmutableList.Builder<ParsedOptionDescription> builder = ImmutableList.builder();
    210     OptionInstanceOrigin originOfSubflags;
    211     ImmutableList<String> options;
    212     if (expansionFlag.hasImplicitRequirements()) {
    213       options = ImmutableList.copyOf(expansionFlag.getImplicitRequirements());
    214       originOfSubflags =
    215           new OptionInstanceOrigin(
    216               originOfExpansionFlag.getPriority(),
    217               String.format(
    218                   "implicitly required by %s (source: %s)",
    219                   expansionFlag, originOfExpansionFlag.getSource()),
    220               expansionFlag,
    221               null);
    222     } else if (expansionFlag.isExpansionOption()) {
    223       options = optionsData.getEvaluatedExpansion(expansionFlag);
    224       originOfSubflags =
    225           new OptionInstanceOrigin(
    226               originOfExpansionFlag.getPriority(),
    227               String.format(
    228                   "expanded by %s (source: %s)", expansionFlag, originOfExpansionFlag.getSource()),
    229               null,
    230               expansionFlag);
    231     } else {
    232       return ImmutableList.of();
    233     }
    234 
    235     Iterator<String> optionsIterator = options.iterator();
    236     while (optionsIterator.hasNext()) {
    237       String unparsedFlagExpression = optionsIterator.next();
    238       ParsedOptionDescription parsedOption =
    239           identifyOptionAndPossibleArgument(
    240               unparsedFlagExpression,
    241               optionsIterator,
    242               originOfSubflags.getPriority(),
    243               o -> originOfSubflags.getSource(),
    244               originOfSubflags.getImplicitDependent(),
    245               originOfSubflags.getExpandedFrom());
    246       builder.add(parsedOption);
    247     }
    248     return builder.build();
    249   }
    250 
    251   boolean containsExplicitOption(String name) {
    252     OptionDefinition optionDefinition = optionsData.getOptionDefinitionFromName(name);
    253     if (optionDefinition == null) {
    254       throw new IllegalArgumentException("No such option '" + name + "'");
    255     }
    256     return optionValues.get(optionDefinition) != null;
    257   }
    258 
    259   /**
    260    * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be
    261    * called recursively. The option's definition dictates how it reacts to multiple settings. By
    262    * default, the arg seen last at the highest priority takes precedence, overriding the early
    263    * values. Options that accumulate multiple values will track them in priority and appearance
    264    * order.
    265    */
    266   List<String> parse(
    267       PriorityCategory priorityCat,
    268       Function<OptionDefinition, String> sourceFunction,
    269       List<String> args)
    270       throws OptionsParsingException {
    271     ResidueAndPriority residueAndPriority =
    272         parse(nextPriorityPerPriorityCategory.get(priorityCat), sourceFunction, null, null, args);
    273     nextPriorityPerPriorityCategory.put(priorityCat, residueAndPriority.nextPriority);
    274     return residueAndPriority.residue;
    275   }
    276 
    277   private static final class ResidueAndPriority {
    278     List<String> residue;
    279     OptionPriority nextPriority;
    280 
    281     public ResidueAndPriority(List<String> residue, OptionPriority nextPriority) {
    282       this.residue = residue;
    283       this.nextPriority = nextPriority;
    284     }
    285   }
    286 
    287   /** Parses the args at the fixed priority. */
    288   List<String> parseOptionsFixedAtSpecificPriority(
    289       OptionPriority priority, Function<OptionDefinition, String> sourceFunction, List<String> args)
    290       throws OptionsParsingException {
    291     ResidueAndPriority residueAndPriority =
    292         parse(OptionPriority.getLockedPriority(priority), sourceFunction, null, null, args);
    293     return residueAndPriority.residue;
    294   }
    295 
    296   /**
    297    * Parses the args, and returns what it doesn't parse. May be called multiple times, and may be
    298    * called recursively. Calls may contain intersecting sets of options; in that case, the arg seen
    299    * last takes precedence.
    300    *
    301    * <p>The method treats options that have neither an implicitDependent nor an expandedFrom value
    302    * as explicitly set.
    303    */
    304   private ResidueAndPriority parse(
    305       OptionPriority priority,
    306       Function<OptionDefinition, String> sourceFunction,
    307       OptionDefinition implicitDependent,
    308       OptionDefinition expandedFrom,
    309       List<String> args)
    310       throws OptionsParsingException {
    311     List<String> unparsedArgs = new ArrayList<>();
    312 
    313     Iterator<String> argsIterator = argsPreProcessor.preProcess(args).iterator();
    314     while (argsIterator.hasNext()) {
    315       String arg = argsIterator.next();
    316 
    317       if (!arg.startsWith("-")) {
    318         unparsedArgs.add(arg);
    319         continue;  // not an option arg
    320       }
    321 
    322       if (arg.equals("--")) {  // "--" means all remaining args aren't options
    323         Iterators.addAll(unparsedArgs, argsIterator);
    324         break;
    325       }
    326 
    327       ParsedOptionDescription parsedOption =
    328           identifyOptionAndPossibleArgument(
    329               arg, argsIterator, priority, sourceFunction, implicitDependent, expandedFrom);
    330       handleNewParsedOption(parsedOption);
    331       priority = OptionPriority.nextOptionPriority(priority);
    332     }
    333 
    334     // Go through the final values and make sure they are valid values for their option. Unlike any
    335     // checks that happened above, this also checks that flags that were not set have a valid
    336     // default value. getValue() will throw if the value is invalid.
    337     for (OptionValueDescription valueDescription : asListOfEffectiveOptions()) {
    338       valueDescription.getValue();
    339     }
    340 
    341     return new ResidueAndPriority(unparsedArgs, priority);
    342   }
    343 
    344   /**
    345    * Implementation of {@link OptionsParser#addOptionValueAtSpecificPriority(OptionInstanceOrigin,
    346    * OptionDefinition, String)}
    347    */
    348   void addOptionValueAtSpecificPriority(
    349       OptionInstanceOrigin origin, OptionDefinition option, String unconvertedValue)
    350       throws OptionsParsingException {
    351     Preconditions.checkNotNull(option);
    352     Preconditions.checkNotNull(
    353         unconvertedValue,
    354         "Cannot set %s to a null value. Pass \"\" if an empty value is required.",
    355         option);
    356     Preconditions.checkNotNull(
    357         origin,
    358         "Cannot assign value \'%s\' to %s without a clear origin for this value.",
    359         unconvertedValue,
    360         option);
    361     PriorityCategory priorityCategory = origin.getPriority().getPriorityCategory();
    362     boolean isNotDefault = priorityCategory != OptionPriority.PriorityCategory.DEFAULT;
    363     Preconditions.checkArgument(
    364         isNotDefault,
    365         "Attempt to assign value \'%s\' to %s at priority %s failed. Cannot set options at "
    366             + "default priority - by definition, that means the option is unset.",
    367         unconvertedValue,
    368         option,
    369         priorityCategory);
    370 
    371     handleNewParsedOption(
    372         new ParsedOptionDescription(
    373             option,
    374             String.format("--%s=%s", option.getOptionName(), unconvertedValue),
    375             unconvertedValue,
    376             origin));
    377   }
    378 
    379   /** Takes care of tracking the parsed option's value in relation to other options. */
    380   private void handleNewParsedOption(ParsedOptionDescription parsedOption)
    381       throws OptionsParsingException {
    382     OptionDefinition optionDefinition = parsedOption.getOptionDefinition();
    383     // All options can be deprecated; check and warn before doing any option-type specific work.
    384     maybeAddDeprecationWarning(optionDefinition);
    385     // Track the value, before any remaining option-type specific work that is done outside of
    386     // the OptionValueDescription.
    387     OptionValueDescription entry =
    388         optionValues.computeIfAbsent(
    389             optionDefinition,
    390             def -> OptionValueDescription.createOptionValueDescription(def, optionsData));
    391     ExpansionBundle expansionBundle = entry.addOptionInstance(parsedOption, warnings);
    392     @Nullable String unconvertedValue = parsedOption.getUnconvertedValue();
    393 
    394     // There are 3 types of flags that expand to other flag values. Expansion flags are the
    395     // accepted way to do this, but two legacy features remain: implicit requirements and wrapper
    396     // options. We rely on the OptionProcessor compile-time check's guarantee that no option sets
    397     // multiple of these behaviors. (In Bazel, --config is another such flag, but that expansion
    398     // is not controlled within the options parser, so we ignore it here)
    399 
    400     // As much as possible, we want the behaviors of these different types of flags to be
    401     // identical, as this minimizes the number of edge cases, but we do not yet track these values
    402     // in the same way. Wrapper options are replaced by their value and implicit requirements are
    403     // hidden from the reported lists of parsed options.
    404     if (parsedOption.getImplicitDependent() == null && !optionDefinition.isWrapperOption()) {
    405       // Log explicit options and expanded options in the order they are parsed (can be sorted
    406       // later). This information is needed to correctly canonicalize flags.
    407       parsedOptions.add(parsedOption);
    408     }
    409 
    410     if (expansionBundle != null) {
    411       ResidueAndPriority residueAndPriority =
    412           parse(
    413               OptionPriority.getLockedPriority(parsedOption.getPriority()),
    414               o -> expansionBundle.sourceOfExpansionArgs,
    415               optionDefinition.hasImplicitRequirements() ? optionDefinition : null,
    416               optionDefinition.isExpansionOption() ? optionDefinition : null,
    417               expansionBundle.expansionArgs);
    418       if (!residueAndPriority.residue.isEmpty()) {
    419         if (optionDefinition.isWrapperOption()) {
    420           throw new OptionsParsingException(
    421               "Unparsed options remain after unwrapping "
    422                   + unconvertedValue
    423                   + ": "
    424                   + Joiner.on(' ').join(residueAndPriority.residue));
    425         } else {
    426           // Throw an assertion here, because this indicates an error in the definition of this
    427           // option's expansion or requirements, not with the input as provided by the user.
    428           throw new AssertionError(
    429               "Unparsed options remain after processing "
    430                   + unconvertedValue
    431                   + ": "
    432                   + Joiner.on(' ').join(residueAndPriority.residue));
    433         }
    434       }
    435     }
    436   }
    437 
    438   private ParsedOptionDescription identifyOptionAndPossibleArgument(
    439       String arg,
    440       Iterator<String> nextArgs,
    441       OptionPriority priority,
    442       Function<OptionDefinition, String> sourceFunction,
    443       OptionDefinition implicitDependent,
    444       OptionDefinition expandedFrom)
    445       throws OptionsParsingException {
    446 
    447     // Store the way this option was parsed on the command line.
    448     StringBuilder commandLineForm = new StringBuilder();
    449     commandLineForm.append(arg);
    450     String unconvertedValue = null;
    451     OptionDefinition optionDefinition;
    452     boolean booleanValue = true;
    453 
    454     if (arg.length() == 2) { // -l  (may be nullary or unary)
    455       optionDefinition = optionsData.getFieldForAbbrev(arg.charAt(1));
    456       booleanValue = true;
    457 
    458     } else if (arg.length() == 3 && arg.charAt(2) == '-') { // -l-  (boolean)
    459       optionDefinition = optionsData.getFieldForAbbrev(arg.charAt(1));
    460       booleanValue = false;
    461 
    462     } else if (allowSingleDashLongOptions // -long_option
    463         || arg.startsWith("--")) { // or --long_option
    464 
    465       int equalsAt = arg.indexOf('=');
    466       int nameStartsAt = arg.startsWith("--") ? 2 : 1;
    467       String name =
    468           equalsAt == -1 ? arg.substring(nameStartsAt) : arg.substring(nameStartsAt, equalsAt);
    469       if (name.trim().isEmpty()) {
    470         throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
    471       }
    472       unconvertedValue = equalsAt == -1 ? null : arg.substring(equalsAt + 1);
    473       optionDefinition = optionsData.getOptionDefinitionFromName(name);
    474 
    475       // Look for a "no"-prefixed option name: "no<optionName>".
    476       if (optionDefinition == null && name.startsWith("no")) {
    477         name = name.substring(2);
    478         optionDefinition = optionsData.getOptionDefinitionFromName(name);
    479         booleanValue = false;
    480         if (optionDefinition != null) {
    481           // TODO(bazel-team): Add tests for these cases.
    482           if (!optionDefinition.usesBooleanValueSyntax()) {
    483             throw new OptionsParsingException(
    484                 "Illegal use of 'no' prefix on non-boolean option: " + arg, arg);
    485           }
    486           if (unconvertedValue != null) {
    487             throw new OptionsParsingException(
    488                 "Unexpected value after boolean option: " + arg, arg);
    489           }
    490           // "no<optionname>" signifies a boolean option w/ false value
    491           unconvertedValue = "0";
    492         }
    493       }
    494     } else {
    495       throw new OptionsParsingException("Invalid options syntax: " + arg, arg);
    496     }
    497 
    498     if (optionDefinition == null
    499         || ImmutableList.copyOf(optionDefinition.getOptionMetadataTags())
    500             .contains(OptionMetadataTag.INTERNAL)) {
    501       // Do not recognize internal options, which are treated as if they did not exist.
    502       throw new OptionsParsingException("Unrecognized option: " + arg, arg);
    503     }
    504 
    505     if (unconvertedValue == null) {
    506       // Special-case boolean to supply value based on presence of "no" prefix.
    507       if (optionDefinition.usesBooleanValueSyntax()) {
    508         unconvertedValue = booleanValue ? "1" : "0";
    509       } else if (optionDefinition.getType().equals(Void.class)
    510           && !optionDefinition.isWrapperOption()) {
    511         // This is expected, Void type options have no args (unless they're wrapper options).
    512       } else if (nextArgs.hasNext()) {
    513         // "--flag value" form
    514         unconvertedValue = nextArgs.next();
    515         commandLineForm.append(" ").append(unconvertedValue);
    516       } else {
    517         throw new OptionsParsingException("Expected value after " + arg);
    518       }
    519     }
    520 
    521     return new ParsedOptionDescription(
    522         optionDefinition,
    523         commandLineForm.toString(),
    524         unconvertedValue,
    525         new OptionInstanceOrigin(
    526             priority, sourceFunction.apply(optionDefinition), implicitDependent, expandedFrom));
    527   }
    528 
    529   /**
    530    * Gets the result of parsing the options.
    531    */
    532   <O extends OptionsBase> O getParsedOptions(Class<O> optionsClass) {
    533     // Create the instance:
    534     O optionsInstance;
    535     try {
    536       Constructor<O> constructor = optionsData.getConstructor(optionsClass);
    537       if (constructor == null) {
    538         return null;
    539       }
    540       optionsInstance = constructor.newInstance();
    541     } catch (ReflectiveOperationException e) {
    542       throw new IllegalStateException("Error while instantiating options class", e);
    543     }
    544 
    545     // Set the fields
    546     for (OptionDefinition optionDefinition :
    547         OptionsData.getAllOptionDefinitionsForClass(optionsClass)) {
    548       Object value;
    549       OptionValueDescription optionValue = optionValues.get(optionDefinition);
    550       if (optionValue == null) {
    551         value = optionDefinition.getDefaultValue();
    552       } else {
    553         value = optionValue.getValue();
    554       }
    555       try {
    556         optionDefinition.getField().set(optionsInstance, value);
    557       } catch (IllegalArgumentException e) {
    558         throw new IllegalStateException(
    559             String.format("Unable to set %s to value '%s'.", optionDefinition, value), e);
    560       } catch (IllegalAccessException e) {
    561         throw new IllegalStateException(
    562             "Could not set the field due to access issues. This is impossible, as the "
    563                 + "OptionProcessor checks that all options are non-final public fields.",
    564             e);
    565       }
    566     }
    567     return optionsInstance;
    568   }
    569 
    570   List<String> getWarnings() {
    571     return ImmutableList.copyOf(warnings);
    572   }
    573 }
    574