Damien Martin-Guillerez | f88f4d8 | 2015-09-25 13:56:55 +0000 | [diff] [blame] | 1 | // Copyright 2014 The Bazel Authors. All rights reserved. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 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 | package com.google.devtools.common.options; |
| 15 | |
warkahscott | de77148 | 2020-10-08 08:25:21 -0700 | [diff] [blame] | 16 | import static com.google.devtools.common.options.OptionsParser.STARLARK_SKIPPED_PREFIXES; |
| 17 | |
seancurran | b158a0f | 2021-06-21 13:57:22 -0700 | [diff] [blame] | 18 | import com.github.benmanes.caffeine.cache.CaffeineSpec; |
dlr | 577d907 | 2020-06-26 10:36:04 -0700 | [diff] [blame] | 19 | import com.google.common.base.Ascii; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 20 | import com.google.common.base.Splitter; |
| 21 | import com.google.common.collect.ImmutableList; |
ccalvarin | 3e44d5b | 2017-08-31 06:32:03 +0200 | [diff] [blame] | 22 | import com.google.common.collect.ImmutableMap; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 23 | import com.google.common.collect.Maps; |
ulfjack | f9625f0 | 2017-07-24 13:01:29 +0200 | [diff] [blame] | 24 | import java.time.Duration; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 25 | import java.util.Iterator; |
| 26 | import java.util.List; |
| 27 | import java.util.Map; |
| 28 | import java.util.logging.Level; |
ulfjack | f9625f0 | 2017-07-24 13:01:29 +0200 | [diff] [blame] | 29 | import java.util.regex.Matcher; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 30 | import java.util.regex.Pattern; |
| 31 | import java.util.regex.PatternSyntaxException; |
| 32 | |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 33 | /** Some convenient converters used by blaze. Note: These are specific to blaze. */ |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 34 | public final class Converters { |
warkahscott | de77148 | 2020-10-08 08:25:21 -0700 | [diff] [blame] | 35 | /** |
| 36 | * The name of the flag used for shorthand aliasing in blaze. {@see |
| 37 | * com.google.devtools.build.lib.analysis.config.CoreOptions#commandLineFlagAliases} for the |
| 38 | * option definition. |
| 39 | */ |
| 40 | public static final String BLAZE_ALIASING_FLAG = "flag_alias"; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 41 | |
dlr | 577d907 | 2020-06-26 10:36:04 -0700 | [diff] [blame] | 42 | private static final ImmutableList<String> ENABLED_REPS = |
| 43 | ImmutableList.of("true", "1", "yes", "t", "y"); |
| 44 | |
| 45 | private static final ImmutableList<String> DISABLED_REPS = |
| 46 | ImmutableList.of("false", "0", "no", "f", "n"); |
| 47 | |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 48 | /** Standard converter for booleans. Accepts common shorthands/synonyms. */ |
| 49 | public static class BooleanConverter implements Converter<Boolean> { |
| 50 | @Override |
| 51 | public Boolean convert(String input) throws OptionsParsingException { |
| 52 | if (input == null) { |
| 53 | return false; |
| 54 | } |
dlr | 577d907 | 2020-06-26 10:36:04 -0700 | [diff] [blame] | 55 | input = Ascii.toLowerCase(input); |
| 56 | if (ENABLED_REPS.contains(input)) { |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 57 | return true; |
| 58 | } |
dlr | 577d907 | 2020-06-26 10:36:04 -0700 | [diff] [blame] | 59 | if (DISABLED_REPS.contains(input)) { |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 60 | return false; |
| 61 | } |
| 62 | throw new OptionsParsingException("'" + input + "' is not a boolean"); |
| 63 | } |
| 64 | |
| 65 | @Override |
| 66 | public String getTypeDescription() { |
| 67 | return "a boolean"; |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | /** Standard converter for Strings. */ |
| 72 | public static class StringConverter implements Converter<String> { |
| 73 | @Override |
| 74 | public String convert(String input) { |
| 75 | return input; |
| 76 | } |
| 77 | |
| 78 | @Override |
| 79 | public String getTypeDescription() { |
| 80 | return "a string"; |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | /** Standard converter for integers. */ |
| 85 | public static class IntegerConverter implements Converter<Integer> { |
| 86 | @Override |
| 87 | public Integer convert(String input) throws OptionsParsingException { |
| 88 | try { |
| 89 | return Integer.decode(input); |
| 90 | } catch (NumberFormatException e) { |
Googler | caa0451 | 2022-03-07 15:55:06 -0800 | [diff] [blame] | 91 | throw new OptionsParsingException("'" + input + "' is not an int", e); |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 92 | } |
| 93 | } |
| 94 | |
| 95 | @Override |
| 96 | public String getTypeDescription() { |
| 97 | return "an integer"; |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | /** Standard converter for longs. */ |
| 102 | public static class LongConverter implements Converter<Long> { |
| 103 | @Override |
| 104 | public Long convert(String input) throws OptionsParsingException { |
| 105 | try { |
| 106 | return Long.decode(input); |
| 107 | } catch (NumberFormatException e) { |
Googler | caa0451 | 2022-03-07 15:55:06 -0800 | [diff] [blame] | 108 | throw new OptionsParsingException("'" + input + "' is not a long", e); |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 109 | } |
| 110 | } |
| 111 | |
| 112 | @Override |
| 113 | public String getTypeDescription() { |
| 114 | return "a long integer"; |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | /** Standard converter for doubles. */ |
| 119 | public static class DoubleConverter implements Converter<Double> { |
| 120 | @Override |
| 121 | public Double convert(String input) throws OptionsParsingException { |
| 122 | try { |
| 123 | return Double.parseDouble(input); |
| 124 | } catch (NumberFormatException e) { |
Googler | caa0451 | 2022-03-07 15:55:06 -0800 | [diff] [blame] | 125 | throw new OptionsParsingException("'" + input + "' is not a double", e); |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 126 | } |
| 127 | } |
| 128 | |
| 129 | @Override |
| 130 | public String getTypeDescription() { |
| 131 | return "a double"; |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | /** Standard converter for TriState values. */ |
| 136 | public static class TriStateConverter implements Converter<TriState> { |
| 137 | @Override |
| 138 | public TriState convert(String input) throws OptionsParsingException { |
| 139 | if (input == null) { |
| 140 | return TriState.AUTO; |
| 141 | } |
dlr | 577d907 | 2020-06-26 10:36:04 -0700 | [diff] [blame] | 142 | input = Ascii.toLowerCase(input); |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 143 | if (input.equals("auto")) { |
| 144 | return TriState.AUTO; |
| 145 | } |
dlr | 577d907 | 2020-06-26 10:36:04 -0700 | [diff] [blame] | 146 | if (ENABLED_REPS.contains(input)) { |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 147 | return TriState.YES; |
| 148 | } |
dlr | 577d907 | 2020-06-26 10:36:04 -0700 | [diff] [blame] | 149 | if (DISABLED_REPS.contains(input)) { |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 150 | return TriState.NO; |
| 151 | } |
| 152 | throw new OptionsParsingException("'" + input + "' is not a boolean"); |
| 153 | } |
| 154 | |
| 155 | @Override |
| 156 | public String getTypeDescription() { |
| 157 | return "a tri-state (auto, yes, no)"; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * Standard "converter" for Void. Should not actually be invoked. For instance, expansion flags |
| 163 | * are usually Void-typed and do not invoke the converter. |
| 164 | */ |
| 165 | public static class VoidConverter implements Converter<Void> { |
| 166 | @Override |
| 167 | public Void convert(String input) throws OptionsParsingException { |
ccalvarin | 3e44d5b | 2017-08-31 06:32:03 +0200 | [diff] [blame] | 168 | if (input == null || input.equals("null")) { |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 169 | return null; // expected input, return is unused so null is fine. |
| 170 | } |
| 171 | throw new OptionsParsingException("'" + input + "' unexpected"); |
| 172 | } |
| 173 | |
| 174 | @Override |
| 175 | public String getTypeDescription() { |
| 176 | return ""; |
| 177 | } |
| 178 | } |
| 179 | |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 180 | /** Standard converter for the {@link java.time.Duration} type. */ |
ulfjack | f9625f0 | 2017-07-24 13:01:29 +0200 | [diff] [blame] | 181 | public static class DurationConverter implements Converter<Duration> { |
| 182 | private final Pattern durationRegex = Pattern.compile("^([0-9]+)(d|h|m|s|ms)$"); |
| 183 | |
| 184 | @Override |
| 185 | public Duration convert(String input) throws OptionsParsingException { |
| 186 | // To be compatible with the previous parser, '0' doesn't need a unit. |
| 187 | if ("0".equals(input)) { |
| 188 | return Duration.ZERO; |
| 189 | } |
| 190 | Matcher m = durationRegex.matcher(input); |
| 191 | if (!m.matches()) { |
| 192 | throw new OptionsParsingException("Illegal duration '" + input + "'."); |
| 193 | } |
| 194 | long duration = Long.parseLong(m.group(1)); |
| 195 | String unit = m.group(2); |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 196 | switch (unit) { |
ulfjack | f9625f0 | 2017-07-24 13:01:29 +0200 | [diff] [blame] | 197 | case "d": |
| 198 | return Duration.ofDays(duration); |
| 199 | case "h": |
| 200 | return Duration.ofHours(duration); |
| 201 | case "m": |
| 202 | return Duration.ofMinutes(duration); |
| 203 | case "s": |
| 204 | return Duration.ofSeconds(duration); |
| 205 | case "ms": |
| 206 | return Duration.ofMillis(duration); |
| 207 | default: |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 208 | throw new IllegalStateException( |
| 209 | "This must not happen. Did you update the regex without the switch case?"); |
ulfjack | f9625f0 | 2017-07-24 13:01:29 +0200 | [diff] [blame] | 210 | } |
| 211 | } |
| 212 | |
| 213 | @Override |
| 214 | public String getTypeDescription() { |
| 215 | return "An immutable length of time."; |
| 216 | } |
| 217 | } |
| 218 | |
ccalvarin | 3e44d5b | 2017-08-31 06:32:03 +0200 | [diff] [blame] | 219 | // 1:1 correspondence with UsesOnlyCoreTypes.CORE_TYPES. |
ulfjack | f9625f0 | 2017-07-24 13:01:29 +0200 | [diff] [blame] | 220 | /** |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 221 | * The converters that are available to the options parser by default. These are used if the |
| 222 | * {@code @Option} annotation does not specify its own {@code converter}, and its type is one of |
| 223 | * the following. |
| 224 | */ |
ccalvarin | 3e44d5b | 2017-08-31 06:32:03 +0200 | [diff] [blame] | 225 | public static final ImmutableMap<Class<?>, Converter<?>> DEFAULT_CONVERTERS = |
| 226 | new ImmutableMap.Builder<Class<?>, Converter<?>>() |
| 227 | .put(String.class, new Converters.StringConverter()) |
| 228 | .put(int.class, new Converters.IntegerConverter()) |
| 229 | .put(long.class, new Converters.LongConverter()) |
| 230 | .put(double.class, new Converters.DoubleConverter()) |
| 231 | .put(boolean.class, new Converters.BooleanConverter()) |
| 232 | .put(TriState.class, new Converters.TriStateConverter()) |
| 233 | .put(Duration.class, new Converters.DurationConverter()) |
| 234 | .put(Void.class, new Converters.VoidConverter()) |
| 235 | .build(); |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 236 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 237 | /** |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 238 | * Join a list of words as in English. Examples: "nothing" "one" "one or two" "one and two" "one, |
| 239 | * two or three". "one, two and three". The toString method of each element is used. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 240 | */ |
| 241 | static String joinEnglishList(Iterable<?> choices) { |
| 242 | StringBuilder buf = new StringBuilder(); |
| 243 | for (Iterator<?> ii = choices.iterator(); ii.hasNext(); ) { |
| 244 | Object choice = ii.next(); |
| 245 | if (buf.length() > 0) { |
| 246 | buf.append(ii.hasNext() ? ", " : " or "); |
| 247 | } |
| 248 | buf.append(choice); |
| 249 | } |
| 250 | return buf.length() == 0 ? "nothing" : buf.toString(); |
| 251 | } |
| 252 | |
janakr | 49625ea | 2022-03-18 19:10:01 -0700 | [diff] [blame] | 253 | /** Converter for a list of options, separated by some separator character. */ |
| 254 | public static class SeparatedOptionListConverter implements Converter<ImmutableList<String>> { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 255 | private final String separatorDescription; |
| 256 | private final Splitter splitter; |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 257 | private final boolean allowEmptyValues; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 258 | |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 259 | protected SeparatedOptionListConverter( |
| 260 | char separator, String separatorDescription, boolean allowEmptyValues) { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 261 | this.separatorDescription = separatorDescription; |
| 262 | this.splitter = Splitter.on(separator); |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 263 | this.allowEmptyValues = allowEmptyValues; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 264 | } |
| 265 | |
| 266 | @Override |
janakr | 49625ea | 2022-03-18 19:10:01 -0700 | [diff] [blame] | 267 | public ImmutableList<String> convert(String input) throws OptionsParsingException { |
| 268 | ImmutableList<String> result = |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 269 | input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input)); |
| 270 | if (!allowEmptyValues && result.contains("")) { |
philwo | 849113c | 2019-02-20 15:09:30 -0800 | [diff] [blame] | 271 | // If the list contains exactly the empty string, it means an empty value was passed and we |
| 272 | // should instead return an empty list. |
| 273 | if (result.size() == 1) { |
| 274 | return ImmutableList.of(); |
| 275 | } |
| 276 | |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 277 | throw new OptionsParsingException( |
| 278 | "Empty values are not allowed as part of this " + getTypeDescription()); |
| 279 | } |
| 280 | return result; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 281 | } |
| 282 | |
| 283 | @Override |
| 284 | public String getTypeDescription() { |
| 285 | return separatorDescription + "-separated list of options"; |
| 286 | } |
| 287 | } |
| 288 | |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 289 | public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 290 | public CommaSeparatedOptionListConverter() { |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 291 | super(',', "comma", true); |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | public static class CommaSeparatedNonEmptyOptionListConverter |
| 296 | extends SeparatedOptionListConverter { |
| 297 | public CommaSeparatedNonEmptyOptionListConverter() { |
| 298 | super(',', "comma", false); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 299 | } |
| 300 | } |
| 301 | |
| 302 | public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter { |
| 303 | public ColonSeparatedOptionListConverter() { |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 304 | super(':', "colon", true); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 305 | } |
| 306 | } |
| 307 | |
| 308 | public static class LogLevelConverter implements Converter<Level> { |
| 309 | |
seancurran | b158a0f | 2021-06-21 13:57:22 -0700 | [diff] [blame] | 310 | static final ImmutableList<Level> LEVELS = |
| 311 | ImmutableList.of( |
| 312 | Level.OFF, |
| 313 | Level.SEVERE, |
| 314 | Level.WARNING, |
| 315 | Level.INFO, |
| 316 | Level.FINE, |
| 317 | Level.FINER, |
| 318 | Level.FINEST); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 319 | |
| 320 | @Override |
| 321 | public Level convert(String input) throws OptionsParsingException { |
| 322 | try { |
| 323 | int level = Integer.parseInt(input); |
seancurran | b158a0f | 2021-06-21 13:57:22 -0700 | [diff] [blame] | 324 | return LEVELS.get(level); |
Jonathan Bluett-Duncan | 0df3ddbd | 2017-08-09 11:13:54 +0200 | [diff] [blame] | 325 | } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) { |
Googler | caa0451 | 2022-03-07 15:55:06 -0800 | [diff] [blame] | 326 | throw new OptionsParsingException("Not a log level: " + input, e); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 327 | } |
| 328 | } |
| 329 | |
| 330 | @Override |
| 331 | public String getTypeDescription() { |
seancurran | b158a0f | 2021-06-21 13:57:22 -0700 | [diff] [blame] | 332 | return "0 <= an integer <= " + (LEVELS.size() - 1); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 333 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 334 | } |
| 335 | |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 336 | /** Checks whether a string is part of a set of strings. */ |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 337 | public static class StringSetConverter implements Converter<String> { |
| 338 | |
| 339 | // TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/ |
| 340 | // here. |
| 341 | private final List<String> values; |
| 342 | |
| 343 | public StringSetConverter(String... values) { |
| 344 | this.values = ImmutableList.copyOf(values); |
| 345 | } |
| 346 | |
| 347 | @Override |
| 348 | public String convert(String input) throws OptionsParsingException { |
| 349 | if (values.contains(input)) { |
| 350 | return input; |
| 351 | } |
| 352 | |
| 353 | throw new OptionsParsingException("Not one of " + values); |
| 354 | } |
| 355 | |
| 356 | @Override |
| 357 | public String getTypeDescription() { |
| 358 | return joinEnglishList(values); |
| 359 | } |
| 360 | } |
| 361 | |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 362 | /** Checks whether a string is a valid regex pattern and compiles it. */ |
Googler | 21b759d | 2019-04-03 10:57:09 -0700 | [diff] [blame] | 363 | public static class RegexPatternConverter implements Converter<RegexPatternOption> { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 364 | |
| 365 | @Override |
Googler | 21b759d | 2019-04-03 10:57:09 -0700 | [diff] [blame] | 366 | public RegexPatternOption convert(String input) throws OptionsParsingException { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 367 | try { |
Googler | 21b759d | 2019-04-03 10:57:09 -0700 | [diff] [blame] | 368 | return RegexPatternOption.create(Pattern.compile(input)); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 369 | } catch (PatternSyntaxException e) { |
| 370 | throw new OptionsParsingException("Not a valid regular expression: " + e.getMessage()); |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | @Override |
| 375 | public String getTypeDescription() { |
| 376 | return "a valid Java regular expression"; |
| 377 | } |
| 378 | } |
| 379 | |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 380 | /** Limits the length of a string argument. */ |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 381 | public static class LengthLimitingConverter implements Converter<String> { |
| 382 | private final int maxSize; |
| 383 | |
| 384 | public LengthLimitingConverter(int maxSize) { |
| 385 | this.maxSize = maxSize; |
| 386 | } |
| 387 | |
| 388 | @Override |
| 389 | public String convert(String input) throws OptionsParsingException { |
| 390 | if (input.length() > maxSize) { |
| 391 | throw new OptionsParsingException("Input must be " + getTypeDescription()); |
| 392 | } |
| 393 | return input; |
| 394 | } |
| 395 | |
| 396 | @Override |
| 397 | public String getTypeDescription() { |
| 398 | return "a string <= " + maxSize + " characters"; |
| 399 | } |
| 400 | } |
| 401 | |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 402 | /** Checks whether an integer is in the given range. */ |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 403 | public static class RangeConverter implements Converter<Integer> { |
| 404 | final int minValue; |
| 405 | final int maxValue; |
| 406 | |
| 407 | public RangeConverter(int minValue, int maxValue) { |
| 408 | this.minValue = minValue; |
| 409 | this.maxValue = maxValue; |
| 410 | } |
| 411 | |
| 412 | @Override |
| 413 | public Integer convert(String input) throws OptionsParsingException { |
| 414 | try { |
| 415 | Integer value = Integer.parseInt(input); |
| 416 | if (value < minValue) { |
| 417 | throw new OptionsParsingException("'" + input + "' should be >= " + minValue); |
| 418 | } else if (value < minValue || value > maxValue) { |
| 419 | throw new OptionsParsingException("'" + input + "' should be <= " + maxValue); |
| 420 | } |
| 421 | return value; |
| 422 | } catch (NumberFormatException e) { |
Googler | caa0451 | 2022-03-07 15:55:06 -0800 | [diff] [blame] | 423 | throw new OptionsParsingException("'" + input + "' is not an int", e); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 424 | } |
| 425 | } |
| 426 | |
| 427 | @Override |
| 428 | public String getTypeDescription() { |
| 429 | if (minValue == Integer.MIN_VALUE) { |
| 430 | if (maxValue == Integer.MAX_VALUE) { |
| 431 | return "an integer"; |
| 432 | } else { |
| 433 | return "an integer, <= " + maxValue; |
| 434 | } |
| 435 | } else if (maxValue == Integer.MAX_VALUE) { |
| 436 | return "an integer, >= " + minValue; |
| 437 | } else { |
| 438 | return "an integer in " |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 439 | + (minValue < 0 ? "(" + minValue + ")" : minValue) |
| 440 | + "-" |
| 441 | + maxValue |
| 442 | + " range"; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 443 | } |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | /** |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 448 | * A converter for variable assignments from the parameter list of a blaze command invocation. |
| 449 | * Assignments are expected to have the form "name=value", where names and values are defined to |
| 450 | * be as permissive as possible. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 451 | */ |
| 452 | public static class AssignmentConverter implements Converter<Map.Entry<String, String>> { |
| 453 | |
| 454 | @Override |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 455 | public Map.Entry<String, String> convert(String input) throws OptionsParsingException { |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 456 | int pos = input.indexOf("="); |
| 457 | if (pos <= 0) { |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 458 | throw new OptionsParsingException( |
| 459 | "Variable definitions must be in the form of a 'name=value' assignment"); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 460 | } |
| 461 | String name = input.substring(0, pos); |
| 462 | String value = input.substring(pos + 1); |
| 463 | return Maps.immutableEntry(name, value); |
| 464 | } |
| 465 | |
| 466 | @Override |
| 467 | public String getTypeDescription() { |
| 468 | return "a 'name=value' assignment"; |
| 469 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 470 | } |
| 471 | |
| 472 | /** |
warkahscott | de77148 | 2020-10-08 08:25:21 -0700 | [diff] [blame] | 473 | * A converter for command line flag aliases. It does additional validation on the name and value |
| 474 | * of the assignment to ensure they conform to the naming limitations. |
| 475 | */ |
| 476 | public static class FlagAliasConverter extends AssignmentConverter { |
| 477 | |
| 478 | @Override |
| 479 | public Map.Entry<String, String> convert(String input) throws OptionsParsingException { |
| 480 | Map.Entry<String, String> entry = super.convert(input); |
| 481 | String shortForm = entry.getKey(); |
| 482 | String longForm = entry.getValue(); |
| 483 | |
| 484 | String cmdLineAlias = "--" + BLAZE_ALIASING_FLAG + "=" + input; |
| 485 | |
| 486 | if (!Pattern.matches("([\\w])*", shortForm)) { |
| 487 | throw new OptionsParsingException( |
| 488 | shortForm + " should only consist of word characters to be a valid alias name.", |
| 489 | cmdLineAlias); |
| 490 | } |
| 491 | if (longForm.contains("=")) { |
| 492 | throw new OptionsParsingException( |
| 493 | "--" + BLAZE_ALIASING_FLAG + " does not support flag value assignment.", cmdLineAlias); |
| 494 | } |
| 495 | |
| 496 | // Remove this check if native options are permitted to be aliased |
| 497 | longForm = "--" + longForm; |
| 498 | if (STARLARK_SKIPPED_PREFIXES.stream().noneMatch(longForm::startsWith)) { |
| 499 | throw new OptionsParsingException( |
| 500 | "--" + BLAZE_ALIASING_FLAG + " only supports Starlark build settings.", cmdLineAlias); |
| 501 | } |
| 502 | |
| 503 | return entry; |
| 504 | } |
| 505 | |
| 506 | @Override |
| 507 | public String getTypeDescription() { |
| 508 | return "a 'name=value' flag alias"; |
| 509 | } |
| 510 | } |
| 511 | |
| 512 | /** |
schmitt | 61e8013 | 2019-11-01 11:38:55 -0700 | [diff] [blame] | 513 | * Base converter for assignments from a value to a list of values. Both the key type as well as |
| 514 | * the type for all instances in the list of values are processed via passed converters. |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 515 | */ |
schmitt | 61e8013 | 2019-11-01 11:38:55 -0700 | [diff] [blame] | 516 | public abstract static class AssignmentToListOfValuesConverter<K, V> |
| 517 | implements Converter<Map.Entry<K, List<V>>> { |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 518 | |
schmitt | 61e8013 | 2019-11-01 11:38:55 -0700 | [diff] [blame] | 519 | /** Whether to allow keys in the assignment to be empty (i.e. just a list of values) */ |
| 520 | public enum AllowEmptyKeys { |
| 521 | YES, |
| 522 | NO |
| 523 | } |
| 524 | |
| 525 | private static final Splitter SPLITTER = Splitter.on(','); |
| 526 | |
| 527 | private final Converter<K> keyConverter; |
| 528 | private final Converter<V> valueConverter; |
| 529 | private final AllowEmptyKeys allowEmptyKeys; |
| 530 | |
| 531 | public AssignmentToListOfValuesConverter( |
| 532 | Converter<K> keyConverter, Converter<V> valueConverter, AllowEmptyKeys allowEmptyKeys) { |
| 533 | this.keyConverter = keyConverter; |
| 534 | this.valueConverter = valueConverter; |
| 535 | this.allowEmptyKeys = allowEmptyKeys; |
| 536 | } |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 537 | |
| 538 | @Override |
schmitt | 61e8013 | 2019-11-01 11:38:55 -0700 | [diff] [blame] | 539 | public Map.Entry<K, List<V>> convert(String input) throws OptionsParsingException { |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 540 | int pos = input.indexOf("="); |
schmitt | 61e8013 | 2019-11-01 11:38:55 -0700 | [diff] [blame] | 541 | if (allowEmptyKeys == AllowEmptyKeys.NO && pos <= 0) { |
| 542 | throw new OptionsParsingException( |
| 543 | "Must be in the form of a 'key=value[,value]' assignment"); |
| 544 | } |
| 545 | |
| 546 | String key = pos <= 0 ? "" : input.substring(0, pos); |
| 547 | List<String> values = SPLITTER.splitToList(input.substring(pos + 1)); |
| 548 | if (values.contains("")) { |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 549 | // If the list contains exactly the empty string, it means an empty value was passed and we |
| 550 | // should instead return an empty list. |
schmitt | 61e8013 | 2019-11-01 11:38:55 -0700 | [diff] [blame] | 551 | if (values.size() == 1) { |
| 552 | values = ImmutableList.of(); |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 553 | } else { |
| 554 | throw new OptionsParsingException( |
| 555 | "Variable definitions must not contain empty strings or leading / trailing commas"); |
| 556 | } |
| 557 | } |
schmitt | 61e8013 | 2019-11-01 11:38:55 -0700 | [diff] [blame] | 558 | ImmutableList.Builder<V> convertedValues = ImmutableList.builder(); |
| 559 | for (String value : values) { |
| 560 | convertedValues.add(valueConverter.convert(value)); |
| 561 | } |
| 562 | return Maps.immutableEntry(keyConverter.convert(key), convertedValues.build()); |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | /** |
| 567 | * A converter for variable assignments from the parameter list of a blaze command invocation. |
| 568 | * Assignments are expected to have the form {@code [name=]value1[,..,valueN]}, where names and |
| 569 | * values are defined to be as permissive as possible. If no name is provided, "" is used. |
| 570 | */ |
| 571 | public static class StringToStringListConverter |
| 572 | extends AssignmentToListOfValuesConverter<String, String> { |
| 573 | |
| 574 | public StringToStringListConverter() { |
| 575 | super(new StringConverter(), new StringConverter(), AllowEmptyKeys.YES); |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 576 | } |
| 577 | |
| 578 | @Override |
| 579 | public String getTypeDescription() { |
steinman | 0588d1f | 2019-07-26 14:20:22 -0700 | [diff] [blame] | 580 | return "a '[name=]value1[,..,valueN]' assignment"; |
philwo | 207ac6e | 2019-02-20 12:44:06 -0800 | [diff] [blame] | 581 | } |
| 582 | } |
| 583 | |
| 584 | /** |
| 585 | * A converter for variable assignments from the parameter list of a blaze command invocation. |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 586 | * Assignments are expected to have the form "name[=value]", where names and values are defined to |
| 587 | * be as permissive as possible and value part can be optional (in which case it is considered to |
| 588 | * be null). |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 589 | */ |
| 590 | public static class OptionalAssignmentConverter implements Converter<Map.Entry<String, String>> { |
| 591 | |
| 592 | @Override |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 593 | public Map.Entry<String, String> convert(String input) throws OptionsParsingException { |
| 594 | int pos = input.indexOf('='); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 595 | if (pos == 0 || input.length() == 0) { |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 596 | throw new OptionsParsingException( |
| 597 | "Variable definitions must be in the form of a 'name=value' or 'name' assignment"); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 598 | } else if (pos < 0) { |
| 599 | return Maps.immutableEntry(input, null); |
| 600 | } |
| 601 | String name = input.substring(0, pos); |
| 602 | String value = input.substring(pos + 1); |
| 603 | return Maps.immutableEntry(name, value); |
| 604 | } |
| 605 | |
| 606 | @Override |
| 607 | public String getTypeDescription() { |
| 608 | return "a 'name=value' assignment with an optional value part"; |
| 609 | } |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 610 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 611 | |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 612 | /** |
| 613 | * A converter for named integers of the form "[name=]value". When no name is specified, an empty |
| 614 | * string is used for the key. |
| 615 | */ |
| 616 | public static class NamedIntegersConverter implements Converter<Map.Entry<String, Integer>> { |
| 617 | |
| 618 | @Override |
| 619 | public Map.Entry<String, Integer> convert(String input) throws OptionsParsingException { |
| 620 | int pos = input.indexOf('='); |
| 621 | if (pos == 0 || input.length() == 0) { |
| 622 | throw new OptionsParsingException( |
| 623 | "Specify either 'value' or 'name=value', where 'value' is an integer"); |
| 624 | } else if (pos < 0) { |
| 625 | try { |
| 626 | return Maps.immutableEntry("", Integer.parseInt(input)); |
| 627 | } catch (NumberFormatException e) { |
Googler | caa0451 | 2022-03-07 15:55:06 -0800 | [diff] [blame] | 628 | throw new OptionsParsingException("'" + input + "' is not an int", e); |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 629 | } |
| 630 | } |
| 631 | String name = input.substring(0, pos); |
| 632 | String value = input.substring(pos + 1); |
| 633 | try { |
| 634 | return Maps.immutableEntry(name, Integer.parseInt(value)); |
| 635 | } catch (NumberFormatException e) { |
Googler | caa0451 | 2022-03-07 15:55:06 -0800 | [diff] [blame] | 636 | throw new OptionsParsingException("'" + value + "' is not an int", e); |
Googler | 0c12603 | 2018-05-03 06:43:51 -0700 | [diff] [blame] | 637 | } |
| 638 | } |
| 639 | |
| 640 | @Override |
| 641 | public String getTypeDescription() { |
| 642 | return "an integer or a named integer, 'name=value'"; |
| 643 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 644 | } |
| 645 | |
| 646 | public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> { |
| 647 | public HelpVerbosityConverter() { |
| 648 | super(OptionsParser.HelpVerbosity.class, "--help_verbosity setting"); |
| 649 | } |
| 650 | } |
| 651 | |
Googler | 2db5411 | 2017-06-02 17:48:48 -0400 | [diff] [blame] | 652 | /** |
| 653 | * A converter to check whether an integer denoting a percentage is in a valid range: [0, 100]. |
| 654 | */ |
| 655 | public static class PercentageConverter extends RangeConverter { |
| 656 | public PercentageConverter() { |
| 657 | super(0, 100); |
| 658 | } |
| 659 | } |
felly | eb5f21d | 2019-12-05 14:20:23 -0800 | [diff] [blame] | 660 | |
| 661 | /** |
seancurran | b158a0f | 2021-06-21 13:57:22 -0700 | [diff] [blame] | 662 | * A {@link Converter} for {@link com.github.benmanes.caffeine.cache.CaffeineSpec}. The spec may |
| 663 | * be empty, in which case this converter returns null. |
| 664 | */ |
| 665 | public static final class CaffeineSpecConverter implements Converter<CaffeineSpec> { |
| 666 | @Override |
| 667 | public CaffeineSpec convert(String spec) throws OptionsParsingException { |
| 668 | try { |
| 669 | return CaffeineSpec.parse(spec); |
| 670 | } catch (IllegalArgumentException e) { |
| 671 | throw new OptionsParsingException("Failed to parse CaffeineSpec: " + e.getMessage(), e); |
| 672 | } |
| 673 | } |
| 674 | |
| 675 | @Override |
| 676 | public String getTypeDescription() { |
| 677 | return "Converts to a CaffeineSpec, or null if the input is empty"; |
| 678 | } |
| 679 | } |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 680 | } |